Należy zapisać savePeople () w testach jednostkowych
Tak, powinno. Ale spróbuj napisać warunki testowe w sposób niezależny od implementacji. Na przykład przekształcając przykład użycia w test jednostkowy:
function testSavePeople() {
myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);
assert(myDataStore.containsPerson('Joe'));
assert(myDataStore.containsPerson('Maggie'));
assert(myDataStore.containsPerson('John'));
}
Ten test wykonuje wiele czynności:
- weryfikuje kontrakt funkcji
savePeople()
- nie dba o wdrożenie
savePeople()
- dokumentuje przykładowe użycie
savePeople()
Pamiętaj, że nadal możesz wyśmiewać / odgałęzić / sfałszować magazyn danych. W takim przypadku nie sprawdziłbym jawnych wywołań funkcji, ale wynik operacji. W ten sposób mój test jest przygotowany na przyszłe zmiany / refaktory.
Na przykład implementacja magazynu danych może zapewnić saveBulkPerson()
metodę w przyszłości - teraz zmiana implementacji savePeople()
użycia saveBulkPerson()
nie przerwałaby testu jednostkowego, o ile saveBulkPerson()
działa zgodnie z oczekiwaniami. A jeśli saveBulkPerson()
jakoś nie zadziała zgodnie z oczekiwaniami, twój test jednostkowy to wykryje.
lub czy takie testy byłyby równoznaczne z testowaniem wbudowanej konstrukcji języka forEach?
Jak powiedziano, spróbuj przetestować oczekiwane wyniki i interfejs funkcji, a nie implementację (chyba że wykonujesz testy integracyjne - wtedy przydałoby się przechwytywanie określonych wywołań funkcji). Jeśli istnieje wiele sposobów implementacji funkcji, wszystkie powinny działać z testem jednostkowym.
Jeśli chodzi o twoją aktualizację pytania:
Testuj zmiany stanu! Np. Część ciasta zostanie wykorzystana. Zgodnie z wdrożeniem, upewnij się, że ilość użytego dough
pasuje do pan
lub zapewnij, że dough
jest zużyty. Upewnij się, że pan
zawiera pliki cookie po wywołaniu funkcji. Upewnij się, że oven
jest pusty / w takim samym stanie jak poprzednio.
W przypadku dodatkowych testów sprawdź przypadki skrajne: Co się stanie, jeśli oven
nie będzie puste przed wywołaniem? Co się stanie, jeśli będzie za mało dough
? Jeśli pan
jest już pełny?
Powinieneś być w stanie wydedukować wszystkie wymagane dane do tych testów z samego ciasta, naczynia i piekarnika. Nie trzeba przechwytywać wywołań funkcji. Traktuj funkcję tak, jakby jej implementacja nie była dla Ciebie dostępna!
W rzeczywistości większość użytkowników TDD pisze testy przed napisaniem funkcji, więc nie są zależni od faktycznej implementacji.
Do najnowszego dodatku:
Gdy użytkownik utworzy nowe konto, musi się zdarzyć kilka rzeczy: 1) należy utworzyć nowy rekord użytkownika w bazie danych 2) należy wysłać powitalny e-mail 3) adres IP użytkownika musi zostać zarejestrowany w celu oszustwa cele.
Chcemy więc stworzyć metodę, która łączy wszystkie kroki „nowego użytkownika”:
function createNewUser(validatedUserData, emailService, dataStore) {
userId = dataStore.insertUserRecord(validateduserData);
emailService.sendWelcomeEmail(validatedUserData);
dataStore.recordIpAddress(userId, validatedUserData.ip);
}
Dla takiej funkcji wyśmiewałbym / stub / fake (cokolwiek wydaje się bardziej ogólne) parametry dataStore
i emailService
. Ta funkcja nie wykonuje żadnych zmian stanów na żadnym parametrze, deleguje je do metod niektórych z nich. Chciałbym sprawdzić, czy wywołanie funkcji spowodowało 4 rzeczy:
- wstawił użytkownika do magazynu danych
- wysłał (lub przynajmniej wywołał odpowiednią metodę) wiadomość powitalną
- zarejestrował adres IP użytkowników w magazynie danych
- przekazał wszelkie napotkane wyjątki / błędy (jeśli występują)
Pierwsze 3 kontrole można wykonać za pomocą próbnych, stubów lub podróbek dataStore
i emailService
(naprawdę nie chcesz wysyłać wiadomości e-mail podczas testowania). Ponieważ musiałem to sprawdzić w przypadku niektórych komentarzy, oto różnice:
- Fałszywy to przedmiot, który zachowuje się tak samo jak oryginał i jest do pewnego stopnia nierozróżnialny. Jego kod można normalnie wykorzystać ponownie w testach. Może to być na przykład prosta baza danych w pamięci dla opakowania bazy danych.
- Kod pośredniczący po prostu implementuje tyle, ile jest potrzebne do spełnienia wymaganych operacji tego testu. W większości przypadków odcinek jest specyficzny dla testu lub grupy testów wymagających jedynie niewielkiego zestawu metod oryginału. W tym przykładzie może to być
dataStore
implementacja odpowiedniej wersji insertUserRecord()
i recordIpAddress()
.
- Makieta to obiekt, który pozwala zweryfikować, w jaki sposób jest używany (najczęściej poprzez umożliwienie oceny wywołań jego metod). Staram się używać ich oszczędnie w testach jednostkowych, ponieważ przy ich użyciu faktycznie próbujesz przetestować implementację funkcji, a nie zgodność z interfejsem, ale nadal mają one swoje zastosowania. Istnieje wiele próbnych ram, które pomagają stworzyć tylko próbkę, której potrzebujesz.
Zauważ, że jeśli którakolwiek z tych metod zgłosi błąd, chcemy, aby błąd pojawił się w kodzie wywołującym, aby mógł obsłużyć błąd według własnego uznania. Jeśli jest wywoływany przez kod API, może przełożyć błąd na odpowiedni kod odpowiedzi HTTP. Jeśli jest wywoływany przez interfejs sieciowy, może przełożyć błąd na odpowiedni komunikat wyświetlany użytkownikowi i tak dalej. Chodzi o to, że ta funkcja nie wie, jak obsłużyć błędy, które mogą zostać zgłoszone.
Oczekiwane wyjątki / błędy są poprawnymi przypadkami testowymi: Potwierdzasz, że w przypadku wystąpienia takiego zdarzenia funkcja zachowuje się tak, jak się spodziewasz. Można to osiągnąć, pozwalając, aby odpowiedni obiekt próbny / fałszywy / skrótowy rzucał w razie potrzeby.
Istotą mojego pomieszania jest to, że do testowania jednostkowego takiej funkcji wydaje się konieczne powtórzenie dokładnej implementacji w samym teście (przez określenie, że metody są wywoływane na próbach w określonej kolejności) i to wydaje się nieprawidłowe.
Czasami trzeba to zrobić (choć w testach integracyjnych zależy Ci na tym). Częściej istnieją inne sposoby weryfikacji oczekiwanych efektów ubocznych / zmian stanu.
Weryfikacja dokładnych wywołań funkcji powoduje dość kruche testy jednostkowe: tylko niewielkie zmiany oryginalnej funkcji powodują, że zawodzą. Może to być pożądane lub nie, ale wymaga zmiany odpowiednich testów jednostkowych za każdym razem, gdy zmieniasz funkcję (czy to refaktoryzacja, optymalizacja, usuwanie błędów, ...).
Niestety, w takim przypadku test jednostkowy traci część swojej wiarygodności: ponieważ został zmieniony, nie potwierdza funkcji po zmianie zachowuje się tak samo jak poprzednio.
Na przykład rozważmy dodanie przez kogoś wywołania oven.preheat()
(optymalizacja!) W przykładzie pieczenia ciasteczek:
- Jeśli wyśmiewałeś obiekt piekarnika, nie spodziewa się on wywołania i niepowodzenia testu, chociaż obserwowalne zachowanie metody nie uległo zmianie (mam nadzieję, że nadal masz garnek ciastek).
- Kod pośredniczący może, ale nie musi, zawieść, w zależności od tego, czy dodałeś tylko metody, które mają być testowane, czy cały interfejs z niektórymi sztucznymi metodami.
- Fałszywy nie powinien zawieść, ponieważ powinien implementować metodę (zgodnie z interfejsem)
W moich testach jednostkowych staram się być tak ogólny, jak to możliwe: jeśli implementacja ulegnie zmianie, ale widoczne zachowanie (z perspektywy osoby wywołującej) pozostanie takie samo, moje testy powinny zostać zaliczone. Idealnie, jedynym przypadkiem, w którym muszę zmienić istniejący test jednostkowy, powinna być naprawa błędu (testu, a nie testowanej funkcji).