如何通过单元测试测试时区方法?

How to test timeZone method via Unit Test?

我使用以下采用时区偏移值的方法,例如GMT-3 和 returns 给定偏移量的时区 ID 列表。

private static List<String> getTimeZonesByZoneOffset(final int offset) {
    return ZoneId.getAvailableZoneIds()
            .stream()
            .filter(zoneId -> ZoneId.of(zoneId)
                    .getRules()
                    .getOffset(Instant.now())
                    .equals(ZoneOffset.ofHours(offset)))
            .sorted()
            .collect(Collectors.toList());
}

然后我从我的数据库中检索与我的区域列表具有相同 zoneId 的相应记录。我在我的服务中使用了类似这样的方法:

public List<Product> getByOffset(int offset) {
    final List<String> zones = getTimeZonesByZoneOffset(offset);
    final List<Product> products = productRepository.findAllByZone(zones);
    return getProductList(products);
}

我想在单元测试中测试这两种方法。但是我不确定应该如何设置测试机制。是否有任何单元测试示例?

由于您的 getTimeZonesByZoneOffset 是私人的,因此没有简单的方法来测试它。不过您可以执行以下操作:

  1. 静态模拟 getTimeZonesByZoneOffest 的内部结构,以确保你会 return 得到预期的结果。
  2. 模拟存储库层以便return模拟结果列表。
  3. 断言 getByOffset 方法的 return 值,从而验证 getProductList 是否正确执行其工作。

对于静态模拟,您可能会锁定 PowerMockMockito-inline,具体取决于您使用的 Junit 版本。

不要使用静态方法。创建组件很便宜。

为您的 getTimeZonesByZoneOffset 方法创建一个接口和实现,并创建另一个接口和实现以提供它使用的 Instant。然后你的测试可以使用一个已知的瞬间。

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.List;
import java.util.stream.Collectors;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class UnitTest {
    interface TZSource {
        List<String> getTimeZonesByZoneOffset(final int offset);
    }

    interface NowSource {
        Instant getNow();
    }

    static class DefaultTZSource implements TZSource {
        private final UnitTest.NowSource nowSource;
        public DefaultTZSource(UnitTest.NowSource nowSource) {
            this.nowSource = nowSource;
        }
        @Override
        public List<String> getTimeZonesByZoneOffset(int offset) {
            return ZoneId.getAvailableZoneIds()
                    .stream()
                    .filter(zoneId -> ZoneId.of(zoneId)
                            .getRules()
                            .getOffset(nowSource.getNow())
                            .equals(ZoneOffset.ofHours(offset)))
                    .sorted()
                    .collect(Collectors.toList());
        }
    }

    @Test
    public void testTZSource() {
        NowSource nowSource = mock(NowSource.class);
        when(nowSource.getNow()).thenReturn(LocalDateTime.of(2021, 8, 26, 20, 30, 0).toInstant(ZoneOffset.ofHours(0)));
        TZSource tzSource = new DefaultTZSource(nowSource);
        Assertions.assertEquals(List.of(), tzSource.getTimeZonesByZoneOffset(5)); // this fails, you'll need to add the timezones to the list
    }
}

类似地,将 getByOffset 放在单独的 class 中,在其构造函数中传递 TZSourceProductRepository。这两个都将在其单元测试中被模拟。

你的方法中有两个静态方法调用,在不同的时间点 return 不同的值,我建议在这两种情况下使用依赖注入来 return 模拟结果。

  1. ZoneRules.getOffset(Instant) returns 不同的值取决于测试是 运行 在夏令时或任何其他时区转换期间。您可以通过测试固定时间来解决此问题。添加对 Clock 的依赖,并在标准代码中使用 Clock.systemUTC() 注入它,并在单元测试中使用 Clock.fixed(Instant, ZoneId) 注入它。
    当添加更多 ZoneId 时,
  1. ZoneId.getAvailableZoneIds() 可以 return 更多值。虽然可用区域 ID 由 ZoneRulesProvider 摘要 class 提供,但没有简单的方法来禁用标准 ZoneIds 并注入您自己的。如果你想通过依赖注入来解决这个问题,那么你必须自己制作 return 可用的 ZoneId 服务。

虽然 ZoneId 中的 ZoneRules 会随时间变化(例如,如果政府机构停止使用夏令时),但它们在过去的时间里是固定的,所以它不是只要 Instant.now() 被模拟到过去的某个时间,问题 returning 现有的 ZoneIds。

示例代码:

import org.assertj.core.api.Assertions;
import org.junit.Test;

import java.time.Clock;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class ProductProvider {

    private final Clock clock;
    private final ZoneIdProvider zoneIdProvider;

    public ProductProvider(Clock clock, ZoneIdProvider zoneIdProvider) {
        this.clock = clock;
        this.zoneIdProvider = zoneIdProvider;
    }

    private List<String> getTimeZonesByZoneOffset(final int offset) {
        return zoneIdProvider.getAvailableZoneIds()
                .stream()
                .filter(zoneId -> ZoneId.of(zoneId)
                        .getRules()
                        .getOffset(clock.instant())
                        .equals(ZoneOffset.ofHours(offset)))
                .sorted()
                .collect(Collectors.toList());
    }

    public List<Product> getByOffset(int offset) {
        final List<String> zones = getTimeZonesByZoneOffset(offset);
        final List<Product> products = productRepository.findAllByZone(zones);
        return getProductList(products);
    }

    public interface ZoneIdProvider {
        Set<String> getAvailableZoneIds();
    }

    public static class ProductProviderTest {
        @Test
        public void testTimezone() {
            OffsetDateTime testTime = OffsetDateTime.of(2021, 8, 26, 11, 51, 4, 0, ZoneOffset.UTC);
            Clock clock = Clock.fixed(testTime.toInstant(), testTime.getOffset());
            ZoneIdProvider zoneIdProvider = Mockito.mock(ZoneIdProvider.class);
            Mockito.when(zoneIdProvider.getAvailableZoneIds()).thenReturn(Set.of(
                    ZoneId.of("America/Argentina/Buenos_Aires"), // UTC-3 year-round
                    ZoneId.of("America/Nuuk"), // UTC-3 as Standard Time only
                    ZoneId.of("America/Halifax"), // UTC-3 as Daylight Savings Time only
                    ZoneId.of("Europe/Paris"))); // Never UTC-3
            ProductProvider productProvider = new ProductProvider(clock, zoneIdProvider);

            Assertions.assertThat(productProvider.getByOffset(-3))
                    .isEmpty(); // Add your expected products here
        }
    }

}