Dzisiaj zobaczymy, jak możemy wykorzystać fake’a zamiast mock’a w kontekście testów integracyjnych w Spring’u. Jak każde narzędzie, ma ono swoje plusy oraz minusy. Jakie one są, przekonamy się o tym za chwilę. Zapraszam do lektury!
Definicja problemu i powtórka
Aby nie marnować energii na definiowanie nowego przykładu, skorzystajmy z kodu przygotowanego w poprzednim artykule. Dla przypomnienia, mamy do dyspozycji ServiceA
oraz ServiceB
, gdzie ServiceA
symuluje komunikację z powolnym, zewnętrznym systemem.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Component
public class ServiceB {
private final ServiceA serviceA;
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
public String print() {
return "result = " + serviceA.returnSomething();
}
}
@Component
public class ServiceA {
@Override
public String returnSomething() {
try {
Thread.sleep(3_000);
} catch (InterruptedException _) {
// consume
}
return "A";
}
}
Uruchommy poniższy test, aby sprawdzić, czy wszystko działa.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
class ServiceBIntTest {
@Autowired
ServiceB serviceB;
@MockitoBean
ServiceA serviceA;
@Test
void test() {
when(serviceA.returnSomething()).thenReturn("A");
assertEquals("result = A", serviceB.print());
}
}
Oczywiście zadziałało. I to szybko, ponieważ mamy do czynienia z mockiem ServiceA
.
Kroki do zastosowania fake’a
Pierwszą rzeczą do wprowadzenia fake’a jest wyekstrahowanie abstrakcji z ServiceA
w postaci interfejsu o nazwie ReturningSomething
.
1
2
3
public interface ReturningSomething {
String returnSomething();
}
Będzie on implementowany przez z ServiceA
i wykorzystany w ServiceB
. Jeśli uruchomimy wcześniejszy test, to wszystko powinno zadziałać, ponieważ Spring za nas “połączy kropeczki”. Dobra, wykonaliśmy takie kroki nie bez przyczyny. Chcielibyśmy stworzyć fake dla naszej nowej abstrakcji, czyli dokładniej rzecz ujmując — implementację, która będzie trywialna. Skoro ReturningSomething
to interfejs funkcyjny (tylko jedna metoda) to, zamiast tworzyć nową klasę, możemy skorzystać z wyrażenia lambda np. () -> "X"
. Tylko niezbędne będzie teraz wykonanie następujących kroków.
Na start musimy utworzyć nową klasę konfiguracyjną. Tylko, zamiast tworzyć klasę produkcyjną, możemy skorzystać z klasy na poziomie testów oznaczonej adnotacją @TestConfiguration
.
1
2
3
4
5
6
7
8
9
10
@TestConfiguration
class TestConfig {
@Bean
@Primary
ReturningSomething returningSomething() {
return () -> "X";
}
}
Komunikujemy w ten sposób, że ta konfiguracja jest niezbędna tylko na potrzeby testów. Następnie musimy w jej ramach utworzyć metodę rejestrującą nowego beana, który będzie typu ReturningSomething
. Dodajemy przy okazji do tej definicji adnotację @Primary
, ponieważ finalnie bean typu ServiceA
będzie znajdował się w kontekście Springa podczas uruchomienia testów. Można próbować z tym walczyć, ale powstaje pytanie, czy warto. To zostawiam jako zadanie domowe. Teraz musimy naszej klasie testowej powiedzieć, aby miała na uwadze nasz nowy komponent.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootTest(
classes = TestConfig.class
)
class ServiceBIntFirstTest {
@Autowired
ServiceB serviceB;
@MockitoBean
ServiceA serviceA;
@Test
void test() {
when(serviceA.returnSomething()).thenReturn("A");
assertEquals("result = A", serviceB.print());
}
}
Robimy to poprzez atrybut classes
dostępny w adnotacji @SpringBootTest
. Jeśli uruchomimy teraz test, to on nie przejdzie. A to z tego powodu, że do instancji ServiceB
została wrzucona nasza fake’owa implementacja interfejsu ReturningSomething
, która zwraca X
. Poprawiając asercję na "result = X"
test zacznie przechodzić. Powstaje teraz pytanie, czy mock ServiceA
jest niezbędny? Przekonajmy się i go usuńmy. Test przechodzi, więc wychodzi na to, że ten mock był już niepotrzebny.
Podsumowanie
Co nam daje taka “zabawa”? Po pierwsze testy są bardziej odporne na zmiany w kodzie produkcyjnym. Testy nie są świadome wykorzystywania konkretnych metod, ich sygnatur, w danym przypadku biznesowym. Możemy dowolnie zmieniać implementację, a na potrzeby testów tylko dostosować fake’a do nowych sygnatur metod, o ile oczywiście uległy zmianie.
Minusem jest na pewno konieczność dorobienia dodatkowej konfiguracji na potrzeby testów. Więc nie dość, że jest większa ilość pracy do wykonania, to jeszcze potencjalnie możemy stworzyć kilka kontekstów Springa w testach, o czym pisałem w poprzednim wpisie. Jednak będąc ostrożnym, to jesteśmy w stanie w łatwy sposób, to ryzyko wyeliminować. Kwestia, którą drogą podążyć zależy tylko od Ciebie.
To by było na tyle. Dziękuję Ci za Twój czas i liczę, że poznałeś nową technikę, którą możesz umieścić w swoim programistycznym toolboxie. Na razie i cześć!