System komentarzy do bloga III - Test
Posty z tej serii:
- System komentarzy do bloga I - Design
- System komentarzy do bloga II - Develop
- System komentarzy do bloga III - Test
- System komentarzy do bloga IV - Deploy
Jest to trzecia część cyklu na temat systemu komentarzy do bloga. W dwóch poprzednich częściach widzieliśmy projekt rozwiązania oraz jego poszczególne elementy implementacji. W tej części zobaczymy wybrane fragmenty implementacji testów.
Narzędzia
Poniższe przykłady bazują na wersjach:
- C# ver. 7.1
- NServiceBus ver. 6.4.3
- NServiceBus.Testing ver. 6.0.3
- NUnit ver. 3.9.0
- NSubstitute ver. 3.1.0
W procesie wytwarzania oprogramowania istnieje wiele różnych rodzajów testów. Jedną z wielu wartości, jakie daje pisanie testów, jest podniesienie jakości tworzonych funkcjonalności. W tej części skupimy się testach jednostkowych oraz integracyjnych.
Unit Tests
To co jest bardzo fajne w NServiceBus’e to jego bardzo rozbudowane możliwości. Jedną z takich możliwości jest testowanie jednostkowe zaimplementowanych Message Handler'ów
przy pomocy NServiceBus.Testing
. Zobaczmy przykłady:
Rozłóżmy powyższy kod na czynniki pierwsze. Nazwa metody reprezentującej test stworzona jest wg konwencji X_Y_Z
gdzie:
- X oznacza nazwę metody, który jest testowana -
Handle
- Y oznacza scenariusz, jaki jest testowany -
StartAddingComment
- Z oznacza oczekiwany rezultat testu -
SendCreateBranchWithProperData
Składając wszystko razem, można wywnioskować, że testowana jest metoda Handle
w scenariuszu dodania komentarza, a oczekiwanym rezultatem jest wysłanie message’a CreateBranch
z poprawnymi danymi. Ciało metody składa się z trzech sekcji:
- Arrange - w tej sekcji inicjowane są wszystkie elementy potrzebne do wykonania testu
- Act - w tej sekcji wywoływana jest metoda, której dotyczy test
- Assert - w tej sekcji sprawdzany jest rezultat wywołania metody z sekcji
Act
Nie ma jednej uniwersalnej metody analizowania testu. Patrząc na całość, widać, że najprostszą sekcją jest sekcja Act
, gdzie od razu możemy zobaczyć, co jest testowane. Potem można przejść do sekcji Assert
, aby przeanalizować, jaki jest spodziewany rezultat, a na końcu sprawdzić, w jaki sposób tworzone są elementy wymagane do wykonania testu.
Z racji tego, że testów jest zawsze więcej niż jeden, pewne powtarzające się elementy kodu można uwspólnić tak, aby możne je było wykorzystać w pozostałych testach. W przykładzie są trzy takie elementy:
- var saga = this.GetHandler(); - metoda
GetHandler
tworzy Message Handler’a, którego metody będą testowane - var context = this.GetContext(); - metoda
GetContext
tworzy context NServiceBus’a tak, aby można było uruchomić test bez konieczności odwoływania się do zewnętrznym źródeł danych np. Queue Transport’u - var sentMessage = this.GetSentMessage<CreateBranch>(context); - metoda
GetSentMessage
zwraca message, który powinien zostać wysłany przez metodęHandle
Patrząc jeszcze raz na całość widać, że oczekiwanym rezultatem jest to, aby metoda Handle
wysłała message typu CreateBranch
z poprawną wartością CommentId
Zobaczmy implementację poszczególnych pomocniczych metod.
Ponieważ message handler HandlerCommentSaga
posiada zależność do IConfigurationManager
, w pierwszej kolejności przy pomocy biblioteki NSubstitute
tworzona jest fake’owa instancja konfiguratora tak, aby nie odwoływać się do prawdziwej implementacji. Następnie zwracany jest Saga handler z przykładowymi danymi
Jest to proste opakowanie klasy TestableMessageHandlerContext
, która jest częścią NServiceBus.Testing
Jest to ponowne opakowanie funkcjonalności udostępnianej przez NServiceBus.Testing
, która zwraca message wysłany przez testowaną metodę. Opakowanie jest trochę bardziej złożone, ponieważ używa parametru generycznego tak, aby kod wołający metodę mógł decydować, jakiego typu message’a się spodziewa.
Zobaczmy inny, trochę bardziej złożony, scenariusz testowy.
W implementacji tego testu został użyty feature biblioteki NUnit
o nazwie TestCase-Attribute. Pozwala on w jednej testowej metodzie zakodować wiele scenariuszy. Kod dla każdego scenariusza jest taki sam, a jedyną różnicą są parametry wejściowe oraz oczekiwany rezultat testowanej metody. Inną możliwością jest napisanie czterech osobnych testów dla każdego scenariusza. Patrząc na nazwę metody można wywnioskować, że sprawdzany jest status odpowiedzi na komentarz w zależności od stanu Pull Request'a
. Testowana metoda GetCommentResponseStatus
przyjmuje dwa parametry:
-
Func<Task<(bool result, string etag)>> - parametr ten jest funkcją zwracającą informację tak lub nie, na pytanie czy
Pull Request
jest nadal otwarty. Rezultat, jaki ma być zwrócony przez tę funkcję, sterowany jest poprzez parametr metodyisPullRequestOpen
. -
Func<Task<bool>> - parametr ten jest również funkcją, która zwraca informację o tym, czy
Pull Request
został z’merge’owany do gałęzimaster
. Rezultat, jaki ma być zwrócony przez tę funkcję, sterowany jest prze parametrisPullRequestMerged
.
Wynik zwracany przez testowaną metodę GetCommentResponseStatus
porównywany jest to oczekiwanego rezultatu, który również jest sterowany przez parametr wejściowy expectedResult
. Scenariusze do przetestowania opisane są w atrybutach metody testującej.
Integration Tests
Do pisania testów integracyjnych można wykorzystać tę samą technikę co przy pisaniu testów jednostkowych. Zasadniczą różnicą, w stosunku do testów jednostkowych jest to, że testy integracyjne odwołują się do prawdziwych źródeł danych. W projektach integracyjnych zazwyczaj występuje więcej niż jedno zewnętrzne źródło, z którym trzeba się zintegrować, co sprawia, że bardzo trudno jest przygotować tak dane oraz asercje, aby testy wykonywały się automatycznie. Przykładowo, w tym projekcie takim zewnętrznymi źródłami są GitHub oraz dostawca, za pomocą którego można wysyłać maile. Mimo tego testy integracyjne są bardzo pomocne, jeśli chcemy przetestować jeden konkretny kawałek rozwiązania, przygotowując dane oraz sprawdzając wyniki manualnie. Nie trzeba wtedy uruchamiać całości, a jedynie wybrany fragment. Oczywiście zawsze warto mieć końcowe testy integracyjne tak, aby nawet ręcznie przejść przez cały proces i sprawdzić czy wszystko działa zgodnie z oczekiwaniami. Zobaczmy przykłady:
W tym teście testowana jest implementacja metody CreatePullRequest
, która tworzy Pull Request'a
na GitHub'e
. Metoda ta jako rezultat zwraca URL
do utworzonego Pull Request'a
. Rezultat ten jest sprawdzany w sekcji Assert
. Dodatkowo zwrócony rezultat wypisywany jest na standardowe wyjście. Aby rzeczywiście przekonać się czy metoda działa zgodnie z oczekiwaniami, trzeba zalogować się na konto GitHub'a
i sprawdzić, czy Pull Request
faktycznie został utworzony.
Na koniec zobaczmy, jak za pomocą testu integracyjnego można zasymulować końcowego klienta, który będzie inicjował proces dodawania komentarza.
Tym razem zacznijmy analizę od początku. W sekcji Arrange
tworzony jest klient HTTP
, za pomocą którego zostanie wysłane żądanie typu POST
oraz przykładowy komentarz (aby test zadziałał wymagane są rzeczywiste dane). W sekcji Act
wysyłane jest żądanie utworzenia komentarza. W sekcji Assert
sprawdzany jest wynik żądania. Każdy inny rezultat niż sukces traktowany jest jako błąd, który wypisywany jest na standardowe wyjście.
Tych parę przykładów pokazuje, w jaki sposób można pisać testy. W następnym poście zobaczymy możliwości wdrażania wytworzonych komponentów.
=