Interviewer: What's wrong with this code?
import java.time.LocalDateTime;
public class Service {
public boolean isMorning() {
LocalDateTime currentDateTime = LocalDateTime.now();
return currentDateTime.getHour() < 12;
}
}
This was my interview experience with the first Test Driven Development team with which I worked. It seemed silly at the time but after 10+ years of developing various software I still use the concepts behind the question.
If you're still scratching your head and wondering whether the class doesn't exist, or if the return type is wrong or maybe just overlooking some basic syntax - I'll save you the trouble and tell you the code is 100% functional. The main problem is that the code is completely untestable. Unless you want to run a server and wait up to 12 hours for the return value to change, the code needs some refactoring.
Tock
To show one way of making time-based business logic testable I wrote a very small SpringBoot app that sends back a greeting based on the system's local time. The fully working codebase can be found at https://github.com/barreeyentos/tock.
The main idea behind the code is that many applications rely on time somewhere in its logic and this code is usually the most error prone. I won't go into testing Time Zones as that needs a full dedicated blog but for now I'll stick to just one time zone (the server's timezone).
Let's start with this simple service:
@Service
public class GreetingService {
public static final String INVALID_HOUR = "You should really fix the clock.";
public static final String GOOD_NIGHT = "Good night";
public static final String GOOD_AFTERNOON = "Good afternoon";
public static final String GOOD_MORNING = "Good morning!";
public static final String NIGHT_OWL = "Good morning?";
private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME;
public GreetingService() {
}
public Greeting createAppropriateGreeting() {
String greeting = null;
LocalDateTime localDateTime = LocalDateTime.now();
String formattedTime = localDateTime.format(formatter);
int hour = localDateTime.getHour();
if (0 <= hour && hour <= 5) {
greeting = NIGHT_OWL;
} else if (6 <= hour && hour <= 11) {
greeting = GOOD_MORNING;
} else if (12 <= hour && hour <= 17) {
greeting = GOOD_AFTERNOON;
} else if (18 <= hour && hour <= 23) {
greeting = GOOD_NIGHT;
} else {
greeting = INVALID_HOUR;
}
return new Greeting(greeting, formattedTime);
}
}
We've made a great service (or just a greet service) ... seems bug-free and even handles invalid hours. Now let's go ahead and write a test.
private GreetingService service;
@Before
public void before() {
service = new GreetingService();
}
@Test
public void testMorning() {
Greeting greeting = service.createAppropriateGreeting();
assertThat(greeting.getGreeting()).isEqualTo(GreetingService.GOOD_MORNING);
}
Hmmm... fail.
This test only passes at certain times of the day...so unless you only test in the morning these tests are pointless.
So far the GreetingService
extremely straight-forward but still untestable so let's start refactoring. In order to make it better we need to be able to control how the localDateTime
variable is set. You may first try passing the time into the method, but you will soon find that at some point in the application you still end up with untestable code. So how about create our own time service.
The TimeService Interface
For this app, the interface will be short and simple:
@FunctionalInterface
public interface TimeService {
LocalDateTime now();
}
And the just as simple implementation:
@Service("timeService")
public class TimeServiceImpl implements TimeService {
public LocalDateTime now() {
return LocalDateTime.now();
}
}
Now you can imagine the implementation of the GreetingService to be injected with a TimeService and now the time can be set as below:
@Autowired
public GreetingService(TimeService timeService){
this.timeService = timeService;
}
...
LocalDateTime localDateTime = timeService.now();
You might be thinking that this is a waste of time and is just an over-engineered example but let's take another stab at our tests.
@Mock
TimeService timeService
private GreetingService service;
@Before
public void before() {
service = new GreetingService(timeService);
when(timeService.now()).thenReturn(LocalDateTime.of(2017, Month.SEPTEMBER, 17, 9, 30));
}
@Test
public void testMorning() {
Greeting greeting = service.createAppropriateGreeting();
assertThat(greeting.getGreeting()).isEqualTo(GreetingService.GOOD_MORNING);
}
Success!
Now you can test every line of code at any time of the day. These simple changes make your app 100% testable at both the unit and integration level.
You can probably stop reading now, but if you're still reading the next step is going to not use Mockito
and just make my own Time Mock....Tock.
Since I already have an interface its easy enough to make my own testing implementation:
public class Tock implements TimeService {
private LocalDateTime mockTime;
public Tock() {
}
public void setTime(LocalDateTime now) {
this.mockTime = now;
}
@Override
public LocalDateTime now() {
if (Objects.isNull(mockTime)) {
return LocalDateTime.now();
}
return mockTime;
}
}
Now the tests look something like:
TimeService timeService
private GreetingService service;
@Before
public void before() {
timeService = new Tock();
service = new GreetingService(timeService);
timeService.setTime(LocalDateTime.of(2017, Month.SEPTEMBER, 17, 9, 30));
}
@Test
public void testMorning() {
Greeting greeting = service.createAppropriateGreeting();
assertThat(greeting.getGreeting()).isEqualTo(GreetingService.GOOD_MORNING);
}
But Why?!?!
Yes, you're right...one has a lot less code and why write your own mock? Well this is obviously a contrived application but if you're testing a loop that checks the time in every iteration, you might find it useful for your clock to advance 1 minute for every call of now()
, or maybe you want to simulate time drift and you need to always be 30 seconds behind real time. Your own Tock
can help you test more intricate scenarios where Mockito
or other popular libraries can't help you.
Hopefully this example can help you test your apps just a little bit better than before and it can remind you to think about tests before you start to write code.
The full code for Tock
that has integration tests for the Controller and unit tests for the service is here for your enjoyment: https://github.com/barreeyentos/tock.