如何使用java.time.Clock模拟时间流逝完成时间依赖类的单元测试
时间依赖类的测试方案
方案1:测试专用可变FakeClock(最简便)
JDK文档中对Clock实现必须不可变、final、线程安全的要求,是面向通用公开Clock实现的约束,仅在测试代码中内部使用的可变FakeClock不需要遵守该规范。只要你明确该FakeClock仅用于单测场景,且单测执行时不存在多线程并发修改的情况,就可以直接使用,完全不会有问题。
你可以按如下方式实现FakeClock:
// 仅在测试代码中存在,不需要对外公开 public class FakeClock extends Clock { private Instant now; private final ZoneId zone; public FakeClock() { this(Instant.now(), ZoneId.systemDefault()); } public FakeClock(Instant now, ZoneId zone) { this.now = now; this.zone = zone; } @Override public ZoneId getZone() { return zone; } @Override public Clock withZone(ZoneId zone) { return new FakeClock(now, zone); } @Override public Instant instant() { return now; } public void goForward(Duration duration) { this.now = this.now.plus(duration); } public void goForward1Hour() { goForward(Duration.ofHours(1)); } public void goForward30Minutes() { goForward(Duration.ofMinutes(30)); } }
测试代码直接用你原来的写法即可,不需要修改生产代码,也不会引入额外依赖。
方案2:抽象自定义时间提供者接口(最符合规范)
如果你严格希望遵守JDK的Clock规范,同时解决jMock无法mock类的问题,可以在生产代码中额外抽象一个时间提供者接口:
// 生产代码中新增 public interface TimeProvider { Instant now(); } // 生产环境默认实现 public class SystemTimeProvider implements TimeProvider { private final Clock clock; public SystemTimeProvider(Clock clock) { this.clock = clock; } @Override public Instant now() { return Instant.now(clock); } }
然后修改ClassToBeTested依赖该接口而非直接依赖Clock:
class ClassToBeTested { private final TimeProvider timeProvider; private Instant lastCalled; public ClassToBeTested(TimeProvider timeProvider) { this.timeProvider = timeProvider; } public String foo() { Instant now = timeProvider.now(); if (lastCalled == null || Duration.between(lastCalled, now).compareTo(Duration.ofHours(1)) >= 0) { lastCalled = now; return "A"; } else { lastCalled = now; return "B"; } } }
测试时既可以自己实现可变的FakeTimeProvider,也可以直接用jMock mockTimeProvider接口,完全符合所有规范要求,也不会破坏封装性。
方案3:反射修改私有字段(无侵入生产代码)
如果你既不想用可变Clock,也不想修改生产代码结构,可以在测试中用反射修改ClassToBeTested的私有字段,配合JDK自带的不可变Clock实现测试:
@Test public void fooReturnsAAfterAnHour() throws NoSuchFieldException, IllegalAccessException { Clock initialClock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); var myInstance = new ClassToBeTested(initialClock); assertThat(myInstance.foo(), is("A")); // 构造偏移1小时的不可变Clock Clock oneHourLaterClock = Clock.offset(initialClock, Duration.ofHours(1)); // 反射修改clock字段 Field clockField = ClassToBeTested.class.getDeclaredField("clock"); clockField.setAccessible(true); clockField.set(myInstance, oneHourLaterClock); assertThat(myInstance.foo(), is("A")); }
该方案完全不需要修改生产代码,也不会破坏生产代码的封装性,只是测试代码会多一点反射逻辑。
内容的提问来源于stack exchange,提问作者Sweeper
相关产品推荐
相关产品推荐

