public List<Notification> findNotificationsToSend() {
List<Notification> notifications = repository.findDailyNotifications();
int today = Calendar.getInstance().get(DAY_OF_WEEK);
if (today == SUNDAY)
notifications.addAll(repository.findAllWeeklyNotifications());
return notifications;
}
and then you can be sure they don't use TDD. Because there is absolutely no way you can create such a bad code when you have tests. But what can we do about the calendar? Same as always. Have you noticed that when there is a call to a database or other external system, people immediately say: 'extract and mock'? But when there is a call to jvm's infrastructure they have no idea what to do. And the answer is simple: 'extract and mock'. Does it mean people just repeat previously seen schemes without thinking?We can start refactoring with:
public class TimeProvider {
public int dayOfWeek() {
return Calendar.getInstance().get(DAY_OF_WEEK);
}
}
That's a good start. Now it's easy to test the findNotificationsToSend but TimeProvider can still contain some complicated time calculations which are not testable. And it will grow with calendar-dependent methods. How to clean it up?- Switch to joda time. It has much better api that protects TimeProvider from uncontrolled growing.
- TimeProvider should contain only often used, calendar-like, parameterless methods dependent on current time. And nothing else. 'isSunday' and 'beginningOfQuarter' are fine but 'shouldIncludeWeeklyNotification' is not.
- Completely separate jvm's infrastructure access from time calculations. In this case I usually choose inheritance over composition because TimeProvider won't ever grow in any additional dependencies. After all, even business guys don't change the definition of Sunday.
abstract class TimeProvider {
protected abstract long currentMillis();
public final DateTime now() {
return new DateTime(currentMillis());
}
public final boolean isSunday() {...} //if often used
// other common business methods. all final.
}
public final class RealTimeProvider extends TimeProvider {
protected long currentMillis() {
return System.currentTimeMillis();
}
}
public class TestTimeProvider extends TimeProvider {
private long currentMillis;
public TestTimeProvider() {
this("2013-05-17"); // preset time; handy for tests
}
public TestTimeProvider(String currentTime) {
setTime(currentTime);
}
public void setTime(String currentTime) {
currentMillis = parseTime(currentTime);
}
protected long currentMillis() {
return currentMillis;
}
private static long parseTime(String time) {...}
}
Of course, we use TestTimeProvider in unit and spring-context tests. Often it's more handy than mocks. If needed, add similar support for timezone. Now we can test
findNotificationsToSend, control time during integration tests and test isSunday method:@RunWith(ZohhakRunner.class)
public class TimeProviderTest {
TestTimeProvider timeProvider = new TestTimeProvider();
@TestWith({
"2013-04-14, true",
"2013-04-15, false"
})
public void should_detect_sunday(String date, boolean shouldBeSunday) {
timeProvider.setTime(date);
boolean isSunday = timeProvider.isSunday();
assertThat(isSunday).isEqualTo(shouldBeSunday);
}
}
One place where time provider alone is not enough is integration testing, when we start the whole server to imitate production environment and connect to it over http. But that's a story for another post.
0 komentarze :
Post a Comment