Testowanie kontra nie powtarzaj się (DRY)

11

Dlaczego tak bardzo zachęca się do pisania testów?

Wygląda na to, że testy w zasadzie wyrażają to samo, co kod, a zatem są duplikatem (w koncepcji, a nie implementacji) kodu. Czy ostatecznym celem DRY nie byłoby wyeliminowanie całego kodu testowego?

John Tseng
źródło

Odpowiedzi:

25

Uważam, że jest to nieporozumienie w każdy możliwy sposób.

Kod testowy testujący kod produkcyjny wcale nie jest podobny. Pokażę w Pythonie:

def multiply(a, b):
    """Multiply ``a`` by ``b``"""
    return a*b

Wtedy prosty test byłby:

def test_multiply():
    assert multiply(4, 5) == 20

Obie funkcje mają podobną definicję, ale obie robią bardzo różne rzeczy. Tutaj nie ma duplikatu kodu. ;-)

Zdarza się również, że ludzie piszą zduplikowane testy, zasadniczo mając jedno stwierdzenie na funkcję testową. To jest szaleństwo i widziałem, jak ludzie to robią. To jest złą praktyką.

def test_multiply_1_and_3():
    """Assert that a multiplication of 1 and 3 is 3."""
    assert multiply(1, 3) == 3

def test_multiply_1_and_7():
    """Assert that a multiplication of 1 and 7 is 7."""
    assert multiply(1, 7) == 7

def test_multiply_3_and_4():
    """Assert that a multiplication of 3 and 4 is 12."""
    assert multiply(3, 4) == 12

Wyobraź sobie, że robisz to dla ponad 1000 efektywnych wierszy kodu. Zamiast tego testujesz według „funkcji”:

def test_multiply_positive():
    """Assert that positive numbers can be multiplied."""
    assert multiply(1, 3) == 3
    assert multiply(1, 7) == 7
    assert multiply(3, 4) == 12

def test_multiply_negative():
    """Assert that negative numbers can be multiplied."""
    assert multiply(1, -3) == -3
    assert multiply(-1, -7) == 7
    assert multiply(-3, 4) == -12

Teraz, gdy funkcje są dodawane / usuwane, muszę jedynie rozważyć dodanie / usunięcie jednej funkcji testowej.

Być może zauważyłeś, że nie zastosowałem forpętli. Jest tak, ponieważ powtarzanie niektórych rzeczy jest dobre. Gdybym zastosował pętle, kod byłby znacznie krótszy. Ale gdy asercja nie powiedzie się, może zaciemnić wyjście wyświetlając niejednoznaczny komunikat. Jeśli to nastąpi wtedy twoje testy będą mniej przydatne i będzie potrzebować debuggera aby sprawdzić, gdzie rzeczy nie udać.

siebz0r
źródło
8
Z technicznego punktu widzenia zalecane jest jedno stwierdzenie na test, ponieważ oznacza to, że wiele problemów nie pojawi się jako jedna awaria. Jednak w praktyce uważam, że staranne agregowanie asercji zmniejsza ilość powtarzanego kodu i prawie nigdy nie trzymam się jednego asersu na wytyczne testowe.
Rob Church
@ pink-diamond-square Widzę, że NUnit nie przestaje testować po niepowodzeniu asercji (co moim zdaniem jest dziwne). W tym konkretnym przypadku rzeczywiście lepiej jest mieć jedno stwierdzenie na test. Jeśli środowisko testów jednostkowych przestaje testować po nieudanym asercji, wiele asercji jest lepszych.
siebz0r
3
NUnit nie zatrzymuje całego zestawu testów, ale ten jeden test się zatrzymuje, chyba że podejmiesz kroki, aby temu zapobiec (możesz złapać wyjątek, który zgłasza, co jest czasami przydatne). Myślę, że chodzi im o to, że jeśli napiszesz testy, które zawierają więcej niż jedno stwierdzenie, nie uzyskasz wszystkich informacji potrzebnych do rozwiązania problemu. Aby przejść przez twój przykład, wyobraź sobie, że ta funkcja mnożenia nie lubi cyfry 3. W tym przypadku assert multiply(1,3)nie powiedzie się, ale nie otrzymasz również raportu z nieudanego testu assert multiply(3,4).
Rob Church
Pomyślałem, że go podniosę, ponieważ jeden test na test to, z tego co przeczytałem w świecie .net, „dobra praktyka”, a wiele twierdzeń to „pragmatyczne użycie”. Trochę inaczej wygląda w dokumentacji Pythona, gdzie przykład def test_shufflewykonuje dwa twierdzenia.
Rob Church
Zgadzam się i nie zgadzam: D Jest tutaj wyraźnie powtórzenie: assert multiply(*, *) == *abyś mógł zdefiniować assert_multiplyfunkcję. W bieżącym scenariuszu nie ma znaczenia liczba wierszy i czytelność, ale przy dłuższych testach można ponownie użyć skomplikowanych asercji, urządzeń, kodu generującego urządzenia itp. Nie wiem, czy jest to najlepsza praktyka, ale zazwyczaj robię to to.
inf3rno
10

Wygląda na to, że testy w zasadzie wyrażają to samo, co kod, a zatem są duplikatami

Nie, to nie jest prawda.

Testy mają inny cel niż implementacja:

  • Testy sprawdzają, czy Twoja implementacja działa.
  • Służą one jako dokumentacja: patrząc na testy, widzisz umowy, które Twój kod musi spełnić, tzn. Które dane wejściowe zwracają dane wyjściowe, jakie są specjalne przypadki itp.
  • Ponadto testy gwarantują, że po dodaniu nowych funkcji istniejąca funkcjonalność nie ulegnie awarii.
Uooo
źródło
4

Nie. DRY polega na pisaniu kodu tylko raz, aby wykonać określone zadanie. Testy sprawdzają poprawność wykonania zadania. Jest to trochę podobne do algorytmu głosowania, w którym oczywiście użycie tego samego kodu byłoby bezużyteczne.

jmoreno
źródło
2

Czy ostatecznym celem DRY nie byłoby wyeliminowanie całego kodu testowego?

Nie, ostateczny cel DRY oznaczałoby wyeliminowanie całego kodu produkcyjnego .

Jeśli nasze testy mogłyby być idealnymi specyfikacjami tego, co system ma zrobić, musielibyśmy automatycznie wygenerować odpowiedni kod produkcyjny (lub pliki binarne), skutecznie usuwając bazę kodu produkcyjnego per se.

To jest właśnie to, co podobno osiąga architektura oparta na modelach - jedno zaprojektowane przez człowieka źródło prawdy, z którego wszystko wynika z obliczeń.

Nie sądzę, aby sytuacja odwrotna (pozbycie się wszystkich testów) była pożądana, ponieważ:

  • Musisz rozwiązać niedopasowanie impedancji między implementacją a specyfikacją. Kod produkcyjny może w pewnym stopniu przekazywać intencje, ale nigdy nie będzie tak łatwe do uzasadnienia na podstawie dobrze wyrażonych testów. My, ludzie, potrzebujemy lepszego spojrzenia na to, dlaczego budujemy różne rzeczy. Nawet jeśli nie przeprowadzasz testów z powodu OSUSZANIA, specyfikacje prawdopodobnie będą musiały zostać zapisane w dokumentach, co jest zdecydowanie bardziej niebezpieczną bestią pod względem niedopasowania impedancji i desynchronizacji kodu, jeśli mnie o to poprosisz.
  • Chociaż kod produkcyjny można łatwo uzyskać na podstawie poprawnych specyfikacji wykonywalnych (przy założeniu wystarczającej ilości czasu), zestaw testów jest znacznie trudniejszy do odtworzenia z końcowego kodu programu. Specyfikacje nie wydają się wyraźnie patrzeć na kod, ponieważ interakcje między jednostkami kodu w czasie wykonywania są trudne do zrozumienia. Właśnie dlatego tak trudno jest nam radzić sobie z bez testowymi starszymi aplikacjami. Innymi słowy: jeśli chcesz, aby aplikacja przetrwała dłużej niż kilka miesięcy, lepiej byłoby, gdybyś stracił dysk twardy, na którym znajduje się twoja produkcyjna baza kodów, niż ten, w którym znajduje się Twój zestaw testowy.
  • O wiele łatwiej jest przypadkowo wprowadzić błąd w kodzie produkcyjnym niż w kodzie testowym. A ponieważ kod produkcyjny nie weryfikuje się samoczynnie (chociaż można się do niego zbliżyć za pomocą systemu Design by Contract lub bogatszych systemów), nadal potrzebujemy zewnętrznego programu do przetestowania go i ostrzeżenia nas w przypadku wystąpienia regresji.
guillaume31
źródło
1

Ponieważ czasami powtarzanie się jest w porządku. Żadnej z tych zasad nie należy traktować w każdych okolicznościach bez pytania lub kontekstu. Czasami pisałem testy przeciwko naiwnej (i powolnej) wersji algorytmu, co jest dość wyraźnym naruszeniem DRY, ale zdecydowanie korzystne.

U2EF1
źródło
1

Ponieważ testy jednostkowe mają na celu utrudnienie niezamierzonych zmian , czasami może to utrudnić również zamierzone zmiany . Fakt ten jest rzeczywiście związany z zasadą SUCHEGO.

Na przykład, jeśli masz funkcję, MyFunctionktóra jest wywoływana w kodzie produkcyjnym tylko w jednym miejscu i piszesz dla niej 20 testów jednostkowych, możesz łatwo mieć 21 miejsc w kodzie, w których ta funkcja jest wywoływana. Teraz, gdy musisz zmienić podpis MyFunction, semantykę lub oba (ponieważ zmieniają się niektóre wymagania), masz 21 miejsc do zmiany zamiast tylko jednego. Powodem jest naruszenie zasady OSUSZANIA: powtórzyłeś (przynajmniej) to samo wywołanie funkcji do MyFunction21 razy.

Prawidłowym podejściem w takim przypadku jest również zastosowanie zasady OSUSZANIA do kodu testowego: podczas pisania 20 testów jednostkowych obuduj wywołania MyFunctionw testach jednostkowych tylko kilkoma funkcjami pomocniczymi (najlepiej tylko jedną), które są używane przez 20 testów jednostkowych. Idealnie byłoby, gdybyś miał tylko dwa miejsca w wywołaniu kodu MyFunction: jedno z kodu produkcyjnego i jedno z testów jednostkowych. Więc kiedy będziesz musiał zmienić podpis MyFunctionpóźniej, będziesz mieć tylko kilka miejsc do zmiany w swoich testach.

„Kilka miejsc” to wciąż więcej niż „jedno miejsce” (to, co otrzymujesz bez testów jednostkowych), ale zalety posiadania testów jednostkowych powinny znacznie przewyższać korzyści wynikające z mniejszej ilości kodu do zmiany (w przeciwnym razie przeprowadzasz pełne testy jednostkowe źle).

Doktor Brown
źródło
0

Jednym z największych wyzwań związanych z budowaniem oprogramowania jest wychwycenie wymagań; to jest odpowiedź na pytanie „co powinno zrobić to oprogramowanie?” Oprogramowanie wymaga dokładnych wymagań, aby dokładnie określić, co powinien zrobić system, ale ci, którzy określają potrzeby systemów oprogramowania i projektów, często obejmują osoby, które nie mają doświadczenia programowego lub formalnego (matematyki). Brak rygorystyczności w definiowaniu wymagań zmusił programistów do znalezienia sposobu na sprawdzenie oprogramowania pod kątem wymagań.

Zespół programistów przełożył potoczny opis projektu na bardziej rygorystyczne wymagania. Dyscyplina testowania połączyła się jako punkt kontrolny dla rozwoju oprogramowania, aby wypełnić lukę między tym, co klient mówi, że chce, a tym, co oprogramowanie rozumie, czego chce. Zarówno twórcy oprogramowania, jak i zespół ds. Jakości / testowania rozumieją (nieformalną) specyfikację i każdy (niezależnie) pisze oprogramowanie lub testy, aby upewnić się, że ich zrozumienie jest zgodne. Dodanie innej osoby w celu zrozumienia (nieprecyzyjnych) wymagań dodało pytania i inną perspektywę w celu dalszego doskonalenia precyzji wymagań.

Ponieważ zawsze istniały testy akceptacyjne, naturalnym było rozszerzenie roli testowania o pisanie testów automatycznych i jednostkowych. Problem polegał na zatrudnianiu programistów do testowania, a tym samym zawęziłeś perspektywę od zapewniania jakości do programistów wykonujących testy.

To powiedziawszy, prawdopodobnie źle wykonujesz testy, jeśli twoje testy niewiele różnią się od rzeczywistych programów. Sugestią Msdy byłoby skupienie się bardziej na tym, co w testach, a mniej na tym, jak.

Ironią jest to, że zamiast przechwycić formalną specyfikację wymagań z potocznego opisu, przemysł postanowił zaimplementować testy punktowe jako kod do automatyzacji testów. Zamiast tworzyć formalne wymagania, które oprogramowanie mogłoby zbudować, aby odpowiedzieć, przyjęto podejście polegające na przetestowaniu kilku punktów, zamiast podejściu do tworzenia oprogramowania przy użyciu formalnej logiki. Jest to kompromis, ale był dość skuteczny i stosunkowo udany.

ChuckCottrill
źródło
0

Jeśli uważasz, że kod testowy jest zbyt podobny do kodu implementacyjnego, może to wskazywać, że nadmiernie używasz fałszywego frameworka. Testy próbne na zbyt niskim poziomie mogą zakończyć się konfiguracją testową przypominającą testowaną metodę. Spróbuj napisać testy wyższego poziomu, które rzadziej ulegną awarii, jeśli zmienisz implementację (wiem, że może to być trudne, ale jeśli możesz to zarządzać, otrzymasz bardziej przydatny zestaw testów).

Jules
źródło
0

Testy jednostkowe nie powinny obejmować powielania testowanego kodu, jak już wspomniano.

Dodałbym jednak, że testy jednostkowe zwykle nie są tak SUCHE jak kod „produkcyjny”, ponieważ konfiguracja zwykle jest podobna (ale nie identyczna) między testami ... zwłaszcza jeśli masz znaczną liczbę zależności, które kpisz / udawanie.
Oczywiście jest możliwe przeformułowanie tego rodzaju rzeczy na wspólną metodę konfiguracji (lub zestaw metod konfiguracji) ... ale odkryłem, że te metody konfiguracji mają zwykle długie listy parametrów i są raczej kruche.

Bądź więc pragmatyczny. Jeśli możesz skonsolidować kod instalacyjny bez uszczerbku dla łatwości konserwacji, zrób to. Ale jeśli alternatywą jest złożony i kruchy zestaw metod konfiguracji, trochę powtórzeń w metodach testowych jest OK.

Lokalny ewangelista TDD / BDD ujmuje to następująco:
„Twój kod produkcyjny powinien być SUCHY. Ale twoje testy mogą być„ wilgotne ”.

David
źródło
0

Wygląda na to, że testy w zasadzie wyrażają to samo, co kod, a zatem są duplikatem (w koncepcji, a nie implementacji) kodu.

To nie jest prawda, testy opisują przypadki użycia, podczas gdy kod opisuje algorytm, który przekazuje przypadki użycia, co jest bardziej ogólne. W TDD zaczynasz od pisania przypadków użycia (prawdopodobnie na podstawie historii użytkownika), a następnie implementujesz kod niezbędny do przekazania tych przypadków użycia. Więc piszesz mały test, małą część kodu, a następnie refaktoryzujesz, jeśli to konieczne, aby pozbyć się powtórzeń. Tak to działa.

Testami mogą być również powtórzenia. Na przykład możesz ponownie użyć urządzeń, kodu generującego urządzenia, skomplikowanych asercji itp. Zazwyczaj robię to, aby uniknąć błędów w testach, ale zwykle zapominam najpierw przetestować, czy test naprawdę się nie powiedzie i może naprawdę zrujnować dzień , kiedy przez pół godziny szukasz błędu w kodzie, a test jest nieprawidłowy ... xD

inf3rno
źródło