Jak uniknąć delikatnych testów jednostkowych?

24

Napisaliśmy prawie 3000 testów - dane zostały zakodowane na stałe, bardzo mało ponownego użycia kodu. Ta metodologia zaczęła nas gryźć w tyłek. Wraz ze zmianami systemu spędzamy więcej czasu na naprawianiu uszkodzonych testów. Posiadamy testy jednostkowe, integracyjne i funkcjonalne.

To, czego szukam, to ostateczny sposób na napisanie łatwych do zarządzania i łatwych do utrzymania testów.

Ramy

Chuck Conway
źródło
Jest to znacznie lepiej dostosowane do Programmers.StackExchange, IMO ...
IAbstract
BDD
Robbie Dee,

Odpowiedzi:

21

Nie myśl o nich jako o „zepsutych testach jednostkowych”, ponieważ tak nie jest.

Są to specyfikacje, których Twój program nie obsługuje.

Nie myśl o tym jako o „naprawianiu testów”, ale o „definiowaniu nowych wymagań”.

Testy powinny najpierw określić twoją aplikację, a nie na odwrót.

Nie możesz powiedzieć, że masz działającą implementację, dopóki nie dowiesz się, że działa. Nie możesz powiedzieć, że działa, dopóki go nie przetestujesz.

Kilka innych uwag, które mogą Cię poprowadzić:

  1. Testy i testowane klasy powinny być krótkie i proste . Każdy test powinien sprawdzać tylko spójny element funkcjonalności. Oznacza to, że nie obchodzi go to, co inne testy już sprawdzają.
  2. Testy i twoje obiekty powinny być luźno powiązane, w taki sposób, że jeśli zmienisz obiekt, zmienisz tylko jego wykres zależności w dół, a inne obiekty, które używają tego obiektu, nie zostaną przez niego zmienione.
  3. Być może tworzysz i testujesz niewłaściwe rzeczy . Czy Twoje obiekty są zbudowane z myślą o łatwym interfejsie lub łatwej implementacji? W drugim przypadku zmienisz dużo kodu korzystającego z interfejsu starej implementacji.
  4. W najlepszym przypadku ściśle przestrzegaj zasady pojedynczej odpowiedzialności. W najgorszym przypadku należy przestrzegać zasady segregacji interfejsów. Zobacz SOLIDNE zasady .
Yam Marcovic
źródło
5
+1 zaDon't think of it as "fixing the tests", but as "defining new requirements".
StuperUser
2
+1 Testy powinny najpierw określić twoją aplikację, a nie odwrotnie
treekoder
11

To, co opisujesz, może nie być wcale takie złe, ale wskaźnikiem głębszych problemów odkrytych przez twoje testy

Wraz ze zmianami systemu spędzamy więcej czasu na naprawianiu uszkodzonych testów. Posiadamy testy jednostkowe, integracyjne i funkcjonalne.

Gdybyś mógł zmienić kod, a testy by się nie zepsuły, byłoby to dla mnie podejrzane. Różnica między uzasadnioną zmianą a błędem polega tylko na tym, że jest o to poproszony, a to, o co jest proszone, jest (zakładane TDD) zdefiniowane przez twoje testy.

dane zostały zakodowane na stałe.

Dobrze zakodowane dane w testach to dobra rzecz. Testy działają jak fałszerstwa, a nie dowody. Jeśli jest zbyt wiele obliczeń, twoje testy mogą być tautologiami. Na przykład:

assert sum([1,2,3]) == 6
assert sum([1,2,3]) == 1 + 2 + 3
assert sum([1,2,3]) == reduce(operator.add, [1,2,3])

Im wyższa abstrakcja, tym bardziej zbliżasz się do algorytmu, a tym samym bliżej porównywania implementacji akutalnej z samym sobą.

bardzo małe ponowne użycie kodu

Najlepsze ponowne użycie kodu w testach to imho „Checks”, podobnie jak w jUnits assertThat, ponieważ zapewniają prostotę testów. Poza tym, jeśli testy można przefaktoryzować w celu współużytkowania kodu, prawdopodobnie może to być również testowany rzeczywisty kod , redukując w ten sposób testy do testów testujących przebudowaną bazę.

keppla
źródło
Chciałbym wiedzieć, gdzie downvoter się nie zgadza.
keppla,
keppla - Nie jestem downvoter, ale ogólnie, w zależności od tego, gdzie jestem w modelu, wolę testowanie interakcji obiektu niż testowanie danych na poziomie jednostki. Testowanie danych działa lepiej na poziomie integracji.
Ritch Melton,
@keppla Mam klasę, która kieruje zamówienie do innego kanału, jeśli jego łączna liczba pozycji zawiera pewne zastrzeżone elementy. Tworzę fałszywe zamówienie, wypełniając je 4 przedmiotami, z których dwa są ograniczone. W zakresie, w jakim dodawane są elementy zastrzeżone, ten test jest unikalny. Ale kroki tworzenia fałszywego zamówienia i dodawania dwóch zwykłych elementów to ta sama konfiguracja, której używa inny test, który testuje przepływ pracy elementów bez ograniczeń. W tym przypadku wraz z pozycjami, jeśli zamówienie wymaga konfiguracji danych klienta i adresu itp., Nie jest to dobry przypadek ponownego użycia pomocników konfiguracji. Dlaczego tylko żądać ponownego użycia?
Asif Shiraz
6

Też miałem ten problem. Moje ulepszone podejście wygląda następująco:

  1. Nie pisz testów jednostkowych, chyba że to jedyny dobry sposób na przetestowanie czegoś.

    Jestem w pełni przygotowany do przyznania, że ​​testy jednostkowe mają najniższy koszt diagnozy i czasu do naprawy. To czyni je cennym narzędziem. Problem polega na tym, że przy oczywistym przebiegu mogą się różnić, testy jednostkowe są często zbyt małe, aby uzasadnić koszty utrzymania masy kodu. Na dole napisałem przykład, spójrz.

  2. Zastosuj twierdzenia, ilekroć są one równoważne z testem jednostkowym dla tego komponentu. Asercje mają tę fajną właściwość, że zawsze są weryfikowane podczas każdej kompilacji debugowania. Dlatego zamiast testować ograniczenia klasy „Pracownik” w osobnej jednostce testów, skutecznie testujesz klasę Pracownik w każdym przypadku testowym w systemie. Asercje mają również tę fajną właściwość, że nie zwiększają masy kodu tak bardzo, jak testy jednostkowe (które ostatecznie wymagają rusztowania / drwiny / cokolwiek innego).

    Zanim ktoś mnie zabije: kompilacje produkcyjne nie powinny ulec awarii na podstawie twierdzeń. Zamiast tego powinni zalogować się na poziomie „Błąd”.

    Uwaga dla kogoś, kto jeszcze o tym nie pomyślał, nie należy niczego potwierdzać na temat danych wejściowych użytkownika lub sieci. To ogromny błąd ™.

    W moich najnowszych bazach kodu rozsądnie usuwam testy jednostkowe wszędzie tam, gdzie widzę oczywistą okazję do asercji. To znacznie obniżyło ogólne koszty utrzymania i uczyniło mnie znacznie szczęśliwszą osobą.

  3. Preferuj testy systemowe / integracyjne, wdrażając je dla wszystkich swoich podstawowych przepływów i doświadczeń użytkownika. Narożniki prawdopodobnie nie muszą tu być. Test systemowy weryfikuje zachowanie na poziomie użytkownika, uruchamiając wszystkie komponenty. Z tego powodu test systemu jest koniecznie wolniejszy, więc napisz te, które mają znaczenie (nie więcej, nie mniej), a złapiesz najważniejsze problemy. Testy systemu mają bardzo niskie koszty utrzymania.

    Należy pamiętać, że ponieważ używasz twierdzeń, każdy test systemu przeprowadzi kilkaset „testów jednostkowych” w tym samym czasie. Masz również pewność, że najważniejsze zostaną uruchomione wiele razy.

  4. Napisz silne interfejsy API, które można przetestować funkcjonalnie. Testy funkcjonalne są niewygodne i (spójrzmy prawdzie w oczy) w pewnym sensie bez znaczenia, jeśli interfejs API utrudnia samodzielną weryfikację działających komponentów. Dobry projekt API a) upraszcza etapy testowania i b) daje jasne i wartościowe stwierdzenia.

    Testowanie funkcjonalne jest najtrudniejsze do zrobienia, zwłaszcza gdy masz komponenty komunikujące się jeden do wielu lub (jeszcze gorzej, o Boże) wiele do wielu przez bariery procesowe. Im więcej wejść i wyjść podłączonych do jednego komponentu, tym trudniejsze jest testowanie funkcjonalne, ponieważ musisz odizolować jeden z nich, aby naprawdę przetestować jego funkcjonalność.


W kwestii „nie pisz testów jednostkowych” przedstawię przykład:

TEST(exception_thrown_on_null)
{
    InternalDataStructureType sink;
    ASSERT_THROWS(sink.consumeFrom(NULL), std::logic_error);
    try {
        sink.consumeFrom(NULL);
    } catch (const std::logic_error& e) {
        ASSERT(e.what() == "You must not pass NULL as a parameter!");
    }
}

Autor tego testu dodał siedem wierszy, które wcale nie przyczyniają się do weryfikacji produktu końcowego. Użytkownik nigdy nie powinien tego widzieć, ponieważ: a) nikt nigdy nie powinien przekazywać NULL (więc napisz więc twierdzenie), lub b) przypadek NULL powinien powodować inne zachowanie. Jeśli przypadek to (b), napisz test, który faktycznie weryfikuje to zachowanie.

Moja filozofia stała się taka, że ​​nie powinniśmy testować artefaktów implementacyjnych. Powinniśmy testować tylko wszystko, co można uznać za rzeczywistą wydajność. W przeciwnym razie nie ma możliwości uniknięcia dwukrotnego zapisania podstawowej masy kodu między testami jednostkowymi (które wymuszają określoną implementację) a samą implementacją.

Należy tutaj zauważyć, że są dobrzy kandydaci do testów jednostkowych. W rzeczywistości istnieje nawet kilka sytuacji, w których test jednostkowy jest jedynym odpowiednim środkiem do weryfikacji czegoś, w którym bardzo ważne jest napisanie i utrzymanie tych testów. Na szczycie mojej listy ta lista zawiera nietrywialne algorytmy, odsłonięte pojemniki danych w interfejsie API oraz wysoce zoptymalizowany kod, który wydaje się „skomplikowany” (inaczej „następny facet prawdopodobnie to spieprzy”.).

Moja konkretna rada dla ciebie: zacznij ostrożnie usuwać testy jednostkowe, gdy się psują, zadając sobie pytanie: „czy to wynik, czy marnuję kod?” Prawdopodobnie uda ci się zmniejszyć liczbę rzeczy, które marnują Twój czas.

Andres Jaan Tack
źródło
3
Preferuj testy systemowe / integracyjne - jest to zdumiewająco złe. Twój system dochodzi do punktu, w którym używa tych (powolnych!) Testów do testowania rzeczy, które można szybko złapać na poziomie jednostki, a ich uruchomienie zajmuje wiele godzin, ponieważ masz tyle podobnych i wolnych testów.
Ritch Melton,
1
@RitchMelton Całkowicie oddzielony od dyskusji, wygląda na to, że potrzebujesz nowego serwera CI. CI nie powinien się tak zachowywać.
Andres Jaan Tack
1
Awaria programu (co właśnie robią asercje) nie powinna zabijać twojego testera (CI). Właśnie dlatego masz testera; więc coś może wykryć i zgłosić takie awarie.
Andres Jaan Tack
1
Asercje w stylu debugowania, które znam (a nie testowe), wyświetlają okno dialogowe, które zawiesza CI, ponieważ oczekuje na interakcję programisty.
Ritch Melton,
1
Ach, cóż, to by wyjaśniało wiele o naszym braku porozumienia. :) Mam na myśli twierdzenia w stylu C. Dopiero teraz zauważyłem, że jest to pytanie dotyczące platformy .NET. cplusplus.com/reference/clibrary/cassert/assert
Andres Jaan Tack
5

Wydaje mi się, że twoje testy jednostkowe działają jak urok. Jest to dobra rzecz, że to jest tak kruche do zmian, ponieważ jest to rodzaj całego punktu. Małe zmiany w testach łamania kodu, aby wyeliminować możliwość wystąpienia błędu w całym programie.

Należy jednak pamiętać, że naprawdę trzeba tylko testować warunki, które spowodowałyby niepowodzenie metody lub nieoczekiwane wyniki. To sprawiłoby, że twoje testy jednostkowe byłyby bardziej podatne na „zerwanie”, jeśli istnieje prawdziwy problem, niż trywialne rzeczy.

Chociaż wydaje mi się, że mocno przeprojektowujesz program. W takich przypadkach zrób wszystko, czego potrzebujesz, i usuń stare testy, a następnie zastąp je nowymi. Naprawianie testów jednostkowych jest opłacalne tylko wtedy, gdy nie naprawiasz ich z powodu radykalnych zmian w twoim programie. W przeciwnym razie może się okazać, że poświęcasz zbyt dużo czasu na przepisywanie testów, aby można je było zastosować w nowo napisanej sekcji kodu programu.

Neil
źródło
3

Jestem pewien, że inni będą mieli znacznie większy wkład, ale z mojego doświadczenia wynika, że ​​są to niektóre ważne rzeczy, które pomogą ci:

  1. Użyj fabryki obiektów testowych do zbudowania struktur danych wejściowych, więc nie musisz powielać tej logiki. Być może zajrzyj do biblioteki pomocniczej, takiej jak AutoFixture, aby zmniejszyć kod potrzebny do konfiguracji testu.
  2. Dla każdej klasy testowej scentralizuj tworzenie SUT, aby łatwo było go zmienić, gdy wszystko zostanie zrefaktoryzowane.
  3. Pamiętaj, że ten kod testowy jest tak samo ważny jak kod produkcyjny. Należy go również refaktoryzować, jeśli okaże się, że się powtarzasz, jeśli kod wydaje się niemożliwy do utrzymania itp. Itp.
driis
źródło
Im częściej używasz kodu w testach, tym bardziej stają się one delikatne, ponieważ teraz zmiana jednego testu może uszkodzić inny. Może to być rozsądny koszt w zamian za łatwość konserwacji - nie wchodzę tutaj w ten argument - ale twierdzenie, że punkty 1 i 2 sprawiają, że testy są mniej kruche (co było pytaniem), jest po prostu błędne.
pdr
@driis - Racja, kod testowy ma inne idiomy niż kod uruchamiający. Ukrywanie rzeczy przez refaktoryzowanie „wspólnego” kodu i używanie rzeczy takich jak kontenery IoC po prostu maskuje problemy projektowe ujawniane przez twoje testy.
Ritch Melton
Chociaż punkt @pdr jest prawdopodobnie ważny dla testów jednostkowych, argumentowałbym, że w przypadku testów integracyjnych / systemowych przydatne może być myślenie w kategoriach „przygotowania aplikacji do zadania X”. Może to obejmować nawigację do odpowiedniego miejsca, ustawienie określonych ustawień czasu wykonywania, otwarcie pliku danych i tak dalej. Jeśli wiele testów integracji rozpocznie się w tym samym miejscu, refaktoryzacja tego kodu w celu ponownego użycia go w wielu testach może nie być złą rzeczą, jeśli rozumiesz ryzyko i ograniczenia takiego podejścia.
CVn
2

Obsługuj testy tak jak robisz to z kodem źródłowym.

Kontrola wersji, wydania punktów kontrolnych, śledzenie problemów, „własność funkcji”, planowanie i szacowanie wysiłków itp. Tam już to zrobiono - myślę, że jest to najbardziej skuteczny sposób radzenia sobie z opisanymi przez ciebie problemami.

komar
źródło
1

Zdecydowanie powinieneś rzucić okiem na wzorce testowe XUnit Gerarda Meszarosa . Ma świetną sekcję z wieloma przepisami na ponowne użycie kodu testowego i uniknięcie powielania.

Jeśli twoje testy są kruche, może to oznaczać, że nie uciekasz się wystarczająco do testowania podwójnych. W szczególności, jeśli odtwarzasz całe wykresy obiektów na początku każdego testu jednostkowego, sekcje Rozmieść w testach mogą być zbyt duże i często możesz znaleźć się w sytuacjach, w których musisz przepisać sekcje Rozmieść w znacznej liczbie testów tylko dlatego, że jedna z najczęściej używanych klas uległa zmianie. Próbki i odcinki mogą ci w tym pomóc, ograniczając liczbę obiektów, które musisz nawodnić, aby uzyskać odpowiedni kontekst testowy.

Usunięcie nieistotnych szczegółów z ustawień testowych za pomocą próbnych i kodów pośredniczących i zastosowanie wzorców testowych do ponownego użycia kodu powinno znacznie zmniejszyć ich kruchość.

guillaume31
źródło