Cześć! Dzisiaj skupimy się na problemie, w którym jeden z serwisów korzysta z zewnętrznego systemu, aby zrealizować swój cel. Niestety ten zewnętrzny system wolno odpowiada albo wręcz jest niestabilny, często dochodzi do awarii (brak dostępności). Nie chcemy, aby to wpływało na nasz lokalny development. Zobaczmy, jak możemy sobie poradzić z tym problemem.

Symulacja sytuacji w aplikacji

Załóżmy, że mamy ServiceB który wykorzystuje ServiceA, aby zrealizować swój cel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class ServiceB {

  private final ServiceA serviceA;

  public ServiceB(ServiceA serviceA) {
    this.serviceA = serviceA;
  }

  public String print() {
    return "result = " + serviceA.returnSomething();
  }

}

Na start przygotujmy sobie test integracyjny, oczywiście oparty o Spring Boota. Wstrzykujemy w nim ServiceB i piszemy metodę testową. W asercji dajemy assertEquals("result = A", serviceB.print());, ponieważ ServiceA w metodzie returnSomething ma na stałe wpisane, że zwraca po prostu literkę A.

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootTest
class ServiceBIntTest {

  @Autowired
  ServiceB serviceB;

  @Test
  void test() {
    assertEquals("result = A", serviceB.print());
  }

}

Uruchommy test. Przechodzi, ale da się zauważyć, że trwa on bardzo długo. Dlaczego tak się dzieje? ServiceA w implementacji ma przygotowaną symulację wywoływania zewnętrznego systemu. Aby nie robił tego naprawdę skorzystaliśmy z Thread.sleep(3_000).

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class ServiceA {

  public String returnSomething() {
    try {
      Thread.sleep(3_000);
    } catch (InterruptedException _) {
      // consume
    }
    return "A";
  }

}

Rozwiązanie problemu

Aby przyspieszyć nasz test, możemy skorzystać z mocka, wykorzystując adnotację @MockitoBean.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
class ServiceBIntTest {

  @Autowired
  ServiceB serviceB;

  @MockitoBean
  ServiceA serviceA;

  @Test
  void test() {
    assertEquals("result = A", serviceB.print());
  }

}

W tej sytuacji, na potrzeby testu, implementacja serwisu ServiceA zostanie zaślepiona przez Springa. Sprawdźmy, czy to wystarczy. Okazuje się, że nie. Nasz mock nie wie, jak ma się zachować. W przypadku, gdy metoda zwraca typ, który nie jest prymitywny, to zamiast tego jest po prostu wstawiany null. W przypadku typów prymitywnych będą podstawione ich domyśle wartości. Musimy rozwiązać ten zastany problem.

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());
  }

}

Niezbędne będzie nauczenie mocka, że gdy wywołamy na nim metodę returnSomething, to chcemy uzyskać zawsze literkę A. W ten oto sposób test przejdzie bez najmniejszego problemu. W dodatku będzie trwał kilkadziesiąt milisekund zamiast kilku sekund.

Ciekawostka dotycząca kontekstów Springa w testach

Zróbmy sobie dodatkowy eksperyment. Stwórzmy po prostu kopię aktualnej klasy testowej, w której usuniemy naszego mocka. Jeśli uruchomimy teraz te dwa testy, to zobaczymy, że Spring utworzy nam dwa konteksty. Z tej racji każdy z testów będzie trwał odpowiednio dłużej. Natomiast jeśli przywrócimy mocka, tak jak mieliśmy to w przypadku pierwszego testu, to Spring utworzy tylko jeden kontekst, który będzie użyty w obydwu testach. Dlaczego tak się dzieje?

Spring tworzy cache kontekstów. Jeśli uzna, że dany test ma taki sam kontekst jak inny, to skorzysta właśnie z tego kontekstu będącego w cache. W przypadku różnic w kontekstach Spring będzie musiał utworzyć nowy kontekst. Dlatego, tworząc mocka w jednym teście, zmieniamy podstawowy kontekst. Nie mamy produkcyjnej konfiguracji, tylko taką dostosowaną do potrzeb testów. Stąd potrzeba utworzenia nowego kontekstu.

Spring nie może mieć pewności, że aplikacja w tych dwóch testach zachowania się identycznie. Stąd taki mechanizm. Warto mieć tę informację z tyłu głowy podczas pisania testów. Zanim doda się kolejnego mocka, warto się dwa razy zastanowić czy nie spowolni to naszych testów.

Podsumowanie

Mocki to mega przydatne narzędzie. Pozwalają nam rozwiązać problemy podobne do tego zaprezentowanego na początku artykułu. Jednak warto znać również ciemne strony zaślepek. W przypadku testów Springa ma to niebagatelne znaczenie pod kątem czasu trwania testów.

To by było na tyle. Mam nadzieję, że ten wpis był dla Ciebie bardzo przydatny. Do zobaczenia w kolejnych. Na razie i cześć!