模拟Java日期/时间进行测试


在源代码中引用当前日期/时间会使测试非常困难。您必须从断言中排除此类引用,这并不是一件容易的事。

时间模拟是避免测试期间当前时间不便的可行解决方案,可以通过使用新的Java 8日期/时间API(JSR 310)来实现。

为了获得当前时刻,java.time包中的所有相关类 都具有static方法now()

LocalDate.now();
LocalTime.now();
LocalDateTime.now();
OffsetDateTime.now();
ZonedDateTime.now();
Instant.now();

由于性能信誉不佳,我们将不考虑使用模拟静态方法的方法。

如果您查看任何Java API类的方法,都会发现被覆盖的方法与系统时钟配合使用: now()

/**
     * Obtains the current time from the system clock in the default time-zone.
     * <p>
     * This will query the {@link Clock#systemDefaultZone() system clock} in the default
     * time-zone to obtain the current time.
     * <p>
     * Using this method will prevent the ability to use an alternate clock for testing
     * because the clock is hard-coded.
     *
     * @return the current time using the system clock and default time-zone, not null
     */
    public static LocalTime now() {
        return now(Clock.systemDefaultZone());
    }

我们可以做的是使用当前时间的所有必需方法以及一个附加的模拟时间方法创建包装器类。为了演示,我们将其称为Time:

package com.example.utils;
import java.time.*;
import java.util.TimeZone;
public class Time {
    private static Clock CLOCK = Clock.systemDefaultZone();
    private static final TimeZone REAL_TIME_ZONE = TimeZone.getDefault();

    public static LocalDate currentDate() {
        return LocalDate.now(getClock());
    }
    public static LocalTime currentTime() {
        return LocalTime.now(getClock());
    }

    public static LocalDateTime currentDateTime() {
        return LocalDateTime.now(getClock());
    }
    public static OffsetDateTime currentOffsetDateTime() {
        return OffsetDateTime.now(getClock());
    }

    public static ZonedDateTime currentZonedDateTime() {
        return ZonedDateTime.now(getClock());
    }

    public static Instant currentInstant() {
        return Instant.now(getClock());
    }
    public static long currentTimeMillis() {
        return currentInstant().toEpochMilli();
    }

    public static void useMockTime(LocalDateTime dateTime, ZoneId zoneId) {
        Instant instant = dateTime.atZone(zoneId).toInstant();
        CLOCK = Clock.fixed(instant, zoneId);
        TimeZone.setDefault(TimeZone.getTimeZone(zoneId));
    }

    public static void useSystemDefaultZoneClock() {
        TimeZone.setDefault(REAL_TIME_ZONE);
        CLOCK = Clock.systemDefaultZone();
    }

    private static Clock getClock() {
        return CLOCK;
    }
}

让我们更仔细地研究包装器类:

  1. 首先,我们记住系统时钟和系统时区。
  2. 使用方法,userMockTime() 我们可以使用固定时钟覆盖时间并提供时区。
  3. 使用方法,useSystemDefaultZoneClock()我们可以将时钟和时区重置为系统默认值。
  4. 所有其他方法都使用提供的时钟实例。 到目前为止,还不错,但是这种方法还存在两个问题:

useMockTime()方法对于生产代码来说太危险了。我们想要以某种方式消除源代码中该方法的使用。 我们需要强制使用 Time类,而不是直接使用*Date/Time.now()方法。 最佳解决方案之一是 ArchUnit库。

它为您提供了使用单元测试控制体系结构规则的机会。让我们看看如何使用JUnit 5引擎定义规则(也可以选择为JUnit 4引擎定义相同的规则)。

我们将需要添加必要的依赖关系(Maven示例):

<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit5</artifactId>
    <version>0.14.1</version>
    <scope>test</scope>
</dependency>

并在单元测试文件夹中添加规则描述:

package com.example;
import com.example.utils.OldTimeAdaptor;
import com.example.utils.Time;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import java.time.*;
import java.util.Date;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
@AnalyzeClasses(
        packages = "com.example",
        importOptions = ImportOption.DoNotIncludeTests.class
)
public class ArchitectureRulesTest {    
    @ArchTest
    public static final ArchRule RESTRICT_TIME_MOCKING = noClasses()
            .should().callMethod(Time.class, "useMockTime", LocalDateTime.class, ZoneId.class)
            .because("Method Time.useMockTime designed only for test purpose and can be used only in the tests");

    @ArchTest
    public static final ArchRule RESTRICT_USAGE_OF_LOCAL_DATE_TIME_NOW = noClasses()
            .should().callMethod(LocalDateTime.class, "now")
            .because("Use Time.currentDateTime methods instead of as it gives opportunity of mocking time in tests");
    @ArchTest
    public static final ArchRule RESTRICT_USAGE_OF_LOCAL_DATE_NOW = noClasses()
            .should().callMethod(LocalDate.class, "now")
            .because("Use Time.currentDate methods instead of as it gives opportunity of mocking time in tests");
    @ArchTest
    public static final ArchRule RESTRICT_USAGE_OF_LOCAL_TIME_NOW = noClasses()
            .should().callMethod(LocalTime.class, "now")
            .because("Use Time.currentTime methods instead of as it gives opportunity of mocking time in tests");

    @ArchTest
    public static final ArchRule RESTRICT_USAGE_OF_OFFSET_DATE_TIME_NOW = noClasses()
            .should().callMethod(OffsetDateTime.class, "now")
            .because("Use Time.currentOffsetDateTime methods instead of as it gives opportunity of mocking time in tests");
    @ArchTest
    public static final ArchRule RESTRICT_USAGE_OF_ZONED_DATE_TIME_NOW = noClasses()
            .should().callMethod(ZonedDateTime.class, "now")
            .because("Use Time.currentZonedDateTime methods instead of as it gives opportunity of mocking time in tests");

    @ArchTest
    public static final ArchRule RESTRICT_USAGE_OF_INSTANT_NOW = noClasses()
            .should().callMethod(Instant.class, "now")
            .because("Use Time.currentInstant methods instead of as it gives opportunity of mocking time in tests");
}

另外,我们还可以限制使用旧的Date / Time API:

@ArchTest
    public static final ArchRule RESTRICT_USAGE_OF_OLD_DATE_API = classes()
            .that().areNotAssignableTo(OldTimeAdaptor.class)
            .should().onlyAccessClassesThat().areNotAssignableTo(Date.class)
            .because("java.util.Date is class from the old Date API. " +
                    "Please use new Date API from the package java.time.* " +
                    "In case when you need current date/time use wrapper class com.example.utils.Time");

如果您使用的旧库/框架不支持新的Date / Time API,我们可以为此创建一个适配器类。(请注意,适配器类的排除已在上一条规则中定义):

package com.example.utils;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
public class OldTimeAdaptor {
    public static LocalDateTime toLocalDateTime(Date date) {
        return (date == null) ? null : LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
    }
    public static Date toDate(LocalDateTime localDatetime) {
        return localDatetime == null ? null : Date.from(localDatetime.atZone(ZoneId.systemDefault()).toInstant());
    }
}

作为该演示的最后一个示例,我们将创建一个JUnit 5扩展,该扩展自动模拟时间以获取预定义的值,例如:01–04–2020 12:45 Europe / Kiev

package com.example.extension;
import com.example.utils.Time;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneId;

public class MockTimeExtension implements BeforeEachCallback, AfterEachCallback {
    @Override
    public void beforeEach(ExtensionContext extensionContext) throws Exception {
        LocalDateTime currentDateTime = LocalDateTime.of(2020, Month.APRIL, 1, 12, 45);
        ZoneId zoneId = ZoneId.of("Europe/Kiev");

        Time.useMockTime(currentDateTime, zoneId);
    }

    @Override
    public void afterEach(ExtensionContext extensionContext) throws Exception {
        Time.useSystemDefaultZoneClock();
    }
}

现在让我们假设我们有一个简单的方法:

package com.example.logic;
import com.example.utils.Time;
public class BusinessLogicClass {
    public String getLastUpdatedTime() {
        return "Last Updated At " + Time.currentDateTime();
    }
}

然后我们可以创建一个单元测试:

package com.example.logic;
import com.example.extension.MockTimeExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockTimeExtension.class)
class BusinessLogicClassTest {
    private BusinessLogicClass businessLogicClass = new BusinessLogicClass();

    @Test
    void getLastUpdatedTime_returnMockTime() {
        //WHEN
        String lastUpdatedTime = businessLogicClass.getLastUpdatedTime();

        //THEN
        assertThat(lastUpdatedTime).isEqualTo("Last Updated At 2020-04-01T12:45");
    }
}


原文链接:http://codingdict.com