Czy magiczne liczby są akceptowane w testach jednostkowych, jeśli liczby nic nie znaczą?


59

W moich testach jednostkowych często rzucam dowolne wartości na mój kod, aby zobaczyć, co on robi. Na przykład, jeśli wiem, że foo(1, 2, 3)ma to zwrócić 17, mógłbym napisać:

assertEqual(foo(1, 2, 3), 17)

Liczby te są czysto arbitralne i nie mają szerszego znaczenia (nie są na przykład warunkami brzegowymi, chociaż testuję je również). Trudno mi wymyślić dobre nazwiska dla tych liczb, a pisanie czegoś takiego const int TWO = 2;jest oczywiście bezużyteczne. Czy pisanie takich testów jest w porządku, czy powinienem rozłożyć liczby na stałe?

W Czy wszystkie magiczne liczby są takie same? , dowiedzieliśmy się, że liczby magiczne są OK, jeśli znaczenie jest oczywiste z kontekstu, ale w tym przypadku liczby faktycznie nie mają żadnego znaczenia.


9
Jeśli dodajesz wartości i spodziewasz się, że będziesz w stanie odczytać te same wartości z powrotem, powiedziałbym, że magiczne liczby są w porządku. Jeśli więc powiedzmy, że 1, 2, 3są indeksami tablic 3D, w których wcześniej zapisałeś wartość 17, to myślę, że ten test byłby dandysowy (pod warunkiem, że masz również kilka testów negatywnych). Ale jeśli jest to wynik obliczeń, upewnij się, że każdy, kto przeczyta ten test, zrozumie, dlaczego foo(1, 2, 3)tak jest 17, a magiczne liczby prawdopodobnie nie osiągną tego celu.
Joe White

24
const int TWO = 2;jest nawet gorszy niż zwykłe używanie 2. Jest zgodny z brzmieniem reguły z zamiarem naruszenia jej ducha.
Agent_L,

4
Co to jest liczba, która „nic nie znaczy”? Dlaczego miałby być w twoim kodzie, gdyby to nic nie znaczyło?
Tim Grant,

6
Pewnie. Zostaw komentarz przed serią takich testów, np. „Mały wybór ręcznie ustalonych przykładów”. To, w odniesieniu do innych testów, które wyraźnie testują granice i wyjątki, będzie jasne.
davidbak

5
Twój przykład jest mylący - gdy nazwa twojej funkcji byłaby naprawdę foo, to nic by to nie znaczyło, a więc parametry. Ale w rzeczywistości, jestem całkiem pewny, że funkcja nie posiada tę nazwę, a parametry nie mają nazwy bar1, bar2i bar3. Zrób bardziej realistyczny przykład, w którym nazwy mają znaczenie, wtedy sensowniej jest omówić, czy wartości danych testowych również wymagają nazwy.
Doc Brown

Odpowiedzi:


81

Kiedy naprawdę masz liczby, które nie mają żadnego znaczenia?

Zwykle, gdy liczby mają jakiekolwiek znaczenie, należy przypisać je do zmiennych lokalnych metody testowej, aby kod był bardziej czytelny i zrozumiały. Nazwy zmiennych powinny przynajmniej odzwierciedlać znaczenie zmiennej, niekoniecznie jej wartość.

Przykład:

const int startBalance = 10000;
const float interestRate = 0.05f;
const int years = 5;

const int expectedEndBalance = 12840;

assertEqual(calculateCompoundInterest(startBalance, interestRate, years),
            expectedEndBalance);

Zauważ, że pierwsza zmienna nie jest nazwana HUNDRED_DOLLARS_ZERO_CENT, ale w startBalancecelu określenia jej znaczenia, ale nie to, że jej wartość jest w jakikolwiek sposób wyjątkowa.


3
@Kevin - w jakim języku testujesz? Niektóre ramy testowe pozwalają skonfigurować dostawców danych, którzy zwracają tablicę tablic wartości do testowania
HorusKol

10
Choć zgadzam się z ideą, strzeżcie się, że taka praktyka może wprowadzić nowe błędy też, jak gdybyś przypadkowo wyodrębnić wartość podobnego 0.05fdo int. :)
Jeff Bowman,

5
+1 - świetne rzeczy. To, że nie obchodzi Cię konkretna wartość, nie oznacza, że ​​wciąż nie jest to magiczna liczba ...
Robbie Dee,

2
@PieterB: AFAIK to wina C i C ++, które sformalizowały pojęcie constzmiennej.
Steve Jessop,

2
Czy nazwałeś swoje zmienne tak samo jak nazwane parametry calculateCompoundInterest? Jeśli tak, to dodatkowe pisanie jest dowodem pracy, że przeczytałeś dokumentację testowanej funkcji lub przynajmniej skopiowałeś nazwy nadane przez twoje IDE. Nie jestem pewien, ile to mówi czytelnikowi o zamiarze kodu, ale jeśli przekażesz parametry w niewłaściwej kolejności, przynajmniej będą mogli powiedzieć, co było zamierzone.
Steve Jessop,

20

Jeśli używasz dowolnych liczb, aby zobaczyć, co robią, to tak naprawdę szukasz prawdopodobnie losowo wygenerowanych danych testowych lub testów opartych na właściwościach.

Na przykład Hipoteza jest fajną biblioteką Pythona do tego rodzaju testów i jest oparta na QuickCheck .

Pomyśl o normalnym teście jednostkowym jako o czymś takim:

  1. Skonfiguruj niektóre dane.
  2. Wykonaj niektóre operacje na danych.
  3. Potwierdź coś o wyniku.

Hipoteza pozwala pisać testy, które zamiast tego wyglądają tak:

  1. Dla wszystkich danych pasujących do niektórych specyfikacji.
  2. Wykonaj niektóre operacje na danych.
  3. Potwierdź coś o wyniku.

Chodzi o to, aby nie ograniczać się do własnych wartości, ale wybrać losowe, które można wykorzystać do sprawdzenia, czy funkcje spełniają ich specyfikacje. Co ważne, systemy te na ogół zapamiętują wszelkie dane wejściowe, które ulegną awarii, a następnie zapewniają, że dane wejściowe będą zawsze testowane w przyszłości.

Punkt 3 może być mylący dla niektórych osób, więc wyjaśnijmy. Nie oznacza to, że podajesz dokładną odpowiedź - jest to oczywiście niemożliwe w przypadku arbitralnych danych wejściowych. Zamiast tego twierdzisz coś o właściwości wyniku. Na przykład możesz twierdzić, że po dodaniu czegoś do listy staje się ono puste lub że samo-równoważące się drzewo wyszukiwania binarnego jest faktycznie zrównoważone (przy użyciu kryteriów określonych przez daną strukturę danych).

Ogólnie rzecz biorąc, samodzielne wybieranie dowolnych liczb jest prawdopodobnie dość złe - nie dodaje żadnej wartości i jest mylące dla każdego, kto je czyta. Automagiczne generowanie wiązki losowych danych testowych i skuteczne ich wykorzystanie jest dobre. Znalezienie hipotezy lub biblioteki podobnej do QuickCheck dla wybranego języka jest prawdopodobnie lepszym sposobem na osiągnięcie celów, pozostając zrozumiałym dla innych.


11
Losowe testowanie może wykryć błędy, które są trudne do odtworzenia, ale losowe testy z trudem wykrywają powtarzalne błędy. Pamiętaj, aby uchwycić wszelkie niepowodzenia testu za pomocą konkretnego odtwarzalnego przypadku testowego.
JBRWilkinson,

5
A skąd wiesz, że Twój test jednostkowy nie jest błędny, gdy „twierdzisz coś o wyniku” (w tym przypadku przelicz to, co foojest obliczeniowe) ...? Gdybyś był w 100% pewien, że twój kod daje prawidłową odpowiedź, to po prostu umieściłbyś ten kod w programie i nie testowałeś go. Jeśli nie, musisz przetestować test i myślę, że wszyscy widzą, dokąd to zmierza.

2
Tak, jeśli przekazujesz losowe dane wejściowe do funkcji, musisz wiedzieć, jakie dane wyjściowe byłyby w stanie stwierdzić, że działa ona poprawnie. Przy ustalonych / wybranych wartościach testowych można oczywiście to wypracować ręcznie itp., Ale na pewno każda zautomatyzowana metoda ustalenia, czy wynik jest poprawny, wiąże się z dokładnie tymi samymi problemami, co testowana funkcja. Albo używasz implementacji, którą posiadasz (której nie możesz, ponieważ testujesz, czy to działa), albo piszesz nową implementację, która równie dobrze może być wadliwa (lub bardziej, że wolałbyś użyć poprawnej) ).
Chris

7
@NajibIdrissi - niekoniecznie. Możesz na przykład przetestować, że zastosowanie odwrotności testowanej operacji do wyniku zwraca wartość początkową, którą zacząłeś. Lub możesz przetestować oczekiwane niezmienniki (np. Dla wszystkich obliczeń odsetek w ddniach, obliczenie w ddniach + 1 miesiąc powinno być znaną wyższą miesięczną stopą procentową) itp.
Jules

12
@Chris - W wielu przypadkach sprawdzanie poprawności wyników jest łatwiejsze niż generowanie wyników. Chociaż nie jest to prawdą we wszystkich okolicznościach, jest wiele takich miejsc. Przykład: dodanie wpisu do zrównoważonego drzewa binarnego powinno dać nowe drzewo, które jest również zrównoważone ... łatwe do przetestowania, dość trudne do wdrożenia w praktyce.
Jules

11

Twoja nazwa testu jednostkowego powinna zapewniać większość kontekstu. Nie z wartości stałych. Nazwa / dokumentacja testu powinna zawierać odpowiedni kontekst i wyjaśnienie wszelkich magicznych liczb obecnych w teście.

Jeśli to nie wystarczy, powinna być w stanie dostarczyć odrobinę dokumentacji (czy to poprzez nazwę zmiennej, czy dokumentację). Należy pamiętać, że sama funkcja ma parametry, które, mam nadzieję, mają sensowne nazwy. Kopiowanie ich do testu w celu nazwania argumentów jest raczej bezcelowe.

I na koniec, jeśli twoje najbardziej nieprzystosowane są na tyle skomplikowane, że jest to trudne / niepraktyczne, prawdopodobnie masz zbyt skomplikowane funkcje i możesz rozważyć, dlaczego tak jest.

Im bardziej niechętnie piszesz testy, tym gorszy będzie twój rzeczywisty kod. Jeśli czujesz potrzebę nazwania wartości testowych, aby test był przejrzysty, zdecydowanie sugeruje to, że twoja rzeczywista metoda wymaga lepszego nazewnictwa i / lub dokumentacji. Jeśli zauważysz potrzebę nazywania stałych w testach, zastanowię się, dlaczego jest to potrzebne - prawdopodobnie problemem nie jest sam test, ale implementacja


Ta odpowiedź wydaje się dotyczyć trudności w wywnioskowaniu celu testu, podczas gdy faktyczne pytanie dotyczy liczb magicznych w parametrach metody ...
Robbie Dee,

@RobbieDee nazwa / dokumentacja testu powinna zawierać odpowiedni kontekst i wyjaśnienie wszelkich magicznych liczb obecnych w teście. Jeśli nie, dodaj dokumentację lub zmień nazwę testu, aby był bardziej przejrzysty.
enderland

Nadal lepiej byłoby podać nazwy magicznych liczb. W przypadku zmiany liczby parametrów istnieje ryzyko, że dokumentacja stanie się nieaktualna.
Robbie Dee,

1
@RobbieDee pamiętaj, że sama funkcja ma parametry, które, mam nadzieję, mają sensowne nazwy. Kopiowanie ich do testu w celu nazwania argumentów jest raczej bezcelowe.
enderland

„Mam nadzieję”, co? Dlaczego po prostu nie zakodować tego poprawnie i nie pozbywać się pozornie magicznej liczby, jak już nakreślił Philipp ...
Robbie Dee

9

Zależy to w dużej mierze od testowanej funkcji. Znam wiele przypadków, w których poszczególne liczby same w sobie nie mają specjalnego znaczenia, ale przypadek testowy jako całość jest skrupulatnie skonstruowany i dlatego ma określone znaczenie. To właśnie należy w jakiś sposób udokumentować. Na przykład, jeśli footak naprawdę jest metoda, testForTrianglektóra decyduje, czy trzy liczby mogą być prawidłowymi długościami krawędzi trójkąta, twoje testy mogą wyglądać następująco:

// standard triangle with area >0
assertEqual(testForTriangle(2, 3, 4), true);

// degenerated triangle, length of two edges match the length of the third
assertEqual(testForTriangle(1, 2, 3), true);  

// no triangle
assertEqual(testForTriangle(1, 2, 4), false); 

// two sides equal
assertEqual(testForTriangle(2, 2, 3), true);

// all three sides equal
assertEqual(testForTriangle(4, 4, 4), true);

// degenerated triangle / point
assertEqual(testForTriangle(0, 0, 0), true);  

i tak dalej. Możesz to poprawić i zamienić komentarze w parametr komunikatu, assertEqualktóry będzie wyświetlany, gdy test się nie powiedzie. Następnie możesz go jeszcze ulepszyć i przekształcić w test oparty na danych (jeśli Twoja platforma testowa to obsługuje). Niemniej jednak robisz sobie przysługę, jeśli umieścisz w kodzie notatkę, dlaczego wybrałeś te liczby i które z różnych zachowań testujesz w indywidualnym przypadku.

Oczywiście w przypadku innych funkcji indywidualne wartości parametrów mogą mieć większe znaczenie, więc używanie bezsensownej nazwy funkcji, takiej jak fooprzy pytaniu o sposób obchodzenia się ze znaczeniem parametrów, prawdopodobnie nie jest najlepszym pomysłem.


Rozsądne rozwiązanie.
user1725145

6

Dlaczego chcemy używać nazwanych Stałych zamiast liczb?

  1. OSUSZANIE - Jeśli potrzebuję wartości w 3 miejscach, chcę ją zdefiniować tylko raz, więc mogę ją zmienić w jednym miejscu, jeśli się zmieni.
  2. Nadaj znaczenie liczbom.

Jeśli napiszesz kilka testów jednostkowych, każdy z zestawem 3 liczb (startBalance, odsetki, lata) - po prostu spakowałbym wartości do testu jednostkowego jako zmienne lokalne. Najmniejszy zakres, do którego należą.

testBigInterest()
  var startBalance = 10;
  var interestInPercent = 100
  var years = 2
  assert( calcCreditSum( startBalance, interestInPercent, years ) == 40 )

testSmallInterest()
  var startBalance = 50;
  var interestInPercent = .5
  var years = 1
  assert( calcCreditSum( startBalance, interestInPercent, years ) == 50.25 )

Jeśli używasz języka, który pozwala na nazwane parametry, jest to oczywiście zbyteczne. Tam po prostu spakowałbym surowe wartości w wywołaniu metody. Nie wyobrażam sobie żadnego refaktoryzacji, dzięki czemu to stwierdzenie byłoby bardziej zwięzłe:

testBigInterest()
  assert( calcCreditSum( startBalance:       10
                        ,interestInPercent: 100
                        ,years:               2 ) = 40 )

Lub użyj Framework testujący, który pozwoli ci zdefiniować przypadki testowe w jakiejś formie tablicy lub mapy:

testcases = { {
                Name: "BigInterest"
               ,StartBalance:       10
               ,InterestInPercent: 100
               ,Years:               2
              }
             ,{ 
                Name: "SmallInterest"
               ,StartBalance:       50
               ,InterestInPercent:  .5
               ,Years:               1
              }
            }

3

... ale w tym przypadku liczby nie mają żadnego znaczenia

Numery są używane do wywoływania metody, więc z pewnością powyższe założenie jest nieprawidłowe. Możesz nie dbać o to, jakie są liczby, ale to nie ma sensu. Tak, możesz wywnioskować, do jakich liczb są używane niektóre wizualizacje IDE, ale byłoby znacznie lepiej, gdybyś tylko nadał nazwy wartościom - nawet jeśli tylko pasują one do parametrów.


1
Jednak niekoniecznie jest to prawda - jak w przykładzie z ostatniego testu jednostkowego, który napisałem ( assertEqual "Returned value" (makeKindInt 42) (runTest "lvalue_operators")). W tym przykładzie 42jest tylko wartością zastępczą, która jest generowana przez kod w skrypcie testowym o nazwie, lvalue_operatorsa następnie sprawdzana, gdy jest zwracany przez skrypt. Nie ma żadnego znaczenia, poza tym, że ta sama wartość występuje w dwóch różnych miejscach. Jaka byłaby tutaj odpowiednia nazwa, która faktycznie nadaje jakieś użyteczne znaczenie?
Jules

3

Jeśli chcesz przetestować czystą funkcję na jednym zestawie danych wejściowych, które nie są warunkami brzegowymi, to prawie na pewno chcesz przetestować ją na całej grupie zestawów danych wejściowych, które nie są (i są) warunkami brzegowymi. A dla mnie oznacza to, że powinna istnieć tabela wartości, za pomocą której można wywoływać funkcję, oraz pętla:

struct test_foo_values {
    int bar;
    int baz;
    int blurf;
    int expected;
};
const struct test_foo_values test_foo_with[] = {
   { 1, 2, 3, 17 },
   { 2, 4, 9, 34 },
   // ... many more here ...
};

for (size_t i = 0; i < ARRAY_SIZE(test_foo_with); i++) {
    const struct test_foo_values *c = test_foo_with[i];
    assertEqual(foo(c->bar, c->baz, c->blurf), c->expected);
}

Narzędzia takie jak te sugerowane w odpowiedzi Dannnno mogą pomóc w zbudowaniu tabeli wartości do przetestowania. bar, bazi blurfpowinny zostać zastąpione znaczącymi nazwami, jak omówiono w odpowiedzi Filipa .

(Sporna ogólna zasada tutaj: liczby nie zawsze są „magicznymi liczbami”, które wymagają nazw; zamiast tego liczby mogą być danymi . Jeśli sensowne byłoby umieszczenie liczb w tablicy, być może w tablicy rekordów, to prawdopodobnie są to dane I odwrotnie, jeśli podejrzewasz, że możesz mieć dane w swoich rękach, rozważ umieszczenie ich w tablicy i zdobycie ich więcej).


1

Testy różnią się od kodu produkcyjnego i przynajmniej w testach jednostkowych napisanych w Spocku, które są krótkie i do tego stopnia, że ​​nie mam problemu z użyciem stałych magicznych.

Jeśli test ma 5 linii długości i jest zgodny z podstawowym schematem podanym / kiedy / wtedy, wyodrębnienie takich wartości do stałych spowodowałoby tylko, że kod byłby dłuższy i trudniejszy do odczytania. Jeśli logika brzmi „Kiedy dodam użytkownika o imieniu Smith, widzę, że użytkownik Smith powrócił na listę użytkowników”, nie ma sensu wyodrębnianie „Smith” do stałej.

Ma to oczywiście zastosowanie, jeśli można łatwo dopasować wartości użyte w bloku „podanym” (setup) do tych, które można znaleźć w blokach „kiedy” i „następnie”. Jeśli Twoja konfiguracja testowa jest oddzielona (w kodzie) od miejsca, w którym dane są używane, lepiej byłoby użyć stałych. Ale ponieważ testy są najlepiej samodzielne, konfiguracja jest zwykle blisko miejsca użycia i ma zastosowanie pierwszy przypadek, co oznacza, że ​​stałe magiczne są w tym przypadku całkiem akceptowalne.


1

Po pierwsze, zgódźmy się, że „test jednostkowy” jest często używany do pokrycia wszystkich testów automatycznych napisanych przez programistę i że nie ma sensu dyskutować, jak każdy test powinien być nazwany…

Pracowałem nad systemem, w którym oprogramowanie pobierało wiele danych wejściowych i opracowywałem „rozwiązanie”, które musiało spełniać pewne ograniczenia, jednocześnie optymalizując inne liczby. Nie było prawidłowych odpowiedzi, więc oprogramowanie musiało dać rozsądną odpowiedź.

W tym celu wykorzystano wiele losowych liczb, aby uzyskać punkt początkowy, a następnie „wspinacz górski”, aby poprawić wynik. To było uruchamiane wiele razy, wybierając najlepszy wynik. Generator liczb losowych może zostać zaszczepiony, aby zawsze dawał te same liczby w tej samej kolejności, dlatego jeśli test ustawi ziarno, wiemy, że wynik byłby taki sam przy każdym uruchomieniu.

Przeprowadziliśmy wiele testów, które wykonały powyższe, i sprawdziliśmy, że wyniki są takie same, co powiedziało nam, że nie zmieniliśmy tego, co ta część systemu zrobiła przez pomyłkę podczas refaktoryzacji itp. Nie powiedziała nam nic o poprawności co zrobiła ta część systemu.

Testy te były kosztowne w utrzymaniu, ponieważ każda zmiana kodu optymalizacyjnego spowodowałaby przerwanie testów, ale wykryły również błędy w znacznie większym kodzie, który wstępnie przetwarzał dane i przetwarzał wyniki.

Gdy „wyśmiewaliśmy” bazę danych, można nazwać te testy „testami jednostkowymi”, ale „jednostka” była dość duża.

Często, gdy pracujesz na systemie bez testów, robisz coś takiego jak wyżej, aby upewnić się, że refaktoryzacja nie zmienia wyniku; miejmy nadzieję, że napisano lepsze testy dla nowego kodu!


1

Myślę, że w tym przypadku liczby powinny być nazywane Liczbami Arbitralnymi, a nie Liczbami Magicznymi, i po prostu komentuj wiersz jako „dowolny przypadek testowy”.

Pewnie, niektóre Liczby Magiczne mogą być również dowolne, jak w przypadku unikalnych wartości „obsługi” (które oczywiście powinny zostać zastąpione nazwanymi stałymi), ale mogą być również wstępnie obliczonymi stałymi, takimi jak „prędkość nieobciążonego europejskiego wróbla w furlongach na dwa tygodnie”, gdzie wartość liczbowa jest podłączana bez komentarzy i pomocnego kontekstu.


0

Nie zaryzykuję, aby powiedzieć ostateczne tak / nie, ale oto kilka pytań, które powinieneś sobie zadać, decydując, czy to jest w porządku, czy nie.

  1. Jeśli liczby nic nie znaczą, dlaczego są one na pierwszym miejscu? Czy można je zastąpić czymś innym? Czy potrafisz przeprowadzić weryfikację na podstawie wywołań metod i przepływu zamiast asercji wartości? Rozważmy coś takiego jak verify()metoda Mockito, która sprawdza, czy pewne wywołania metod zostały wykonane w celu wyszydzenia obiektów zamiast faktycznego potwierdzenia wartości.

  2. Jeśli numery zrobić coś znaczy, to powinny one być przypisane do zmiennych, które są nazwane odpowiednio.

  3. Zapisanie liczby, 2która TWOmoże być pomocna w niektórych kontekstach, a nie tyle w innych kontekstach.

    • Na przykład: assertEquals(TWO, half_of(FOUR))ma sens dla kogoś, kto czyta kod. Od razu wiadomo, co testujesz.
    • Jeśli jednak twój test jest assertEquals(numCustomersInBank(BANK_1), TWO), to nie ma to większego sensu. Dlaczego ma BANK_1zawierać dwie klientów? Co mamy do testowania?
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.