
Data utworzenia, data modyfikacji, data wygenerowania. Nasze systemy często potrzebują wstawić takie informacje do obiektów, którymi zarządzają. Ale jak przetestować, że poprawna wartość została wpisana do takich pól? O tym, w poniższym wpisie.
Spójrzmy na fragment kodu.
class InvoiceService {
Invoice generate(Order order) {
Invoice invoice = // ...;
invoice.generatedAt(LocalDateTime.now());
return invoice;
}
}
Klasa InvoiceService
ma za zadanie wygenerować fakturę Invoice
na podstawie zamówienia Order
.
W trakcie tworzenia do klasy Invoice
wstawiana jest data wygenerowania faktury invoice.generatedAt(...)
.
Wszystko wygląda poprawnie. Czas więc by napisać test sprawdzający wpisanie prawidłowej daty.
class InvoiceServiceTest {
InvoiceService invoiceService = new InvoiceService()
def "contains date of generation"() {
given:
Order order = givenOrder();
when:
Invoice invoice = invoiceService.generate(order);
then:
invoice.generatedAt == LocalDateTime.now()
}
}
W sekcji then
porównujemy dwie wartości.
invoice.generatedAt == LocalDateTime.now()
Datę wstawioną do faktury Invoice
z aktualną datą LocalDateTime.now()
. Na pierwszy rzut wszystko powinno być ok.
Wystarczy jednak, że uruchomisz ten kod na swoim komputerze i zobaczysz, że daty się nie zgadzają. Różnica będzie rzędu milisekund, czy nanosekund.
Nie sposób by data była dokładnie ta sama w momencie generowania faktury invoiceService.generate(order)
jak i w chwili weryfikowania testu.
Co więc robi w takiej sytuacji doświadczony programista?
Wprowadza abstrakcję!
Wprowadź Clock i nie martw się czasem :) #
Wystarczy, że do naszego kodu dodamy nowy interfejs Clock
z jedną metodą time()
.
interface Clock {
LocalDateTime time();
}
Spójrzmy teraz na poniższy fragment kodu zmienionej klasy InvoiceService
.
class InvoiceService {
Clock clock;
Invoice generate(Order order) {
Invoice invoice = // ...;
invoice.generatedAt(clock.time());
return invoice;
}
}
W klasie pojawiło się pole typu Clock
, które jest używane w momencie wpisywania daty przez metodę clock.time()
.
invoice.generatedAt(clock.time())
Po co to wszystko? A no to po, żeby w trakcie testów mieć pewność, że możemy bezpiecznie sprawdzić wstawioną wartość. Spójrzmy poniżej na zmodyfikowany test.
class InvoiceServiceTest {
Clock clock = new FakeClock()
InvoiceService invoiceService = new InvoiceService(clock);
def "contains date of generation"() {
given:
Order order = givenOrder();
when:
Invoice invoice = invoiceService.generate(order);
then:
invoice.generatedAt == clock.time()
}
}
Do testu została wprowadzona “fałszywa” implementacja interfejsu Clock
o nazwie FakeClock
. Jest przekazywana do InvoiceService
w trakcie konstrukcji, a później używana w sekcji then
przy weryfikacji testu.
invoice.generatedAt == clock.time()
I jaki tu zysk? A no taki, że “fałszywa” implementacja zawsze zwraca tę samą wartość! Spójrzmy na jej przykładowy sposób zakodowania.
class FakeClock implements Clock {
private final LocalDateTime time;
FakeClock() {
this.time = LocalDateTime.now();
}
LocalDateTime time() {
return time;
}
}
Zauważ, że czas używany przez FakeClock
jest ustawiany tylko raz w momencie konstrukcji.
class FakeClock() {
this.time = LocalDateTime.now();
}
Później, każdorazowe zawołanie metody time()
zwraca zawsze jedną i tę samą instancję zmiennej time
, a więc zawsze jedną i tę samą wartość.
Dzięki temu zarówno w momencie generowania faktury Invoice
w trakcie testu jak i w chwili weryfikacji testu pobranie czasu clock.time()
zwróci te same wartości i test będzie działać poprawnie.
W ten sposób zweryfikujemy, że data została poprawnie wstawiona do klasy Invoice
. a test nie będzie się “wywalać” z powodu przesunięcia czasu o kilka milisekund.
A co z klasą Clock dla kodu produkcyjnego?
Tutaj kwestia jest prosta.
Wystarczy przygotować osobną implementację i dostarczyć ją do systemu (np. poprzez zadeklarowanie beana
jeśli korzystamy ze Springa).
class SystemClock implements Clock {
LocalDateTime now() {
return LocalDateTime.now();
}
}
Podsumowanie
Teraz już wiesz jak mądrze podejść do testowania atrybutów opartych o czas. Pamiętaj, że za każdym razem kiedy w systemie wołasz ręcznie new Date()
czy LocalDateTime.now()
warto zastanowić się, czy w tym miejscu nie powinieneś korzystać z abstrakcji Clock
.