Jeśli korzystasz w swoim kodzie z konstrukcji Thread.sleep
, ale zawsze miałeś problem z tym by ją prawidłowo przetestować, ten wpis jest dla Ciebie :).
Nie raz zdarza się, iż potrzebujemy zwolnić wykonywanie naszego kodu. Nie chcemy by rzeczy działy się od razu, ale by między kolejnymi wykonaniami nastąpił pewien odstęp.
Weźmy za przykład poniższy prosty interfejs.
public
interface
CurrencyRate
{
BigDecimal
rate(String
from,
String
to);
}
Załóżmy, iż chcemy mieć następującą logikę:
- metoda
rate
zwracanull
, gdy nie uda się pobrać wartości, - chcemy mieć opcjonalnie mechanizm, który ponawia nam zapytanie o stawkę wymiany w powyższej sytuacji.
W tym celu tworzymy klasę dekorującą RetriableCurrencyRate
, która ponawia próbę maksymalnie N razy i
czeka między kolejnymi wywołaniami metody po 1 sekundę przerwy.
public
class
RetriableCurrencyRate
implements
CurrencyRate
{
private
final
CurrencyRate
origin;
private
int
retriesLimit;
@Override
public
BigDecimal
rate(String
from,
String
to)
throws
Exception
{
BigDecimal
rate
=
origin.rate(from,
to);
int
retries
=
0;
while
(rate
==
null
&&
retries
<
retriesLimit)
{
Threads.sleep(1_000);
rate
=
origin.rate(from,
to);
++retries;
}
return
rate;
}
}
Jak mógłby wyglądać prosty test tej klasy? Spójrzmy na RetriableCurrencyRateTest
.
class
RetriableCurrencyRateTest
{
int
retriesLimit
=
3;
RetriableCurrencyRate
sut
=
new
RetriableCurrencyRate(
new
FailingCurrencyRate(),
retriesLimit
);
@Test
public
void
shouldRetry3Times()
{
// when
BigDecimal
rate
=
sut.rate("usd",
"pln");
// then
assertNull(rate);
// and 3 retries - how to test that? :(
}
}
Test jest bardzo prosty, ale ma dwie wady:
- trwa ponad 3.000 ms - bo 3 razy powtarzamy wywołanie pętli, w której wątek śpi przez 1.000 ms,
- w sekcji
then
nie jesteśmy w stanie zweryfikować ile faktycznie razy pętla była powtórzona.
Czy możemy zrobić coś lepiej? Oczywiście! #
Co robi dobry programista, gdy nie wie co zrobić? Wprowadza abstrakcję!
Zróbmy podobnie :)
Zamiast korzystać bezpośrednio z klasy Thread
i jej metody sleep
, schowajmy to sobie za dedykowany interfejs Sleeper
.
public
interface
Sleeper
{
void
sleep(long
millis);
}
I użyjmy go w naszej klasie w miejsce Thread.sleep
. :)
@AllArgsConstructor
public
class
RetriableCurrencyRate
implements
CurrencyRate
{
private
final
CurrencyRate
origin;
private
final
Sleeper
sleeper;
private
int
retriesLimit;
@Override
@SneakyThrows
public
BigDecimal
rate(String
from,
String
to)
{
BigDecimal
rate
=
origin.rate(from,
to);
int
retries
=
0;
while
(rate
==
null
&&
retries
<
retriesLimit)
{
sleeper.sleep(1_000);
rate
=
origin.rate(from,
to);
++retries;
}
return
rate;
}
}
Jak teraz będzie wyglądał test?
class
RetriableCurrencyRateTest
{
int
retriesLimit
=
3;
CountingSleeper
sleeper
=
new
CountingSleeper(new
NoopSleeper());
RetriableCurrencyRate
sut
=
new
RetriableCurrencyRate(
new
FailingCurrencyRate(),
sleeper,
retriesLimit
);
@Test
public
void
shouldRetry3Times()
{
// when
BigDecimal
rate
=
sut.rate("usd",
"pln");
// then
assertNull(rate);
assertEquals(retriesLimit,
sleeper.count());
}
}
Ile to trwa? 28ms!
O stary, ale jak to zrobiłeś?! #
Do testu wprowadziłem dedykowane implementacje interfejsu Sleeper
: CountingSleeper
oraz NoopSleeper
.
CountingSleeper
- to dekorator, który pozwala mi zliczyć ile razy metoda sleep
została zawołana,
a NoopSleeper
- to implementacja-wydmuszka - która nic nie robi.
W sam raz do testów! :)
Dzięki temu, udało się uzyskać, tak krótki czas wykonania kodu.
@RequiredArgsConstructor
public
class
CountingSleeper
implements
Sleeper
{
private
final
Sleeper
origin;
private
long
count
=
0;
@Override
public
void
sleep(long
millis)
{
count++;
origin.sleep(millis);
}
public
void
reset()
{
count
=
0;
}
public
long
count()
{
return
count;
}
}
public
class
NoopSleeper
implements
Sleeper
{
@Override
public
void
sleep(long
millis)
{
System.out.println("Noop sleep :)");
}
}
A co w “normalnym” kodzie? #
A co zrobić w normalnym = produkcyjnym kodzie.
Po prostu dostarczyć “normalną” implementację. W której skorzystamy ze znanej nam dobrze metody Thread.sleep
.
public
class
ThreadSleeper
implements
Sleeper
{
@Override
public
void
sleep(long
millis)
{
Thread.sleep(millis);
}
}
Podsumowanie #
W tym wpisie dowiedziałeś się jak sprytnie przetestować Thread.sleep
, jak można do tego wykorzystać interfejsy w Javie oraz jak w tym wszystkim zastosować wzorzec dekorator.
Daj znać, co myślisz o takim rozwiązaniu i czy stosujesz / planujesz stosować w swoim kodzie :)