user16409822
user16409822

Reputation:

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?

Upvotes: 3

Views: 4883

Answers (3)

Patrick Hooijer
Patrick Hooijer

Reputation: 741

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

}

Upvotes: 5

tgdavies
tgdavies

Reputation: 11421

Don't use static methods. Creating components is cheap.

Create an interface and implementation for your getTimeZonesByZoneOffset method, and create another interface and implementation to provide the Instant it uses. Then your tests can use a known 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
    }
}

Similarly, put getByOffset in a separate class, which is passed a TZSource and a ProductRepository in its constructor. Both of these will be mocked in its unit test.

Upvotes: 0

akourt
akourt

Reputation: 5563

Since your getTimeZonesByZoneOffset is private, there is no easy way to test this. What you could do though is the following:

  1. Statically mock the innards of getTimeZonesByZoneOffest in order to make sure that yo would be returning an expected result.
  2. Mock the repository layer in order to return a list of mocked results.
  3. Assert the return value of the getByOffset method, thus validating that getProductList is performing its job correctly.

For static mocking you could potentially lock into PowerMock or Mockito-inline, depending on the version of Junit you are using.

Upvotes: 0

Related Questions