Reputation:
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
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.
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.ZoneId.getAvailableZoneIds()
can return more values when more ZoneId
s are added. While the available zone IDs are provided by the ZoneRulesProvider
abstract class, there is no easy way to disable the standard ZoneId
s 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 ZoneId
s.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 ZoneId
s 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
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
Reputation: 5563
Since your getTimeZonesByZoneOffset
is private, there is no easy way to test this. What you could do though is the following:
getTimeZonesByZoneOffest
in order to make sure that yo would be returning an expected result.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