sobota, 5 stycznia 2013

JUnit rule i mockowanie new Date() cz.2

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.

3 komentarze:

  1. 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ć.
    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.

    OdpowiedzUsuń
  2. 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ń
  3. 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ń