「昨日まで通った期限判定テストが、今日になったら落ちた」。期限を固定して期待結果だけ書き、判定の中でLocalDate.now()を呼んでいると起こる失敗です。テストのたびに予定日を書き換える運用では、期限当日と翌日の境界を保証できません。
ここで取る方針は、現在日時を業務ロジックの入力として扱い、Clockを渡すことです。題材は「締切日当日まで申請でき、翌日からできない」という架空の申請機能。画面表示やDB保存ではなく、期限判定を毎日同じ条件で確かめます。
現在日時をコード内に埋め込むと、テストから動かせない
次の形では、テストに2026-04-01の締切日を渡せても、「今」がOSの時計で決まります。
boolean canSubmit(LocalDate deadline) {
return !LocalDate.now().isAfter(deadline);
}たとえば4月1日に実行すればtrue、4月2日に実行すればfalse。どちらも正しい動きですが、同じテストケースの期待値を固定できません。Thread.sleep()で日付境界を待つ方法は、実行時間も結果も不安定にします。
Clockを受け取る期限判定にする
例の前提はJava 17、JUnit Jupiter 5.11.4、Maven Surefire 3.5.2です。単独プロジェクトならpom.xmlを次のようにします。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId><artifactId>deadline-rule</artifactId><version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies><dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId><version>5.11.4</version><scope>test</scope>
</dependency></dependencies>
<build><plugins><plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId><version>3.5.2</version>
</plugin></plugins></build>
</project>src/main/java/example/DeadlinePolicy.java。LocalDate.now(clock)は、受け取った時計の時刻とタイムゾーンで「今日」を決めます。締切日と同じ日は許可し、翌日になれば拒否します。
package example;
import java.time.Clock;
import java.time.LocalDate;
import java.util.Objects;
public final class DeadlinePolicy {
private final Clock clock;
public DeadlinePolicy(Clock clock) {
this.clock = Objects.requireNonNull(clock);
}
public boolean canSubmit(LocalDate deadline) {
Objects.requireNonNull(deadline);
LocalDate today = LocalDate.now(clock);
return !today.isAfter(deadline);
}
}本番で使うときは、たとえばnew DeadlinePolicy(Clock.system(ZoneId.of("Asia/Tokyo")))と作ります。SpringならClockをBeanとして注入しても構いません。この例では、申請期限の基準地域を日本時間と決めました。海外利用者の現地日付を基準とするサービスなら、利用者のタイムゾーンをどう決めるかが別の仕様です。
同じ時刻を固定し、日付の切り替わりを確かめる
src/test/java/example/DeadlinePolicyTest.java。UTCで2026年4月1日15時は、日本時間では4月2日0時です。その直前と直後を置くと、タイムゾーンの設定漏れを見つけやすくなります。
| 固定するUTC時刻 | 日本時間の日付 | 締切日 | 申請可能か |
|---|---|---|---|
| 4月1日14:59:59 | 4月1日 | 4月1日 | ○ |
| 4月1日15:00:00 | 4月2日 | 4月1日 | × |
| 4月1日15:00:00 | 4月2日 | 4月2日 | ○ |

package example;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
class DeadlinePolicyTest {
private static final ZoneId JAPAN = ZoneId.of("Asia/Tokyo");
@ParameterizedTest(name = "UTC={0}, 締切={1} → 申請可能={2}")
@CsvSource({
"2026-04-01T14:59:59Z, 2026-04-01, true",
"2026-04-01T15:00:00Z, 2026-04-01, false",
"2026-04-01T15:00:00Z, 2026-04-02, true"
})
void deadlineBoundary(String utcTime, String deadlineText, boolean expected) {
Clock fixed = Clock.fixed(Instant.parse(utcTime), JAPAN);
DeadlinePolicy policy = new DeadlinePolicy(fixed);
boolean actual = policy.canSubmit(LocalDate.parse(deadlineText));
assertEquals(expected, actual);
}
}mvn testでは3行とも通る想定です。もし本体をLocalDate.now()に戻すと、実行日の影響を受け、環境によって結果が変わります。Clock.fixedは日時を変化させない時計なので、深夜まで待たずに二つの瞬間を別々に作れます。この記事のコードは原稿作成環境では通し実行していないため、導入先で実行結果を確認してください。
「締切日」か「締切時刻」かで、型も変える
この例は日付単位で「4月1日いっぱい」を許可するルールです。「4月1日18:00 JSTを過ぎたら不可」なら、LocalDate同士の比較では時刻を表せません。InstantやZonedDateTimeで締切の瞬間を定め、Instant.now(clock)と比較します。
また、システムの既定タイムゾーンを暗黙に使うと、開発PCと本番サーバーで日付が違うことがあります。テストに固定するのはInstantだけでなく、業務が採用するZoneIdもセットです。表示だけJSTにして判定をUTCのままにしていないかも確認します。
日時以外にも外部入力を固定したい場合は、ServiceのJUnit・Mockitoテストで依存関係を分ける考え方へつなげられます。
さらに学ぶなら
日時に限らず「どの依存をテストから制御し、どこを実物で確かめるか」を考えたい人には『単体テストの考え方/使い方』を紹介します。出版社の紹介で対象範囲を確認できます。例に使う言語はC#であり、Clock.fixedやJUnitの操作を教える本ではありません。実務担当の本棚(Amazon・アフィリエイトを含みます)にも置いています。この記事の期限判定は本の掲載例や本人の読後体験として示したものではありません。
