Chciałbym rozwinąć trochę temat poruszony w
1 części dotyczący mockowania new Date(). Często w testach tworzonych jest po kilka obiektów, dla których oczekujemy, że będą miały np. różne daty utworzenia (pole prywatne w klasie). Nie mając nad tym władzy zdajemy się na JVM i raz te daty faktycznie będą się od siebie różniły, a raz nie. Bazując na
poprzednich klasach, test z użyciem Clock'a mógłby wyglądać tak:
@Rule
public Clock clock = Clock.standard();
@Test
public void should() {
// given
DateTime firstDate = new DateTime();
DateTime secondDate = firstDate.plusDays(1);
clock.setFixedTime(firstDate);
Order firstOrder = new Order();
clock.setFixedTime(secondDate);
Order secondOrder = new Order();
// when
// then
}
W rezultacie mamy 2 zamówienia z różnymi datami ich utworzenia. Jeśli nie zależny nam na pełnej kontroli tych dat, to można to zrobić trochę bardziej fancy, np.
clock.afterDay();
Order secondOrder = new Order();
Można również wykorzystać
MillisProvider'a i zaimplementować go tak aby każde kolejne wywołanie zwracało "datę" (tj. longa) przesuniętą względem poprzedniej np. o 1 milisekundę. Symulujemy tym samym upływ czasu i mamy pewności, że wywołanie
new DateTime() zawsze zwróci nam późniejsza datę.
public class Clock extends ExternalResource {
private static DateTime currentTime;
private MillisProvider millisProvider;
public static final MillisProvider ONE_MILLIS_INTERVAL = new MillisProvider() {
@Override
public long getMillis() {
currentTime = currentTime.plusMillis(1);
return currentTime.getMillis();
}
};
private Clock() {}
public Clock(MillisProvider millisProvider) {
this.millisProvider = millisProvider;
}
public static Clock standard() {
return new Clock();
}
public static Clock alwaysNewTime() {
return new Clock(ONE_MILLIS_INTERVAL);
}
@Override
protected void before() throws Throwable {
currentTime = new DateTime();
setProviderIfExists();
}
private void setProviderIfExists() {
if (millisProvider != null) {
DateTimeUtils.setCurrentMillisProvider(millisProvider);
}
}
@Override
protected void after() {
DateTimeUtils.setCurrentMillisSystem();
}
/**
* set date and stop the clock
*/
public void setFixedTime(Date date) {
currentTime = new DateTime(date);
DateTimeUtils.setCurrentMillisFixed(currentTime.getMillis());
}
/**
* set date and stop the clock
*/
public void setFixedTime(DateTime dateTime) {
setFixedTime(dateTime.toDate());
}
public DateTime getCurrentDateTime() {
return currentTime;
}
public Date getCurrentDate() {
return currentTime.toDate();
}
}
I sam test:
@Rule
public Clock clock = Clock.alwaysNewTime();
@Test
public void should() {
// given
Order firstOrder = new Order();
Order secondOrder = new Order();
// when
// then
assertThat(firstOrder.getCreateDate().before(secondOrder.getCreateDate()).isTrue()
}
Kiedy to się może przydać? Na pewno jeśli gdzieś w teście zamówienia lądują w HashMapie, której kluczem jest data utworzenia zamówienia.
Ogółem tworzenie daty gdzieś tam w bebeachach logiki biznesowej jest conajmniej kłopotliwe i nieładne. Widać to po komplikacjach w testowaniu i wyszukanych (aczkolwiek prostych) trikach, jak to obejść. Sam kiedyś długo się męczyłem, co z tym fantem zrobić.
OdpowiedzUsuńRozwiązanie do jakiego doszedłem, to uznanie atualnej daty / czasu ZAWSZE jako daną wejściową! Najczęściej przekazywanej gdzieś tam jako argument, lub w opakowan w obiekt z danymi wejściowymi. Ułatwia to znacząco testowanie.
I tak, i nie, Twoja propozycja jest OK, jeśli robimy system od podstaw i mamy takie założenia, ale kto ma taki komfort? Z reguły trzeba sobie radzić z tym co jest:) Wtedy każdy trick jest jest przydatny.
OdpowiedzUsuńJa mam taki komfort :) Co prawda muszę korzystać z paru archaizmów (tabele w bazie danych bez kluczy obcych). Zawsze można zrobić refactoring, ale pewnie użyć new Date() w kodzie jest tak dużo, że nikomu się nie chce za to wziąć.
OdpowiedzUsuń