Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test timeZone method via Unit Test?

I use the following approach that takes a time zone offset value e.g. GMT-3 and returns the list of time zoneId for the given offset.

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());
}

Then I retrieve corresponding records that have the same zoneId as my zone list from my database. I use an approach something like this in my service:

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

I want to test these 2 methods in a Unit Test. But I am not sure how should I set the test mechanism. Is there any Unit Test example for that?


1 Answers

There are two static method calls in your method than return different values at different points in time, and I suggest using dependency injection to return mocked results in both those cases.

  1. ZoneRules.getOffset(Instant) returns different values depending if the test is run during Daylight Savings Time or any other timezone transition. You can solve this by testing a fixed time. Add a dependency on Clock and inject it with Clock.systemUTC() in the standard code and Clock.fixed(Instant, ZoneId) for your Unit tests.
  1. ZoneId.getAvailableZoneIds() can return more values when more ZoneIds are added. While the available zone IDs are provided by the ZoneRulesProvider abstract class, there is no easy way to disable the standard ZoneIds and inject your own. If you want to solve this by dependency injection then you have to make your own service that returns the available ZoneIds.

While the ZoneRules in the ZoneId can change over time (for example, if a governmental body discontinues Daylight Savings Time), they are fixed for times in the past, so it is no problem returning existing ZoneIds as long as Instant.now() is mocked to a time in the past.

Example code:

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
        }
    }

}
like image 145
Patrick Hooijer Avatar answered May 15 '26 02:05

Patrick Hooijer