Wróć do strony głównej
NestJS

NestJS – Mockowanie zewnętrznych zależności w testach e2e aplikacji

Testowanie e2e w NestJS

Każdy z nas spotkał się z przypadkiem, gdy testowanie E2E jest nie do końca możliwe z różnych powodów, zwłaszcza gdy system zewnętrzny, na którym działamy:

  • nie posiada w ogóle wersji developerskiej/sandboxa
  • wysyłanie żądań wiąże się z dodatkowymi kosztami
  • przeprowadza fizyczne operacje na realnych urządzeniach
  • tworzy niepożądane artefakty uboczne (wysyłka maili, notyfikacji)
  • zewnętrzny system jest niestabilny (jego działanie, a właściwie jego brak, powoduje błędy w testach)
  • nie mamy bezpośredniej kontroli nad wykonywaniem operacji po jego stronie (wysłanie ITNa, webhooka z nowymi danymi – np w CMS)

Na potrzeby rozważań pogodzimy się z redukcją naszych testów do formalnie integracyjnych, jednak traktujących nasz system/aplikację jako całość. Warto nadmienić, że wyżej wymienione przeszkody mogą być akceptowalne w kontrolowanych środowiskach bądź w przypadku krytycznego procesu biznesowego (jak chociażby kupno realnego biletu autobusowego w systemie produkcyjnym w okresach poprzedzających największe natężenie ruchu)

Możliwe podejścia

Jak w takim razie poradzić sobie z naszym wyzwaniem? Zastanówmy się nad potencjalnymi rozwiązaniami:

  • oszukiwanie żądań HTTP, w szczególności
    • stub istniejących metod HttpService
    • testowy HttpService
    • interceptor żądań na poziomie biblioteki (np axios)
  • podstawienie “własnego” serwisu imitującego zachowanie zewnętrznego serwisu
  • podstawienie imitacji zależności całej “paczki” odpowiadającej za realizację zadań

Sprawdźmy sposoby użycia w/w w testach.

Nadpisanie implementacji metod HttpService

Zalety:

  • trywialna implementacja
  • bardzo niski próg wejścia

Wady:

  • wymaga przechowywania referencji do zmockowanej metody
  • wymaga “ręcznego” sprawdzenia parametrów wywołania
  • dopuszcza możliwość “przepuszczenia” niechcianych żądań
  • w teście musimy znać wszystkie możliwe żądania
  • w teście musimy sprawować kontrolę nad kolejnością wykonywania (np pozwolić zakończyć się sukcesem dopiero trzeciemu z kolei żądaniu o takich samych parametrach)

Własny HttpService

Polega na “wstawieniu” naszego własnego serwisu w miejsce oryginalne (np poprzez DI). Przykładowa implementacja w Angular. W wersji skróconej użycie wygląda tak:

Zalety:

  • duża kontrola nad wykonywaniem żądań (pozwala chociażby opóźniać “wykonanie” żądania)

Wady:

  • duża kontrola nad wykonywaniem żądań (cóż, z drugiej strony wymaga to akcji ze strony programisty)
  • w teście musimy znać wszystkie możliwe żądania
  • w teście musimy sprawować kontrolę nad kolejnością wykonywania (np pozwolić zakończyć się sukcesem dopiero trzeciemu z kolei żądaniu o takich samych parametrach)

Bardziej “naiwna” wersja:

W wersji “naiwnej” mamy możliwość samodzielnej implementacji zachowań, własnych “matcherów” i tym podobne. W porównaniu do nadpisania implementacji w oryginalnym HttpService, ta opcja ma “wbudowany” jest.fn().

Axios interceptor

Zalety:

  • możliwość “dopasowania” żądania nawet po nagłówkach i body 
  • nie wymaga nadpisywania/zmiany w aplikacji

Wady:

  • “dobieramy” się do wnętrza HttpService (który bazuje na bibliotece axios)
  • w teście musimy znać wszystkie możliwe żądania
  • w teście musimy sprawować kontrolę nad kolejnością wykonywania (np. pozwolić zakończyć się sukcesem dopiero trzeciemu z kolei żądaniu o takich samych parametrach)

Serwis imitujący zewnętrzną zależność

Możemy też uruchomić “obok” drugą aplikację, która będzie odbierała żądania wysyłane przez nasz system. Przyjmujemy założenie, że adres(y), pod które powinny dotrzeć żądania, nie są hardocodwane w naszej aplikacji, lecz znajdują się w konfiguracji (zmiennych środowiskowych).

Zalety:

  • pełna kontrola nad procesem zewnętrznym (np udawanie “webhooka”)
  • odseparowanie zachowania zewnętrznego serwisu od testów aplikacji
  • względna reużywalność w testach
  • kontrola nad obsługą ścigających się żądań
  • wiedza o działaniu zewnętrznej usługi jest ukryta w jednym miejscu
  • próg wejścia (dla istniejących imitacji, programista nie musi znać zewnętrznego procesu)

Wady:

  • początkowy koszt implementacji (samodzielna aplikacja NestJS)
  • w przypadku rosnącej ilości takich “imitacji”, koszt uruchomienia ich (a więc i testów) wzrasta (w zależności od ich złożoności i liczności)
  • może wymagać znajomości zewnętrznego procesu (np potwierdzanie płatności przez ITN)
  • jako programista-konsument, nie musimy już wprawdzie znać konkretnych żądań, ale nadal potrzebujemy wiedzieć, że ten serwis działa

Imitacja paczki/biblioteki

No właśnie – żadne z powyższych nie uratuje nas, gdy chcemy przetestować zewnętrznego dostawcę, z którym komunikacja nie kończy się na kilku pogmatwanych żądaniach HTTP, które są w miarę stabilne (np wysyłanie pliku na S3, używając aws-sdk ). Na szczęście nie jesteśmy bezsilni – możemy zmockować całą paczkę lub dostarczyć mock zenkapsulowanej biblioteki (robimy tak, prawda?) – implementacja wygląda analogicznie jak Mock HttpService .

Podsumowanie – co wybrać?

Bez żartów – to rzeczywiście zależy. Dobierajmy siły na zamiary (zespołu, produktu, czasu). Im bardziej skomplikowany proces związany z zewnętrznym dostawcą, tym więcej kontroli możemy potrzebować – zwłaszcza, jeżeli istnieją jakieś ograniczenia od tegoż. 

W myśl zasady ciągłego refaktoru, możemy z czystym sercem zaczynać od ordynarnego nadpisywania metod  HttpService  i ewolucji do kolejnych sposobów w miarę rosnącego skomplikowania procesu. W większości przypadków interceptor Axiosa/stub HttpService będzie w zupełności wystarczający – zwłaszcza, że część z naszych unit testów, z dużym prawdopodobieństwem, posiada już zaimplementowane “oszustwa”, więc łatwo o reużywalność.

O autorze

Kamil Gajowy

Zapisz się do naszego newslettera. Bądź na bieżąco z najnowszymi trendami, poradami, meetupami i stań się częścią społeczności Angulara w Polsce. Rynek pracy docenia członków społeczności.

2 komentarzy

  1. Mateusz

    Fajnie byłoby poczytać o mockowaniu repo oraz bazy 😉 osobiście korzystamy z np MongoInMemoryServer, w drugiej pracy jest obszar pracy na żywca – nie ma tam żadnych insertów ale no cóż trochę to wykonywanie trwa ;/

    • Cześć Mateusz!

      Tak, świetny pomysł na artykuł! O którym obszarze najchętniej byś poczytał? E2E (budowanie świata / BDD, seedy), integracyjne, unity, stawianie bazy pod lokalny development/CI ? A może masz konkretny problem z wydajnością testów?

      z góry dzięki!

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *