Testowanie NgRx – komponenty

W ostatnim wpisie przedstawiłem sposoby testowania selektorów, reducerów oraz efektów.
http://www.angular.love/2018/12/16/testowanie-ngrx-jak-zaczac/
Czas zabrać się za komponenty! Teraz już nie będzie tak łatwo, szybko i przyjemnie 😉 Zaanlizuję przypadki testowania komponentów, które:

  1. wyłącznie dispatchują akcje (store.dispatch)
  2. dispatchują akcje oraz wyciągają dane ze store za pomocą metody select lub pipeable selectOperator

Z ciekawostek: W NgRx 6.1.0 pojawiła się informacja, że store.select staje się przestarzały (deprecated) i przeskakujemy na pipeable operator select. Od NgRx 7.0.0 beta jest już to nieaktualne, autorzy NgRx stwierdzili, że nie jest to jednak dobry pomysł z wielu powodów, więc można dalej śmiało używać metody select lub jak kto woli, operatora select.

UWAGA: Artykuł jest przeznaczony dla osób już obytych z pracą w NgRx & Angular i ze znajomością frameworka do testowania Jasmine lub innego. W artykule nie tłumaczę funkcji z Jasmine i mokowania modułów testowych. Skupiam się wyłącznie na teście NgRx.

Jeśli nigdy nie testowałeś komponentów, zacznij od przeczytania dokumentacji :-):
https://angular.io/guide/testing

Co testować w komponencie świadomym Store?

Ja testuję wyłącznie:

  • czy store.dispatch wysyła dobrą akcję (np. na kliknięcie etc.)
  • streamy wystawione przez selektory i łączone np. poprzez combineLatest, po którym dalej np. w operatorze map jest jakaś logika. W takiej sytuacji, w teście subskrybuję się do takiego strumienia udostępnionego pod polem klasy i sprawdzam  emitowaną wartość.

Oczywiście testuję również inne zachowania komponentu, ale powyższe wymienione są stricte związane z NgRx.

1. Tylko dispatch w komponencie

Przetestowanie komponentu świadomego Store, który wyłącznie wysyła akcje, jest najprostszą sytuacją. Rozpatrzmy nasz przypadek testowy:

template:

klasa:

Jak widzisz, żadnych selektorów. Wyłącznie wysłanie akcji na kliknięcie przycisku „Fetch Cars”. Zatem napiszmy test, który sprawdzi, czy akcja została wysłana na kliknięcie:

Ok… to było proste! 🙂 Nawet nie musiałem mockować Store, wystarczyło zaimportować StoreModule.forRoot, który normalnie oczekiwałby mapy reducerów. Nie interesuje nas testowanie reducerów, więc wstawiłem pusty obiekt, tym niemniej zaimportowanie tego modułu jest konieczne do działania Store:

W samym teście, interesuje nas, czy na kliknięcie została wysłana akcja:

Oczywiście musiałem wcześniej stworzyć szpiega, aby móc użyć „toHaveBeenCalledWith(new FetchCars())” :

voilà! Sprawa się komplikuje, gdy w naszym komponencie pojawiają się selektory.

2. Dispatch + Select w komponencie

Dorzućmy do klasy komponentu selektor:

I do templatki asyncPipe:

Pierwsza myśl – wyciągamy selektorem listę samochodów, ale skąd nasz Store w teście ma posiadać listę samochodów, którą udostępni selektorowi getCars? Spodziewam się errora przy odpaleniu wcześniej napisanego testu:

oczywiście, otrzymaliśmy error. Zatem nasz test musimy być świadomy listy samochodów, aby zadziałał. Pamiętaj również, że nie chcemy tu testować selektora – on ma swój osobny test.
Teraz mamy do wyboru dwie drogi:

1) zamokować co zwraca select

Z tym podejściem spotkałem się już w projektach i różnych tutorialach. Generalnie nie polecam, ale o tym zaraz ;):

Powyżej użyłem .and.callFake, aby zawołać swoją implementację dla selektora, sprawdziłem także za pomocą IFa, czy chodzi mi na pewno o ten selektor, dla którego chce zwrocić Observabla (select zwraca typ Observable) z pustą listą samochodów. Teraz test się nie wysypuje i wszystko gra, bo asyncPipe w templatce, ma się na co zasubskrybować!

Minusy:
– nie działa z pipeable selectOperator (store.pipe(select(selector…)))

– w przypadku wielu selektorów w komponencie, musimy pamiętać o obsłużeniu wszystkich w callFake i zadbać, aby zwracały dobre wartości jeżeli to istotne
– niewygodne, jeśli np dla pewnego testu chciałbym aby selektor zwracał jakąś inną wartość (bo może np. selektor ma jakąś logikę, która może być powiązana z jakimś zachowaniem na UI – np. jeśli selektor wyemitował true, to pokaż mi element). Musiałbym powtarzać się z callFake.
– porównanie === wysypie się dla selektorów parametryzowanych, zwracanych przez funkcje, przykład poniżej:

Plusy:
– jeśli selektor polega na wielu innych selektorach, przykładowo:

To bardzo łatwo mogę zamokować co zwraca, bez dowożenia do selektora potrzebnych mu wartości
– Nie muszę mokować w teście wartości dla Store, z których korzystają selektory znajdujące się w testowanym komponencie

2) zamokować store z danymi, aby select się nie wysypał z braku danych

Następujące podejście bardziej mi się podoba. Polega na stworzeniu swojej klasy generycznej MockStore z metodą setState, i dziedziczącej po Store. Ja korzystam z gotowca:
https://github.com/tomastrajan/angular-ngrx-material-starter/blob/master/src/testing/utils.ts#L16

Teraz test wygląda następująco:

Kluczowym momentem powyżej jest użycie metody setState – ustawiamy w niej stan wyłącznie dla naszego testu, począwszy od najwyższej gałęzi w State (dostarczamy stan globalny, ale tylko tyle, ile potrzebujemy!).

Minusy:

  • w przypadku użycia w komponencie selektorów bazujących na wielu wartościach ze Store z różnych modułów, musimy zadbać aby je wszystkie dostarczyć w setState

Plusy:

  • nie obchodzi nas mokowanie selektorów
  • kompatybilne z pipeable select operator i store.select
  • test wygląda czyściej i rozwiązanie jest lepiej skalowalne (można np. podrzucić initialState, jeśli nie potrzebujemy danych)
  • najlepsze – możemy dla dowolnego testu zawołać store.setState z innymi wartościami i przetestować np. specyficzny przypadek zachowania UI dla danego stanu

Cały test:

Podsumowanie

Najtrudniejszym momentem jest przygotowanie modułu testowego, aby test w ogóle się poprawnie uruchomił. Samo sprawdzanie wysyłania akcji to banał. Pamiętaj, aby nie testować w teście klasy komponentu czy selektor zwraca dobrą wartość! Selektory mają swoje testy. Testuj wszystko, co uważasz za słuszne i istotne dla Twojego projektu :).

Swoją drogą, autorzy NgRx pracują nad swoim mockStore utilem do testowania. Więcej informacji tutaj:
https://github.com/ngrx/platform/issues/915
Właściwie to MockStore w 7.0.0-beta.1 został już zmergowany ale to świeża sprawa (bodajże z 25 listopada), ale do końca to jeszcze nie działa z wszystkimi zapowiadanymi funkcjonalnościami, jak np. mokowanie selektorów.
https://github.com/ngrx/platform/pull/1027#issuecomment-433054427

W każdym razie, jeśli już jedziesz na NgRx 7.0.0-beta.1 to polecam wypróbować ten już z @ngrx/testing:
https://github.com/ngrx/platform/pull/1027, funkcja provideMockStore

ps. nauczyłeś się czegoś nowego? to zostaw lajka na FB!
https://www.facebook.com/www.angular.love/

5 Comments

  1. Tomasz

    Siema, takie pytanie:
    Testując componenty defakto sa to testy integracyjne a nie jednostkowe. Wiec czy jest sens mockowania State ?

    Jeśli napisałbyś ten test bez mokowanie stata to wtedy podczas configureTestingModule wogole nie trzeba podawac providers: [State].
    Wciąż możesz wyciągnąć state i go zespyowac zeby sprawdzic czy akcja została z dispatchowana.

    W drukim przypadku jak chcesz sprawdzic czy dana sie wyświetlone ze stata. Mozesz przekazac state zamiast pustego obiektu w: StoreModule.forRoot({ feature: fromFeature.reducer }).
    Dzieki temu test staje sie „super integracyjny” Bo sprawdzi tez czy cały Store działa poprawnie.
    Dodatkowo sam test jest duzo bardziej czytelny.

    P.S Jesli twoj initialState ktore przekazałes do testu w fromFeature.reducer nie ma odpowiedniej wartosci. Wciaz mozesz normalnie z dispachowac akcje ktora Ci ustawi odpowiedni state i dopiero potem wykonać jakąś assercje.
    P.S.2 To podejscie sprawdza sie tez przy testowanie Effektow.

    Co o tym myślisz?

    • Tomasz Nastały

      hej Tomek,

      „Testując componenty defakto sa to testy integracyjne a nie jednostkowe. ” -> właściwie mix integracyjnych i jednostkowych, w zależności co testujemy z komponentu :).
      —-
      „Wciaz mozesz normalnie z dispachowac akcje ktora Ci ustawi odpowiedni state i dopiero potem wykonać jakąś assercje.” – to podejście jest słabe w przypadku testu komponentów, zwłaszcza jak mamy komponent, który wyciąga wiele rzeczy ze stanu, wtedy musisz dispatchować X akcji na starcie.
      —-
      „…wogole nie trzeba podawac providers: [State].” – jak w ogóle nie trzeba? test od razu się wysypie z powodu braku dostarczenia zależności komponentu.
      —-
      StoreModule.forRoot({ feature: fromFeature.reducer }).
      Dzieki temu test staje sie „super integracyjny” Bo sprawdzi tez czy cały Store działa poprawnie.” – to może być niewygodne w przypadku bardziej skomplikowanych sytuacji, jak np. mapa reducerów jest bardziej rozbudowana a komponent wyciąga ze stanu rzeczy z różnych części store, nie tylko z danej części z reducerami dodanymi do testu. I znowu pojawia się problem, jak chcemy zasymulować konkretny stan store, wracamy do dispatchowania akcji. Wygodniej jest dla mnie użyć store.setState(…), które np. dla mnie będzie bardziej czytelne.

      pzdr!

  2. Tomasz

    Dziwne u mnie te testy przechodza bez podawania providers: [Store]. Wydaje mi sie że u Ciebie testy sie wywalaja bo podajesz pusty state w forRoot.

    Nie musisz dispatchowac akcji mozesz na starcie podac taki state ktory jest wymagany do testu komponentu.

  3. Nie do końca rozumiem 🙂
    „Nie musisz dispatchowac akcji mozesz na starcie podac taki state ktory jest wymagany do testu komponentu.”
    gdzie mam go podać? możesz wkleić przykład Twojego testu komponentu?

  4. Tomasz

    Chodzi mi o to, że albo uzywasz w testach smart componentow. `imports: [StoreModule.forRoot({})] ALBO providers: [Store].
    Uzywanie obu wg mnie jest nie poprawne. Jesli uzywasz imports to wtedy testujesz component w założeniu że sa to testy integracyjne (Sprawdzaja component i caly Store)
    Jesli używasz providets to testujesz unitowo.

    Jeśli używasz StoreModule.forRoot to on juz sam dodaje Store wiec nie musisz go znowy dodawac w providers.
    https://github.com/ngrx/platform/blob/7.0.0/modules/store/src/store_module.ts#L140

    Tutaj jest repo. Napisalem testy integracyjne z danym statem
    https://github.com/Tomek6789/AngularTests/blob/master/tests/src/app/app.component.spec.ts#L21

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *