Projektowanie testów jednostkowych dla systemu stanowego


20

tło

Test Driven Development został spopularyzowany po ukończeniu szkoły i w branży. Próbuję się tego nauczyć, ale pewne ważne rzeczy wciąż mi uciekają. Zwolennicy TDD mówią wiele rzeczy takich jak (zwanych dalej „zasadą pojedynczego twierdzenia” lub SAP ):

Od jakiegoś czasu zastanawiałem się, jak testy TDD mogą być tak proste, wyraziste i tak eleganckie, jak to tylko możliwe. W tym artykule wyjaśniono trochę, jak to jest uczynić testy tak prostymi i rozłożonymi, jak to możliwe: dążąc do jednego stwierdzenia w każdym teście.

Źródło: http://www.artima.com/weblogs/viewpost.jsp?thread=35578

Mówią także takie rzeczy (zwane dalej „zasadą metody prywatnej” lub PMP ):

Zasadniczo nie testuje się bezpośrednio metod prywatnych. Ponieważ są one prywatne, rozważ je jako szczegół implementacji. Nikt nigdy nie zadzwoni do jednego z nich i nie spodziewa się, że zadziała w określony sposób.

Zamiast tego powinieneś przetestować swój publiczny interfejs. Jeśli metody wywołujące metody prywatne działają zgodnie z oczekiwaniami, to przyjmujemy przez rozszerzenie, że metody prywatne działają poprawnie.

Źródło: Jak przeprowadzasz testowanie metod prywatnych?

Sytuacja

Próbuję przetestować stanowy system przetwarzania danych. System może robić różne rzeczy dla tego samego kawałka danych, biorąc pod uwagę jego stan przed otrzymaniem tych danych. Rozważ prosty test, który buduje stan w systemie, a następnie testuje zachowanie, które ma przetestować dana metoda.

  • SAP sugeruje, że nie powinienem testować „procedury budowania stanu”, powinienem założyć, że stan jest tym, czego oczekuję od kodu budowania, a następnie przetestować zmianę stanu, który próbuję przetestować

  • PMP sugeruje, że nie mogę pominąć tego kroku „budowania stanu” i po prostu przetestować metody rządzące tą funkcjonalnością niezależnie.

Rezultatem mojego rzeczywistego kodu są testy, które są rozdęte, skomplikowane, długie i trudne do napisania. A jeśli zmiany stanu zmieniają się, testy muszą zostać zmienione ... co byłoby w porządku w przypadku małych, wydajnych testów, ale niezwykle czasochłonnych i mylących z tymi długo rozdętymi testami. Jak to zwykle się robi?


2
Nie sądzę, że znajdziesz na to eleganckie rozwiązanie. Ogólne podejście nie polega na tym, aby system był na początku stanowy, co nie pomaga w testowaniu czegoś, co już zostało zbudowane. Przekształcenie go w stan bezstanowy prawdopodobnie też nie jest warte kosztów.
Doval


@Doval: Proszę wyjaśnić, jak zrobić coś takiego jak telefon (SIP UserAgent) niepaństwowy. Oczekiwane zachowanie tego urządzenia jest określone w RFC za pomocą diagramu przejścia stanu.
Bart van Ingen Schenau

Czy kopiujesz / wklejasz / edytujesz testy, czy piszesz metody narzędziowe, aby udostępnić wspólną konfigurację / usunięcie / funkcjonalność? Chociaż niektóre przypadki testowe z pewnością mogą być długie i wzdęte, nie powinno to być takie powszechne. W systemie stanowym oczekiwałbym powszechnej procedury konfiguracji, w której stan końcowy jest parametrem i ta procedura prowadzi cię do stanu, który chcesz przetestować. Dodatkowo na koniec każdego testu miałbym metodę porzucenia, która przywraca cię do znanego stanu początkowego (jeśli jest to wymagane), więc twoja metoda konfiguracji będzie działać poprawnie, gdy rozpocznie się następny test.
Dunk

Na stycznej, ale dodam też, że diagramy stanu są narzędziem komunikacji, a nie dekretem implementacyjnym, nawet jeśli jest w RFC. Dopóki spełniasz opisaną funkcjonalność, spełniasz standard. Miałem kilka okazji, kiedy przekształciłem bardzo skomplikowane implementacje przejścia stanu (jak zdefiniowano w RFC) w naprawdę proste ogólne funkcje przetwarzania. Jeden przypadek pamiętam, kiedy pozbyłem się kilku tysięcy wierszy kodu, kiedy zdałem sobie sprawę, że oprócz kilku flag około 5 stanów zrobiło dokładnie to samo, kiedy zmieniłeś nazwę „ukrytych” wspólnych elementów.
Dunk

Odpowiedzi:


15

Perspektywiczny:

Cofnijmy się więc i zapytajmy, w czym TDD próbuje nam pomóc. TDD stara się pomóc nam ustalić, czy nasz kod jest poprawny, czy nie. I poprawnie, mam na myśli „czy kod spełnia wymagania biznesowe?” Chodzi o to, że wiemy, że zmiany będą wymagane w przyszłości i chcemy mieć pewność, że nasz kod pozostanie poprawny po ich wprowadzeniu.

Podnoszę tę perspektywę, ponieważ myślę, że łatwo jest zagubić się w szczegółach i stracić z oczu to, co próbujemy osiągnąć.

Zasady - SAP:

Chociaż nie jestem ekspertem w TDD, myślę, że brakuje ci części tego, czego stara się nauczać Zasada Single Assertion (SAP). SAP można przekształcić w „testowanie jednej rzeczy naraz”. Ale TOTAT nie zsuwa się z języka tak łatwo, jak SAP.

Testowanie jednej rzeczy na raz oznacza, że ​​skupiasz się na jednej sprawie; jedna ścieżka; jeden warunek brzegowy; jeden przypadek błędu; jedno co za test. Podstawową ideą jest to, że musisz wiedzieć, co się zepsuło, gdy przypadek testowy się nie powiedzie, abyś mógł szybciej rozwiązać problem. Jeśli w teście przetestujesz wiele warunków (tj. Więcej niż jedną rzecz), a test się nie powiedzie, masz o wiele więcej pracy. Najpierw musisz określić, które z wielu przypadków zakończyły się niepowodzeniem, a następnie dowiedzieć się, dlaczego ta sprawa zakończyła się niepowodzeniem.

Jeśli testujesz jedną rzecz na raz, zakres wyszukiwania jest znacznie mniejszy, a wada jest wykrywana szybciej. Pamiętaj, że „testowanie jednej rzeczy na raz” niekoniecznie wyklucza patrzenie na więcej niż jeden wynik procesu na raz. Na przykład, podczas testowania „znanej dobrej ścieżki”, mogę oczekiwać konkretnej wartości końcowej, fooa także innej wartości, bari mogę to zweryfikować foo != barw ramach mojego testu. Kluczem jest logiczne zgrupowanie kontroli wyników na podstawie testowanej sprawy.

Zasady - PMP:

Podobnie, myślę, że brakuje ci trochę tego, czego nauczy nas zasada prywatnej metody (PMP). PMP zachęca nas do traktowania systemu jak czarnej skrzynki. Dla danego wejścia powinieneś otrzymać dane wyjściowe. Nie obchodzi Cię, w jaki sposób czarna skrzynka generuje dane wyjściowe. Dbasz tylko o to, aby Twoje wyniki były zgodne z danymi wejściowymi.

PMP jest naprawdę dobrą perspektywą do spojrzenia na aspekty API twojego kodu. Może także pomóc w określeniu zakresu tego, co musisz przetestować. Zidentyfikuj punkty interfejsu i sprawdź, czy spełniają warunki swoich umów. Nie musisz przejmować się tym, w jaki sposób metody ukryte w interfejsie (znane również jako prywatne) wykonują swoje zadanie. Musisz tylko sprawdzić, czy zrobili to, co mieli zrobić.


Applied TDD ( dla Ciebie )

Więc twoja sytuacja stanowi nieco zmarszczkę poza zwykłą aplikacją. Metody aplikacji są stanowe, więc ich wyniki zależą nie tylko od danych wejściowych, ale także od wcześniejszych działań. Jestem pewien, że powinienem <insert some lecture>tutaj powiedzieć o byciu okropnym i bla bla bla bla, ale to naprawdę nie pomaga rozwiązać twojego problemu.

Zakładam, że masz jakąś tabelę diagramów stanów, która pokazuje różne potencjalne stany i co należy zrobić, aby uruchomić przejście. Jeśli nie, będziesz go potrzebować, ponieważ pomoże to wyrazić wymagania biznesowe dla tego systemu.

Testy: Najpierw skończysz z zestawem testów, które wprowadzają zmianę stanu. Idealnie, będziesz mieć testy, które sprawdzają pełen zakres zmian stanu, które mogą wystąpić, ale widzę kilka scenariuszy, w których może nie być konieczne przejście w pełnym zakresie.

Następnie musisz zbudować testy, aby sprawdzić poprawność przetwarzania danych. Niektóre z tych testów stanu zostaną ponownie wykorzystane podczas tworzenia testów przetwarzania danych. Załóżmy na przykład, że masz metodę, Foo()która ma różne wyniki oparte na stanach Initi State1. Będziesz chciał użyć swojego ChangeFooToState1testu jako kroku konfiguracji w celu przetestowania wyjścia, gdy „ Foo()jest w State1”.

Podejście to ma pewne implikacje, o których chcę wspomnieć. Spoiler, tutaj właśnie rozwścieczę purystów

Po pierwsze, musisz zaakceptować fakt, że używasz czegoś jako testu w jednej sytuacji, a konfiguracji w innej sytuacji. Z jednej strony wydaje się, że jest to bezpośrednie naruszenie SAP. Ale jeśli logicznie tworzysz ramy dla ChangeFooToState1dwóch celów, nadal spełniasz duch tego, czego uczy nas SAP. Kiedy musisz upewnić się, że Foo()zmiany są w stanie, użyj go ChangeFooToState1jako testu. A kiedy trzeba zweryfikować Foo()dane wyjściowe State1„ in in ”, używasz ChangeFooToState1jako konfiguracji.

Po drugie, z praktycznego punktu widzenia nie będziesz chciał w pełni losowych testów jednostkowych dla swojego systemu. Przed uruchomieniem testów sprawdzania poprawności danych wyjściowych należy uruchomić wszystkie testy zmiany stanu. SAP jest rodzajem przewodniej zasady tego zamówienia. Aby stwierdzić, co powinno być oczywiste - nie można użyć czegoś jako konfiguracji, jeśli nie powiedzie się to jako test.

Składając to razem:

Za pomocą diagramu stanu wygenerujesz testy obejmujące przejścia. Ponownie, korzystając ze schematu, generujesz testy obejmujące wszystkie przypadki przetwarzania danych wejściowych / wyjściowych sterowane stanem.

Jeśli zastosujesz to podejście, bloated, complicated, long, and difficult to writetesty powinny być nieco łatwiejsze do zarządzania. Zasadniczo powinny one być mniejsze i powinny być bardziej zwięzłe (tj. Mniej skomplikowane). Powinieneś zauważyć, że testy są również bardziej niezależne lub modułowe.

Nie twierdzę, że proces ten będzie całkowicie bezbolesny, ponieważ pisanie dobrych testów wymaga pewnego wysiłku. Niektóre z nich nadal będą trudne, ponieważ mapujesz drugi parametr (stan) na kilka swoich przypadków. Nawiasem mówiąc, powinno być nieco bardziej oczywiste, dlaczego system bezstanowy jest łatwiejszy do zbudowania testów. Ale jeśli dostosujesz to podejście do swojej aplikacji, powinieneś stwierdzić, że jesteś w stanie udowodnić, że aplikacja działa poprawnie.


11

Zazwyczaj wyodrębniasz szczegóły konfiguracji na funkcje, więc nie musisz się powtarzać. W ten sposób musisz zmienić to w jednym miejscu testu, jeśli zmieni się funkcjonalność.

Jednak normalnie nie chciałbyś opisywać nawet funkcji konfiguracji jako nadęty, skomplikowany lub długi. To znak, że twój interfejs wymaga refaktoryzacji, ponieważ jeśli twoje testy są trudne w użyciu, trudno jest również użyć twojego prawdziwego kodu.

Jest to często oznaką zbytniego wkładania w jedną klasę. Jeśli masz wymagania stanowe, potrzebujesz klasy, która zarządza stanem i nic więcej. Klasy, które ją obsługują, powinny być bezstanowe. W naszym przykładzie SIP parsowanie pakietu powinno być całkowicie bezstanowe. Możesz mieć klasę, która analizuje pakiet, a następnie wywołuje coś w rodzaju sipStateController.receiveInvite()zarządzania zmianami stanu, które samo wywołuje inne bezpaństwowe klasy, aby wykonywać takie czynności, jak dzwonienie telefonu.

To sprawia, że ​​konfigurowanie testów jednostkowych dla klasy maszyny stanów jest prostą sprawą kilku wywołań metod. Jeśli Twoja konfiguracja testów jednostek maszyn stanowych wymaga tworzenia pakietów, za dużo włożyłeś w tę klasę. Podobnie, twoja klasa parsera pakietów powinna być względnie łatwa do utworzenia kodu instalacyjnego, przy użyciu makiety dla klasy maszyny stanów.

Innymi słowy, nie można całkowicie uniknąć stanu, ale można go zminimalizować i odizolować.


Dla przypomnienia przykład SIP był mój, a nie OP. Niektóre automaty stanów mogą potrzebować więcej niż kilku wywołań metod, aby wprowadzić je w odpowiedni stan do określonego testu.
Bart van Ingen Schenau

+1 za „nie można całkowicie uniknąć stanu, ale można go zminimalizować i odizolować”. Nie mogłem się zgodzić. Państwo jest złem koniecznym w oprogramowaniu.
Brandon

0

Podstawową ideą TDD jest to, że pisząc najpierw testy, otrzymujesz system, który przynajmniej jest łatwy do przetestowania. Mam nadzieję, że to działa, jest łatwe w utrzymaniu, dobrze udokumentowane i tak dalej, ale jeśli nie, to przynajmniej nadal jest łatwe do przetestowania.

Jeśli więc TDD zakończy się trudnym do przetestowania systemem, coś poszło nie tak. Być może niektóre rzeczy, które są prywatne, powinny być publiczne, ponieważ potrzebujesz ich do testowania. Być może nie pracujesz na odpowiednim poziomie abstrakcji; coś tak prostego jak lista jest stanowe na jednym poziomie, ale wartość na innym. A może przykładasz zbyt dużą wagę do porad, które nie mają zastosowania w twoim kontekście, albo twój problem jest po prostu trudny. A może twój projekt jest po prostu zły.

Bez względu na przyczynę, prawdopodobnie nie zamierzasz wracać i pisać systemu ponownie, aby uczynić go bardziej testowalnym za pomocą prostego kodu testowego. Prawdopodobnie najlepszym planem jest zastosowanie nieco bardziej fantazyjnych technik testowych, takich jak:

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.