Jak przetestować, gdy porządkowanie danych jest zbyt uciążliwe?

19

Piszę parser i jako część tego mam Expanderklasę, która „rozwija” jedną złożoną instrukcję w wiele prostych instrukcji. Na przykład rozwinąłby to:

x = 2 + 3 * a

w:

tmp1 = 3 * a
x = 2 + tmp1

Teraz zastanawiam się, jak przetestować tę klasę, a konkretnie jak zorganizować testy. Mógłbym ręcznie utworzyć wejściowe drzewo składni:

var input = new AssignStatement(
    new Variable("x"),
    new BinaryExpression(
        new Constant(2),
        BinaryOperator.Plus,
        new BinaryExpression(new Constant(3), BinaryOperator.Multiply, new Variable("a"))));

Lub mógłbym napisać go jako ciąg i przeanalizować:

var input = new Parser().ParseStatement("x = 2 + 3 * a");

Druga opcja jest znacznie prostsza, krótsza i czytelna. Ale wprowadza także denpendencję Parser, co oznacza, że ​​błąd w tym Parsermoże zakończyć się niepowodzeniem. Test przestałby być testem jednostkowym Expanderi wydaje mi się, że technicznie staje się testem integracyjnym Parseri Expander.

Moje pytanie brzmi: czy można polegać głównie (lub całkowicie) na tego rodzaju testach integracyjnych w celu przetestowania tej Expanderklasy?

svick
źródło
3
To, że błąd Parsermoże się nie powieść w jakimś innym teście, nie stanowi problemu, jeśli zwykle popełniasz tylko błędy zerowe, przeciwnie, oznacza to, że masz większy zasięg Parser. Wolałbym się martwić, że błąd Parsermoże sprawić, że test się powiedzie, gdy powinien się nie udać . W końcu testy jednostkowe służą do znajdowania błędów - test jest przerywany, kiedy nie, ale powinien.
Jonas Kölker,

Odpowiedzi:

27

Przekonasz się, że piszesz o wiele więcej testów, o wiele bardziej skomplikowanych, interesujących i przydatnych zachowaniach, jeśli możesz to zrobić po prostu. Więc opcja, która obejmuje

var input = new Parser().ParseStatement("x = 2 + 3 * a");

jest całkiem poprawny. To zależy od innego komponentu. Ale wszystko zależy od dziesiątek innych komponentów. Jeśli wyśmiewasz coś w ciągu jednego cala jego życia, prawdopodobnie zależy Ci od wielu drwiących funkcji i urządzeń testowych.

Czasami programiści nadmiernie koncentrują się na czystości swoich testów jednostkowych lub opracowują tylko testy jednostkowe i testy jednostkowe , bez żadnego modułu, integracji, obciążenia lub innych testów. Wszystkie te formularze są ważne i użyteczne, a wszystkie ponoszą właściwą odpowiedzialność za programistów - nie tylko Q / A lub personel operacyjny w dalszej części rurociągu.

Jednym z zastosowanych przeze mnie podejść jest rozpoczęcie od testów wyższego poziomu, a następnie wykorzystanie uzyskanych z nich danych do skonstruowania długiej formy testu o najniższym wspólnym mianowniku. Np. Kiedy zrzucisz strukturę danych z inpututworzonego powyżej, możesz łatwo zbudować:

var input = new AssignStatement(
    new Variable("x"),
    new BinaryExpression(
        new Constant(2),
        BinaryOperator.Plus,
        new BinaryExpression(new Constant(3), BinaryOperator.Multiply, new Variable("a"))));

rodzaj testu, który sprawdza się na najniższym poziomie. W ten sposób otrzymujesz niezłą mieszankę: garść najbardziej podstawowych, prymitywnych testów (czyste testy jednostkowe), ale nie spędziłem tygodnia na pisaniu testów na tym prymitywnym poziomie. To daje ci czas potrzebny na napisanie o wiele więcej, nieco mniej testów atomowych przy pomocy Parserjako pomocnika. Wynik końcowy: więcej testów, większy zasięg, więcej narożników i inne ciekawe przypadki, lepszy kod i wyższa kontrola jakości.

Jonathan Eunice
źródło
2
Jest to rozsądne - szczególnie biorąc pod uwagę fakt, że wszystko zależy od wielu innych. Dobry test jednostkowy powinien przetestować minimum. Wszystko, co mieści się w tej minimalnej możliwej ilości, powinno zostać przetestowane w poprzedzającym teście jednostkowym. Jeśli całkowicie przetestowałeś Parsera, możesz założyć, że możesz bezpiecznie używać Parsera do testowania ParseStatement
Jon Story
6
Główną troską o czystość (tak myślę) jest unikanie pisania okrągłych zależności w testach jednostkowych. Jeśli analizator składni lub testy analizatora składni korzystają z ekspandera, a ten test ekspandera zależy od działającego analizatora składni, istnieje trudne do zarządzania ryzyko, że wszystko, co testujesz, to spójność parsera i ekspandera , podczas gdy chciałeś przetestować, czy ekspander rzeczywiście robi to, co powinien . Ale dopóki nie ma odwrotnej zależności, użycie parsera w tym teście jednostkowym nie różni się niczym od użycia standardowej biblioteki w teście jednostkowym.
Steve Jessop,
@SteveJessop Dobra uwaga. Ważne jest, aby używać niezależnych komponentów.
Jonathan Eunice,
3
Coś, co zrobiłem w przypadkach, gdy sam analizator składni jest kosztowną operacją (np. Odczyt danych z plików Excela za pomocą com interop), polega na napisaniu metod generowania testów, które uruchamiają analizator składni i kod wyjściowy w konsoli, odtwarzają strukturę danych zwracaną przez analizator składni . Następnie kopiuję dane wyjściowe z generatora do bardziej konwencjonalnych testów jednostkowych. Pozwala to zmniejszyć zależność krzyżową, ponieważ analizator składni musi działać poprawnie tylko wtedy, gdy testy były tworzone nie za każdym razem, gdy są uruchamiane. (Nie marnowanie kilku sekund / test na tworzenie / niszczenie procesów Excela było miłym dodatkiem.)
Dan Neely,
+1 za podejście @ DanNeely. Używamy czegoś podobnego do przechowywania kilku serializowanych wersji naszego modelu danych jako danych testowych, dzięki czemu możemy mieć pewność, że nowy kod nadal będzie działał ze starszymi danymi.
Chris Hayes,
6

Oczywiście, że jest OK!

Zawsze potrzebujesz testu funkcjonalnego / integracyjnego, który wykonuje pełną ścieżkę kodu. A pełna ścieżka kodu w tym przypadku oznacza łącznie ocenę wygenerowanego kodu. Oznacza to, że parsowanie x = 2 + 3 * apowoduje wygenerowanie kodu, który po uruchomieniu a = 5ustawi xna, 17a jeśli a = -2zostanie uruchomiony xna, ustawi się na -4.

Poniżej należy wykonać testy jednostkowe dla mniejszych bitów, o ile faktycznie pomaga to w debugowaniu kodu . Im drobniejsze testy będziesz miał, tym większe prawdopodobieństwo, że każda zmiana kodu będzie musiała zmienić test, ponieważ zmienia się interfejs wewnętrzny. Taki test ma niewielką wartość długoterminową i wymaga dodatkowych prac konserwacyjnych. Jest więc punkt malejących zwrotów i powinieneś przestać przed nim.

Jan Hudec
źródło
4

Testy jednostkowe pozwalają ustalić, które elementy się psują i gdzie złamały kod. Są więc dobre do bardzo drobnoziarnistych testów. Dobre testy jednostkowe pomogą skrócić czas debugowania.

Jednak z mojego doświadczenia wynika, że ​​testy jednostkowe rzadko są wystarczająco dobre, aby faktycznie sprawdzić poprawność działania. Dlatego testy integracyjne są również pomocne w celu weryfikacji łańcucha lub sekwencji operacji. Testy integracyjne pomogą ci przejść przez testy funkcjonalne. Jak już wskazałeś, ze względu na złożoność testów integracyjnych trudniej jest znaleźć określone miejsce w kodzie, w którym test się psuje. Ma również nieco większą kruchość, ponieważ awarie w dowolnym miejscu w łańcuchu spowodują niepowodzenie testu. Wciąż będziesz mieć ten łańcuch w kodzie produkcyjnym, więc testowanie rzeczywistego łańcucha jest nadal pomocne.

Idealnie byłoby mieć oba, ale w każdym razie ogólnie automatyczny test jest lepszy niż brak testu.

Peter Smith
źródło
0

Wykonaj wiele testów na parserze, a gdy parser przejdzie testy, zapisz te wyniki w pliku, aby wyśmiewać parser i przetestować inny komponent.

Tulains Córdova
źródło