Walka z cyklicznymi zależnościami w testach jednostkowych

24

Próbuję ćwiczyć TDD, używając go do opracowania takiego prostego, jak Bit Vector. Zdarza mi się używać Swift, ale jest to pytanie zależne od języka.

My BitVectorto structprzechowuje singiel UInt64i przedstawia nad nim interfejs API, który pozwala traktować go jak kolekcję. Szczegóły nie mają większego znaczenia, ale jest to dość proste. Wysokie 57 bitów to bity pamięci, a dolne 6 bitów to bity „zliczające”, które informują, ile bitów pamięci faktycznie przechowuje zawartą wartość.

Do tej pory mam garść bardzo prostych możliwości:

  1. Inicjator, który konstruuje puste wektory bitowe
  2. countNieruchomość typuInt
  3. isEmptyNieruchomość typuBool
  4. Operator równości ( ==). Uwaga: jest to operator równości wartości podobny do Object.equals()Javy, a nie referencyjny operator równości jak ==w Javie.

Wpadam na kilka cyklicznych zależności:

  1. Test jednostkowy, który testuje mój inicjalizator, musi zweryfikować, czy nowo zbudowany BitVector. Można to zrobić na jeden z 3 sposobów:

    1. Czek bv.count == 0
    2. Czek bv.isEmpty == true
    3. Sprawdź to bv == knownEmptyBitVector

    Metoda 1 polega na count, metoda 2 polega na isEmpty( na czym sama polega count, więc nie ma sensu jej używać), metoda 3 polega na ==. W każdym razie nie mogę przetestować mojego inicjalizatora w izolacji.

  2. Test countna coś musi działać na czymś, co nieuchronnie testuje moje inicjatory

  3. Wdrożenie isEmptypolega nacount

  4. Wdrożenie ==polega na count.

Udało mi się częściowo rozwiązać ten problem, wprowadzając prywatny interfejs API, który konstruuje BitVectorz istniejącego wzorca bitowego (jako a UInt64). Pozwoliło mi to na zainicjowanie wartości bez testowania innych inicjatorów, dzięki czemu mogłem „uruchomić pasek” na swojej drodze.

Aby moje testy jednostkowe były naprawdę testami jednostkowymi, robię mnóstwo włamań, które znacznie komplikują mój kod prod i test.

Jak dokładnie radzisz sobie z tego rodzaju problemami?

Alexander - Przywróć Monikę
źródło
20
Zbyt wąsko patrzysz na termin „jednostka”. BitVectorjest idealnie drobnym rozmiarem jednostki do testów jednostkowych i natychmiast rozwiązuje problemy, których publiczni członkowie BitVectorpotrzebują się nawzajem w celu przeprowadzenia sensownych testów.
Bart van Ingen Schenau
Z góry znasz zbyt wiele szczegółów implementacji. Czy Twój rozwój jest naprawdę oparty na testach ?
herby
@herby Nie, dlatego ćwiczę. Chociaż wydaje się to naprawdę nieosiągalnym standardem. Nie wydaje mi się, żebym kiedykolwiek coś programował bez wyraźnego przybliżenia mentalnego tego, co pociągnie za sobą wdrożenie.
Alexander - Przywróć Monikę
@Alexander Powinieneś spróbować to rozluźnić, w przeciwnym razie będzie to test, ale nie test. Po prostu powiedz niejasne „Zrobię trochę wektor z jednym 64-bitowym int jako sklep z podkładami” i to wszystko; od tego momentu wykonaj TDD czerwono-zielony refaktor jeden po drugim. Szczegóły implementacji, a także API, powinny wynikać z próby uruchomienia testów (pierwsza), a przede wszystkim z napisania tych testów (druga).
herby

Odpowiedzi:

66

Za bardzo martwisz się szczegółami implementacji.

To nie ma znaczenia, że w bieżącej realizacji , isEmptyopiera się na count(lub cokolwiek innego może mieć relacje): wszystko powinno być dbanie o to interfejs publiczny. Na przykład możesz mieć trzy testy:

  • Ten nowo zainicjowany obiekt ma count == 0.
  • Ten nowo zainicjowany obiekt ma isEmpty == true
  • Że nowo zainicjowany obiekt jest równy znanemu pustemu obiektowi.

Są to wszystkie ważne testy, które stają się szczególnie ważne, jeśli kiedykolwiek zdecydujesz się na refaktoryzację wewnętrznych elementów swojej klasy, tak aby isEmptymiała inną implementację, na której nie można polegać count- dopóki wszystkie testy nadal się sprawdzają, wiesz, że nie regresowałeś byle co.

Podobne rzeczy dotyczą innych punktów - pamiętaj o przetestowaniu interfejsu publicznego, a nie wewnętrznej implementacji. TDD może się tu przydać, ponieważ wtedy będziesz pisać testy, których potrzebujesz, isEmptyzanim napiszesz jakąkolwiek implementację dla niego.

Philip Kendall
źródło
6
@Alexander Brzmisz jak człowiek potrzebujący jasnej definicji testów jednostkowych. Najlepszy, jaki znam, pochodzi od Michaela Feathersa
candied_orange
14
@Alexander traktujesz każdą metodę jako niezależnie testowalny fragment kodu. To jest źródło twoich trudności. Te trudności znikają, jeśli przetestujesz obiekt jako całość, bez próby podzielenia go na mniejsze części. Zależności między obiektami nie są porównywalne z zależnościami między metodami.
amon
9
@Alexander „kawałek kodu” jest arbitralnym pomiarem. Po zainicjowaniu zmiennej używasz wielu „fragmentów kodu”. Liczy się to, że testujesz spójną jednostkę behawioralną zdefiniowaną przez ciebie .
Ant P
9
„Z tego, co przeczytałem, mam wrażenie, że jeśli złamiesz tylko fragment kodu, tylko testy jednostkowe bezpośrednio związane z tym kodem powinny zakończyć się niepowodzeniem”. To wydaje się być bardzo trudną zasadą. (np. jeśli napiszesz klasę wektorową i popełnisz błąd w metodzie indeksowej, prawdopodobnie będziesz mieć mnóstwo przerw w całym kodzie, który używa tej klasy wektorowej)
jhominal
4
@Alexander Sprawdź także wzorzec „Ułóż, działaj, potwierdzaj” w celu przeprowadzenia testów. Zasadniczo ustawiasz obiekt w jakimkolwiek stanie, w jakim powinien być (Rozmieść), wywołujesz metodę, którą faktycznie testujesz (Działaj), a następnie weryfikujesz, czy jego stan zmienił się zgodnie z Twoimi oczekiwaniami. (Zapewniać). Rzeczy ustawione w aranżacji byłyby „warunkiem wstępnym” testu.
GalacticCowboy
5

Jak dokładnie radzisz sobie z tego rodzaju problemami?

Dokonujesz przeglądu swojego myślenia na temat „testu jednostkowego”.

Obiekt zarządzający zmiennymi danymi w pamięci jest zasadniczo maszyną stanu. Tak więc każdy cenny przypadek użycia będzie co najmniej wywoływał metodę umieszczania informacji w obiekcie i wywoływał metodę odczytu kopii informacji z obiektu. W interesujących przypadkach będziesz również przywoływać dodatkowe metody zmieniające strukturę danych.

W praktyce często tak wygląda

// GIVEN
obj = new Object(...)

// THEN
assert object.read(...)

lub

// GIVEN
obj = new Object(...)

// WHEN
object.change(...)

// THEN
assert object.read(...)

Terminologia „test jednostkowy” - cóż, ma długą historię niezbyt dobrą.

Nazywam je testami jednostkowymi, ale nie pasują one do przyjętej definicji testów jednostkowych - Kent Beck, Test Driven Development przez przykład

Kent napisał pierwszą wersję SUnit w 1994 roku , port do JUnit był w 1998 roku, pierwszy szkic książki TDD był na początku 2002 roku. Zamieszanie miało dużo czasu na rozprzestrzenienie się.

Kluczową ideą tych testów (dokładniej nazywanych „testami programistycznymi” lub „testami programistycznymi”) jest to, że testy są od siebie odizolowane. Testy nie współużytkują żadnych modyfikowalnych struktur danych, więc można je uruchamiać jednocześnie. Nie ma obaw, że testy muszą być przeprowadzane w określonej kolejności, aby poprawnie zmierzyć rozwiązanie.

Podstawowym przypadkiem użycia tych testów jest to, że są one uruchamiane przez programistę między edycjami własnego kodu źródłowego. Jeśli wykonujesz czerwony zielony protokół refaktora, nieoczekiwany CZERWONY zawsze oznacza błąd w ostatniej edycji; cofniesz tę zmianę, sprawdź, czy testy są ZIELONE, i spróbuj ponownie. Próba zainwestowania w projekt, w którym każdy możliwy błąd jest wykrywany tylko przez jeden test, nie ma dużej przewagi.

Oczywiście, jeśli scalenie wprowadza błąd, to stwierdzenie, że błąd nie jest już trywialny. Istnieją różne kroki, które można podjąć, aby zapewnić łatwą lokalizację usterek. Widzieć

VoiceOfUnreason
źródło
1

Ogólnie (nawet jeśli nie używasz TDD) powinieneś starać się pisać testy w jak największym stopniu, udając, że nie wiesz, jak to jest realizowane.

Jeśli faktycznie robisz TDD, powinno już tak być. Twoje testy są wykonywalną specyfikacją programu.

Wygląd wykresu połączeń pod testami nie ma znaczenia, o ile same testy są rozsądne i dobrze utrzymane.

Myślę, że twoim problemem jest twoje zrozumienie TDD.

Moim zdaniem twoim problemem jest to, że „miksujesz” swoje osobowości TDD. Twoje „test”, „kod” i „refaktoryzator” działają całkowicie niezależnie od siebie, najlepiej. W szczególności twoje osoby kodujące i refaktoryzujące nie mają żadnych zobowiązań do testów poza tym, aby zapewnić / utrzymać ich ekologiczność.

Oczywiście, w zasadzie najlepiej byłoby, gdyby wszystkie testy były ortogonalne i niezależne od siebie. Ale to nie dotyczy twoich dwóch osób TDD i zdecydowanie nie jest to ścisły, a nawet niekoniecznie realistyczny, trudny wymóg twoich testów. Zasadniczo: Nie wyrzucaj zdrowego rozsądku na temat jakości kodu, aby spróbować spełnić wymóg, o którym nikt cię nie prosi.

Tim Seguine
źródło