Gdzie przebiega granica między logiką aplikacji do testowania jednostkowego a konstrukcjami nieufnymi językami?

87

Rozważ taką funkcję:

function savePeople(dataStore, people) {
    people.forEach(person => dataStore.savePerson(person));
}

Można go użyć w następujący sposób:

myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);

Pozwól nam zakładać, że Storema swoje własne testy jednostkowe, czy sprzedawca-warunkiem. W każdym razie ufamy Store. Załóżmy ponadto, że obsługa błędów - np. Błędów rozłączenia bazy danych - nie jest odpowiedzialna za savePeople. Rzeczywiście, załóżmy, że sam sklep jest magiczną bazą danych, która nie może w żaden sposób popełnić błędu. Biorąc pod uwagę te założenia, pytanie brzmi:

Czy powinny savePeople()być testowane jednostkowo, czy też takie testy mogłyby oznaczać testowanie wbudowanej forEachkonstrukcji języka?

Moglibyśmy oczywiście wprowadzić próbną kpinę dataStorei zapewnić, że dataStore.savePerson()wezwanie jest raz dla każdej osoby. Z pewnością można argumentować, że taki test zapewnia bezpieczeństwo przed zmianami implementacyjnymi: np. Jeśli zdecydujemy się zastąpić forEachtradycyjną forpętlą lub inną metodą iteracji. Test nie jest więc całkowicie trywialny. A jednak wydaje się okropnie blisko ...


Oto kolejny przykład, który może być bardziej owocny. Zastanów się nad funkcją, która jedynie koordynuje inne obiekty lub funkcje. Na przykład:

function bakeCookies(dough, pan, oven) {
    panWithRawCookies = pan.add(dough);
    oven.addPan(panWithRawCookies);
    oven.bakeCookies();
    oven.removePan();
}

Jak taką funkcję należy testować jednostkowo, zakładając, że tak powinno być? Trudno mi sobie wyobrazić, wszelkiego rodzaju testów jednostkowych, które nie tylko Mock dough, pani oven, a następnie stwierdzić, że metody są wywoływane na nich. Ale taki test nie robi nic więcej, jak powielenie dokładnej implementacji funkcji.

Czy ta niezdolność do przetestowania funkcji w sensowny czarny sposób wskazuje na wadę projektową samej funkcji? Jeśli tak, jak można to poprawić?


Aby jeszcze bardziej wyjaśnić pytanie motywujące ten bakeCookiesprzykład, dodam bardziej realistyczny scenariusz, z którym się spotkałem przy próbie dodania testów i refaktoryzacji starszego kodu.

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);
}

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.

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.

Jonasz
źródło
44
Po uruchomieniu. Czy masz ciasteczka
Ewan
6
odnośnie twojej aktualizacji: dlaczego miałbyś kiedykolwiek kpić z patelni? czy ciasto? brzmią one jak proste obiekty w pamięci, których tworzenie powinno być trywialne, dlatego nie ma powodu, dla którego nie należy testować ich jako jednej jednostki. pamiętaj, że „jednostka” w „testach jednostkowych” nie oznacza „pojedynczej klasy”. oznacza to „najmniejszą możliwą jednostkę kodu używaną do wykonania zadania”. patelnia jest prawdopodobnie niczym więcej niż pojemnikiem na przedmioty z ciasta, więc byłoby wymyślone, aby przetestować ją w izolacji zamiast po prostu testowania metody pieczenia ciastek z zewnątrz w środku
sara
11
Pod koniec dnia podstawową zasadą jest to, że piszesz wystarczającą liczbę testów, aby upewnić się, że kod działa, i że jest to odpowiedni „kanarek w kopalni węgla”, gdy ktoś coś zmieni. Otóż ​​to. Nie ma magicznych zaklęć, formułowanych przypuszczeń ani twierdzeń dogmatycznych, dlatego 85% do 90% pokrycia kodu (nie 100%) jest powszechnie uważane za doskonałe.
Robert Harvey
5
@RobertHarvey niestety formalne frazesy i zgryzienia dźwięku TDD, choć na pewno przyniosą Ci entuzjastyczne ukłony, nie pomagaj rozwiązywać rzeczywistych problemów. w tym celu musisz ubrudzić sobie ręce i zaryzykować odpowiedź na rzeczywiste pytanie
Jonah,
4
Test jednostkowy w kolejności malejącej złożoności cyklicznej. Zaufaj mi, skończy Ci się czas, zanim przejdziesz do tej funkcji
Neil McGuigan

Odpowiedzi:

118

Czy savePeople()należy przetestować jednostkę? Tak. Nie testujesz, czy dataStore.savePersondziała, czy działa połączenie db, a nawet czy foreachdziała. Testujesz, który savePeoplespełnia obietnicę, jaką daje dzięki umowie.

Wyobraź sobie ten scenariusz: ktoś dokonuje dużego refaktoryzacji bazy kodu i przypadkowo usuwa forEachczęść implementacji, aby zawsze zapisywał tylko pierwszy element. Czy nie chciałbyś, aby test jednostkowy to wykrył?

Bryan Oakley
źródło
20
@RobertHarvey: Jest dużo szarej strefy, a wyróżnienie, IMO, nie jest ważne. Masz jednak rację - nie jest tak naprawdę ważne, aby sprawdzać, czy „wywołuje właściwe funkcje”, ale raczej „robi właściwą rzecz” niezależnie od tego, jak to robi. Ważne jest przetestowanie, że biorąc pod uwagę określony zestaw danych wejściowych do funkcji, otrzymujesz określony zestaw danych wyjściowych. Widzę jednak, że to ostatnie zdanie może być mylące, więc je usunąłem.
Bryan Oakley
64
„Testujesz, czy savePeople spełnia obietnicę zawartą w umowie”. To. Tyle tego.
Lovis,
2
Chyba że masz test systemowy „end to end”, który go obejmuje.
Ian
6
@Ian Kompleksowe testy nie zastępują testów jednostkowych, są komplementarne. Tylko dlatego, że możesz mieć kompleksowy test, który zapewnia, że ​​możesz zapisać jaskinię listę osób, nie oznacza, że ​​nie powinieneś mieć testu jednostkowego, aby go objąć.
Vincent Savard
4
@VincentSavard, ale koszt / korzyść testu jednostkowego jest zmniejszona, jeśli ryzyko jest kontrolowane w inny sposób.
Ian
36

Zazwyczaj tego rodzaju pytanie pojawia się, gdy ludzie robią programowanie „po testach”. Podejdź do tego problemu z punktu widzenia TDD, gdzie testy poprzedzają implementację, i zadaj sobie to pytanie ponownie jako ćwiczenie.

Przynajmniej w mojej aplikacji TDD, która zwykle jest na zewnątrz, nie implementowałbym funkcji takiej jak savePeoplepo jej wdrożeniu savePerson. Funkcje savePeoplei savePersonbyłyby uruchamiane jako jedna i byłyby sterowane testami z tych samych testów jednostkowych; rozdzielenie między nimi nastąpiłoby po kilku testach na etapie refaktoryzacji. Ten tryb pracy postawiłby również pytanie, gdzie savePeoplepowinna być funkcja - czy jest to funkcja wolna, czy jej część dataStore.

W końcu, testy nie tylko sprawdzić, czy można poprawnie zapisać Personw Store, ale także wiele osób. Doprowadziłoby mnie to również do pytania, czy konieczne są inne kontrole, na przykład: „Czy muszę się upewnić, że savePeoplefunkcja jest atomowa, albo zapisuje wszystko, albo nie ma żadnej?”, „Czy może po prostu zwrócić błędy osobom, które nie mogły” zostać zapisane? Jak wyglądałyby te błędy? ”itd. Wszystko to oznacza znacznie więcej niż tylko sprawdzenie użycia jednej forEachlub innych form iteracji.

Chociaż, jeśli wymóg zapisania więcej niż jednej osoby naraz pojawił się dopiero po tym, jak savePersonzostał już spełniony , zaktualizowałbym istniejące testy, savePersonaby przejść przez nową funkcję savePeople, upewniając się, że nadal może uratować jedną osobę, po prostu delegując na początku, następnie przetestuj zachowanie więcej niż jednej osoby za pomocą nowych testów, zastanawiając się, czy konieczne byłoby nadanie temu atomowi charakteru, czy nie.

MichelHenrich
źródło
4
Zasadniczo przetestuj interfejs, a nie implementację.
Snoop,
8
Uczciwe i wnikliwe punkty. Jednak wydaje mi się, że w jakiś sposób unikam mojego prawdziwego pytania :) Twoja odpowiedź brzmi: „W prawdziwym świecie, w dobrze zaprojektowanym systemie, nie sądzę, by istniała ta uproszczona wersja twojego problemu”. Ponownie, uczciwie, ale specjalnie stworzyłem tę uproszczoną wersję, aby podkreślić istotę bardziej ogólnego problemu. Jeśli nie potrafisz ominąć sztucznej natury przykładu, możesz sobie wyobrazić inny przykład, w którym miałeś dobry powód dla podobnej funkcji, która polegała tylko na iteracji i delegowaniu. A może uważasz, że to po prostu niemożliwe?
Jonah
@Jonah zaktualizowane. Mam nadzieję, że lepiej odpowie na twoje pytanie. Wszystko to opiera się na opiniach i może być sprzeczne z celem tej witryny, ale z pewnością jest to bardzo interesująca dyskusja. Nawiasem mówiąc, próbowałem odpowiedzieć z punktu widzenia pracy zawodowej, w której musimy starać się pozostawić testy jednostkowe dla wszystkich zachowań aplikacji, niezależnie od tego, jak banalna może być implementacja, ponieważ mamy obowiązek zbudować dobrze przetestowany i udokumentowany system dla nowych opiekunów, jeśli odejdziemy. W przypadku projektów osobistych lub, powiedzmy, niekrytycznych (pieniądze są również kluczowe), mam zupełnie inne zdanie.
MichelHenrich
Dziękuję za aktualizację. Jak dokładnie byś przetestował savePeople? Jak opisałem w ostatnim akapicie OP lub w inny sposób?
Jonah
1
Przepraszam, nie wyraziłem się jasno w części „bez mocków”. Miałem na myśli, że nie użyję makiety dla savePersonfunkcji, jak zasugerowałeś, zamiast tego przetestowałbym ją bardziej ogólnie savePeople. Testy jednostkowe dla Storezostałyby zmienione tak, aby uruchamiały savePeoplezamiast bezpośrednio wywoływać savePerson, więc do tego nie są stosowane żadne makiety. Ale baza danych oczywiście nie powinna być obecna, ponieważ chcielibyśmy odizolować problemy z kodowaniem od różnych problemów z integracją, które występują w rzeczywistych bazach danych, więc tutaj wciąż mamy kpinę.
MichelHenrich
21

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 doughpasuje do panlub zapewnij, że doughjest zużyty. Upewnij się, że panzawiera pliki cookie po wywołaniu funkcji. Upewnij się, że ovenjest pusty / w takim samym stanie jak poprzednio.

W przypadku dodatkowych testów sprawdź przypadki skrajne: Co się stanie, jeśli ovennie będzie puste przed wywołaniem? Co się stanie, jeśli będzie za mało dough? Jeśli panjest 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 dataStorei 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 dataStorei 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ć dataStoreimplementacja 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).

hoffmale
źródło
1
Problem polega na tym, że jak tylko napiszesz myDataStore.containsPerson('Joe'), zakładasz istnienie funkcjonalnej testowej bazy danych. Gdy to zrobisz, piszesz test integracyjny, a nie test jednostkowy.
Jonah
Zakładam, że mogę polegać na posiadaniu testowego magazynu danych (nie dbam o to, czy to prawdziwy czy fałszywy) i że wszystko działa zgodnie z konfiguracją (ponieważ powinienem już mieć testy jednostkowe dla tych przypadków). Jedyne, co test chce przetestować, to savePeople()faktyczne dodanie tych osób do dowolnego magazynu danych, który udostępniasz, o ile ten magazyn danych implementuje oczekiwany interfejs. Test integracji polegałby na przykład na sprawdzeniu, czy moje opakowanie bazy danych faktycznie wykonuje odpowiednie wywołania bazy danych dla wywołania metody.
hoffmale
W celu wyjaśnienia, jeśli używasz makiety, wszystko, co można zrobić, to twierdzą, że metoda na tym mock był nazywany , być może z jakiegoś konkretnego parametru. Nie możesz później potwierdzić stanu próbnego. Jeśli więc chcesz wywoływać stan bazy danych po wywołaniu testowanej metody, tak jak w myDataStore.containsPerson('Joe'), musisz użyć funkcjonalnej bazy danych . Po wykonaniu tego kroku nie jest to już test jednostkowy.
Jonah
1
nie musi to być prawdziwa baza danych - tylko obiekt, który implementuje ten sam interfejs co rzeczywista składnica danych (czytaj: przechodzi odpowiednie testy jednostkowe dla interfejsu składnicy danych). Nadal uważałbym to za kpinę. Pozwól mockowi przechowywać wszystko, co dodaje dowolna metoda, aby to zrobić, w tablicy i sprawdź, czy dane testowe (elementy myPeople) znajdują się w tablicy. IMHO próbny powinien nadal mieć takie samo obserwowalne zachowanie, jakiego oczekuje się od rzeczywistego obiektu, w przeciwnym razie testujesz zgodność z próbnym interfejsem, a nie rzeczywistym interfejsem.
hoffmale
„Pozwól mockowi przechowywać wszystko, co dodaje dowolna metoda, aby to zrobić w tablicy i sprawdź, czy dane testowe (elementy myPeople) znajdują się w tablicy” - to wciąż „prawdziwa” baza danych, tylko ad hoc, zbudowany w pamięci. „IMHO makieta powinna nadal mieć takie samo obserwowalne zachowanie, jakiego oczekuje się od prawdziwego obiektu” - przypuszczam, że możesz się za tym opowiadać, ale nie to oznacza „kpina” w literaturze testowej lub w dowolnej popularnej bibliotece, którą ja ” widziałam. Próbka po prostu sprawdza, czy oczekiwane metody są wywoływane z oczekiwanymi parametrami.
Jonah
13

Podstawową wartością, jaką zapewnia taki test, jest fakt, że implementacja jest refaktoryzowalna.

W swojej karierze przeprowadzałem wiele optymalizacji wydajności i często napotykałem problemy z dokładnie pokazanym wzorem: aby zapisać N jednostek w bazie danych, wykonać N wstawień. Zwykle bardziej wydajne jest wykonywanie wstawiania zbiorczego za pomocą pojedynczej instrukcji.

Z drugiej strony nie chcemy też przedwcześnie optymalizować. Jeśli zwykle ratujesz tylko 1-3 osoby na raz, pisanie zoptymalizowanej partii może być przesadą.

Przy odpowiednim teście jednostkowym możesz napisać go tak, jak go wdrożyłeś powyżej, a jeśli okaże się, że musisz go zoptymalizować, możesz to zrobić za pomocą siatki bezpieczeństwa automatycznego testu w celu wykrycia błędów. Oczywiście różni się to w zależności od jakości testów, więc testuj swobodnie i dobrze testuj.

Drugą zaletą testowania tego zachowania przez jednostkę jest służenie jako dokumentacja tego, do czego służy. Ten trywialny przykład może być oczywisty, ale biorąc pod uwagę następny punkt poniżej, może być bardzo ważny.

Trzecią zaletą, na którą zwrócili uwagę inni, jest możliwość testowania ukrytych detali, które są bardzo trudne do przetestowania za pomocą testów integracyjnych lub akceptacyjnych. Na przykład, jeśli istnieje wymóg, aby wszyscy użytkownicy byli zapisywani atomowo, możesz napisać testową skrzynkę, która da ci pewność, że zachowuje się zgodnie z oczekiwaniami, a także służy jako dokumentacja dla wymagania, które może nie być oczywiste dla nowych programistów.

Dodam myśl, którą dostałem od instruktora TDD. Nie testuj metody. Przetestuj zachowanie. Innymi słowy, nie testujesz, czy to savePeopledziała, testujesz, czy wielu użytkowników można zapisać w jednym połączeniu.

Przekonałem się, że moja zdolność do przeprowadzania testów jednostkowych jakości i TDD poprawia się, kiedy przestałem myśleć o testach jednostkowych jako o sprawdzaniu działania programu, ale raczej sprawdzają, czy jednostka kodu działa zgodnie z oczekiwaniami . Te są różne. Nie sprawdzają, czy to działa, ale sprawdzają, czy działa tak, jak sądzę. Kiedy zacząłem tak myśleć, moja perspektywa się zmieniła.

Brandon
źródło
Przykład refaktoryzacji wkładki luzem jest dobry. Możliwy test jednostkowy, który zasugerowałem w OP - że próbka danych StoreStore wywołała savePersongo dla każdej osoby na liście - przerwałaby się jednak przy masowym refaktoryzowaniu wkładki. Co dla mnie wskazuje, że jest to zły test jednostkowy. Nie widzę jednak alternatywy, która przekazałaby zarówno implementacje zbiorcze, jak i implementacje typu jeden zapis na osobę, bez użycia rzeczywistej testowej bazy danych i zapewnienia jej, co wydaje się błędne. Czy możesz podać test, który działa dla obu implementacji?
Jonah
1
@ jpmc26 Co powiesz na test sprawdzający, czy ludzie zostali uratowani ...?
immibis
1
@immibis Nie rozumiem, co to znaczy. Przypuszczalnie prawdziwy sklep jest wspierany przez bazę danych, więc będziesz musiał go wyśmiewać lub usunąć z niego w celu przeprowadzenia testu jednostkowego. W tym momencie testowałbyś, czy twój próbny lub skrótowy może przechowywać obiekty. To całkowicie bezużyteczne. Najlepsze, co możesz zrobić, to stwierdzić, że savePersonmetoda została wywołana dla każdego wejścia, a jeśli zastąpisz pętlę wstawieniem zbiorczym, nie będziesz już wywoływać tej metody. Twój test się zepsuje. Jeśli masz na myśli coś innego, jestem otwarty na to, ale jeszcze tego nie widzę. (I nie widziałem, że o to mi chodziło).
jpmc26,
1
@immibis Nie uważam tego za przydatny test. Korzystanie z fałszywego magazynu danych nie daje mi żadnej pewności, że zadziała z rzeczywistością. Skąd mam wiedzieć, że moje fałszywe działa jak prawdziwe? Wolę po prostu pozwolić, aby obejmował to zestaw testów integracyjnych. (Prawdopodobnie powinienem wyjaśnić, że miałem na myśli „dowolny test jednostkowy ” w moim pierwszym komentarzu tutaj.)
jpmc26
1
@immibis Właściwie zastanawiam się nad moją pozycją. Sceptycznie podchodzę do wartości testów jednostkowych z powodu takich problemów, ale może nie doceniam tej wartości, nawet jeśli sfałszujesz dane wejściowe. I nie wiem, że testowanie wejście / wyjście wydaje się być o wiele bardziej przydatne niż pozornie ciężkich testów, ale może odmowa zastąpić wejście z fałszywy jest faktycznie częścią problemu tutaj.
jpmc26
6

Powinien bakeCookies()zostać przetestowany? Tak.

Jak taką funkcję należy testować jednostkowo, zakładając, że tak powinno być? Trudno mi wyobrazić sobie jakikolwiek test jednostkowy, który nie wyśmiewałby ciasta, patelni i piekarnika, a następnie twierdził, że wywoływane są metody.

Nie całkiem. Przyjrzyj się dokładnie, CO ma robić funkcja - ma ustawić ovenobiekt w określonym stanie. Patrząc na kod, wydaje się, że stany obiektów pani doughnie mają tak naprawdę większego znaczenia. Powinieneś więc przekazać ovenobiekt (lub wyśmiewać go) i upewnić się, że jest on w określonym stanie na końcu wywołania funkcji.

Innymi słowy, powinieneś stwierdzić, że bakeCookies()upiekły ciasteczka .

W przypadku bardzo krótkich funkcji testy jednostkowe mogą wydawać się czymś więcej niż tautologią. Ale nie zapominaj, że twój program będzie trwał znacznie dłużej niż czas, w którym go piszesz. Ta funkcja może, ale nie musi ulec zmianie w przyszłości.

Testy jednostkowe spełniają dwie funkcje:

  1. Sprawdza, czy wszystko działa. Jest to najmniej przydatna funkcja testów jednostkowych i wydaje się, że wydaje się, że rozważasz tę funkcjonalność tylko przy zadawaniu pytania.

  2. Sprawdza, czy przyszłe modyfikacje programu nie psują funkcjonalności, która została wcześniej zaimplementowana. Jest to najbardziej przydatna funkcja testów jednostkowych i zapobiega wprowadzaniu błędów do dużych programów. Jest przydatny w normalnym kodowaniu podczas dodawania funkcji do programu, ale jest bardziej użyteczny w refaktoryzacji i optymalizacjach, w których podstawowe algorytmy wdrażające program są dramatycznie zmieniane bez zmiany jakiegokolwiek możliwego do zaobserwowania zachowania programu.

Nie testuj kodu wewnątrz funkcji. Zamiast tego sprawdź, czy funkcja robi to, co mówi. Kiedy spojrzysz na testy jednostkowe w ten sposób (testowanie funkcji, a nie kodu), zdasz sobie sprawę, że nigdy nie testujesz konstrukcji języka, ani nawet logiki aplikacji. Testujesz interfejs API.

Slebetman
źródło
Cześć, dziękuję za odpowiedź. Czy mógłbyś spojrzeć na moją drugą aktualizację i podzielić się swoimi przemyśleniami na temat testowania funkcji w tym przykładzie?
Jonah
Uważam, że może to być skuteczne, jeśli możesz użyć prawdziwego piekarnika lub fałszywego piekarnika, ale jest znacznie mniej skuteczne w przypadku fałszywego piekarnika. Drwiny (według definicji Meszarosa) nie mają innego stanu niż zapis metod, które zostały na nich wywołane. Z mojego doświadczenia wynika, że ​​gdy takie funkcje bakeCookiessą testowane w ten sposób, mają tendencję do psucia się podczas refaktorów, co nie wpłynęłoby na obserwowalne zachowanie aplikacji.
James_pic
@James_pic, dokładnie. I tak, to jest próbna definicja, której używam. Biorąc pod uwagę komentarz, co robisz w takim przypadku? Zignorować test? W każdym razie napisać kruchy, powtarzający się test? Coś innego?
Jonah
@Jonah Zasadniczo albo spojrzę na testowanie tego komponentu za pomocą testów integracyjnych (znalazłem ostrzeżenia o tym, że trudniej jest debugować, aby zostać przesadzonym, być może ze względu na jakość nowoczesnych narzędzi), albo podejmę trud tworzenia na wpół przekonujący podróbka.
James_pic
3

Czy metoda savePeople () powinna być testowana jednostkowo, czy może to oznaczać testowanie wbudowanej konstrukcji języka forEach?

Tak. Ale możesz to zrobić w sposób, który po prostu ponownie przetestuje konstrukcję.

Należy tutaj zauważyć, jak zachowuje się ta funkcja, gdy savePersonawaria jest w połowie? Jak to ma działać?

Jest to rodzaj subtelnego zachowania, które zapewnia funkcja, którą należy egzekwować za pomocą testów jednostkowych.

Telastyn
źródło
Tak, zgadzam się, że należy przetestować subtelne warunki błędu , ale imo to nie jest interesujące pytanie - odpowiedź jest jasna. Stąd powód, dla którego wyraźnie stwierdziłem, że do celów mojego pytania savePeoplenie powinienem być odpowiedzialny za obsługę błędów. Aby wyjaśnić jeszcze raz, zakładając, że savePeoplejest on odpowiedzialny tylko za iterację po liście i delegowanie zapisywania każdego elementu na inną metodę, czy należy go nadal testować?
Jonah
@Jonah: Jeśli zamierzasz nalegać na ograniczenie testu jednostkowego wyłącznie do foreachkonstrukcji, a nie żadnych warunków, skutków ubocznych lub zachowań poza nim, masz rację; nowy test jednostkowy nie jest wcale taki interesujący.
Robert Harvey
1
@jonah - czy powinien iterować i zapisywać jak najwięcej, czy też zatrzymać błąd? Pojedynczy zapis nie może o tym zadecydować, ponieważ nie może wiedzieć, w jaki sposób jest używany.
Telastyn
1
@jonah - witamy na stronie. Jednym z kluczowych elementów naszego formatu pytań i odpowiedzi jest to, że nie jesteśmy tutaj, aby Ci pomóc . Twoje pytanie oczywiście ci pomaga, ale pomaga także wielu innym osobom, które odwiedzają witrynę w poszukiwaniu odpowiedzi na swoje pytania. Odpowiedziałem na pytanie, które zadałeś. To nie moja wina, że ​​nie podoba ci się odpowiedź lub wolisz przesunąć bramki. I szczerze mówiąc, wygląda na to, że inne odpowiedzi mówią tę samą podstawową rzecz, choć bardziej wymownie.
Telastyn
1
@Telastyn, próbuję uzyskać wgląd w testy jednostkowe. Moje początkowe pytanie nie było wystarczająco jasne, więc dodaję wyjaśnienia, aby skierować rozmowę na moje prawdziwe pytanie. Zdecydowałeś się zinterpretować to jako moje oszustwo w grze „mieć rację”. Spędziłem setki godzin odpowiadając na pytania dotyczące przeglądu kodu i SO. Moim celem jest zawsze pomaganie osobom, na które odpowiadam. Jeśli nie, to twój wybór. Nie musisz odpowiadać na moje pytania.
Jonah
3

Kluczem tutaj jest twoje spojrzenie na określoną funkcję jako banalną. Większość programowania jest trywialna: przypisz wartość, zrób matematykę, podejmij decyzję: jeśli to, to kontynuuj pętlę, aż ... W izolacji wszystkie trywialne. Właśnie przejrzałeś 5 pierwszych rozdziałów każdej książki uczącej języka programowania.

Fakt, że pisanie testu jest tak łatwe, powinien być znakiem, że Twój projekt nie jest taki zły. Czy wolisz projekt, który nie jest łatwy do przetestowania?

„To się nigdy nie zmieni”. tak zaczyna się większość nieudanych projektów. Test jednostkowy określa tylko, czy jednostka działa zgodnie z oczekiwaniami w określonych okolicznościach. Zdobądź go, a wtedy możesz zapomnieć o szczegółach jego implementacji i po prostu go użyć. Wykorzystaj przestrzeń mózgu do następnego zadania.

Wiedza o tym, że wszystko działa zgodnie z oczekiwaniami, jest bardzo ważna i nie jest trywialna w dużych projektach, a zwłaszcza w dużych zespołach. Jeśli jest coś, co łączy programistów, to fakt, że wszyscy mieliśmy do czynienia z okropnym kodem innej osoby. Możemy przynajmniej przeprowadzić testy. W razie wątpliwości napisz test i przejdź dalej.

JeffO
źródło
Dziękuję za opinie. Słuszne uwagi. Pytanie, na które naprawdę chcę uzyskać odpowiedź (właśnie dodałem kolejną aktualizację w celu wyjaśnienia), to właściwy sposób testowania funkcji, które wykonują jedynie wywołanie sekwencji innych usług za pośrednictwem delegacji. W takich przypadkach wydaje się, że testy jednostkowe odpowiednie do „udokumentowania kontraktu” są jedynie powtórzeniem implementacji funkcji, zapewniając, że metody są wywoływane w różnych próbach. Jednak test identyczny z wdrożeniem w tych przypadkach wydaje się błędny ...
Jonah
1

Czy metoda savePeople () powinna być testowana jednostkowo, czy może to oznaczać testowanie wbudowanej konstrukcji języka forEach?

Odpowiedział na to już @BryanOakley, ale mam dodatkowe argumenty (tak sądzę):

Po pierwsze, test jednostkowy służy do testowania wykonania umowy, a nie wdrożenia interfejsu API; test powinien ustalić warunki wstępne, a następnie wywołać, a następnie sprawdzić efekty, skutki uboczne, niezmienniki i warunki po. Kiedy decydujesz, co przetestować, implementacja interfejsu API nie ma (i nie powinna) mieć znaczenia .

Po drugie, twój test będzie tam, aby sprawdzić niezmienniki, gdy funkcja się zmieni . Fakt, że teraz się nie zmienia , nie oznacza, że ​​nie powinieneś mieć testu.

Po trzecie, warto wdrożyć trywialny test, zarówno w podejściu TDD (który tego wymaga), jak i poza nim.

Pisząc C ++, na moich zajęciach piszę trywialny test, który tworzy instancję obiektu i sprawdza niezmienniki (przypisywalne, zwykłe itp.). Zaskakujące było to, ile razy test ten był przerywany podczas programowania (np. Przez pomyłkę dodanie do klasy niemożliwego do przeniesienia członka).

utnapistim
źródło
1

Myślę, że twoje pytanie sprowadza się do:

Jak mogę przetestować funkcję typu void bez testowania integracji?

Jeśli zmienimy funkcję pieczenia plików cookie, aby na przykład zwracać pliki cookie, natychmiast stanie się oczywiste, jaki powinien być test.

Jeśli musimy wywołać pan.GetCookies po wywołaniu funkcji, możemy jednak zapytać, czy jest to „test integracji”, czy „ale czy nie testujemy tylko obiektu panoramowania?”.

Myślę, że masz rację pod tym względem, że przeprowadzanie testów jednostkowych z wyśmiewanym wszystkim i sprawdzanie funkcji xy i z nazwano brakiem wartości.

Ale! Argumentowałbym, że w takim przypadku powinieneś zrefaktoryzować swoje nieważne funkcje, aby zwrócić testowalny wynik LUB użyć rzeczywistych obiektów i wykonać test integracji

--- Aktualizacja dla przykładu createNewUser

  • w bazie danych należy utworzyć nowy rekord użytkownika
  • powitalna wiadomość e-mail musi zostać wysłana
  • adres IP użytkownika musi zostać zarejestrowany w celu oszustwa.

OK, więc tym razem wynik funkcji nie jest łatwo zwracany. Chcemy zmienić stan parametrów.

W tym miejscu jestem nieco kontrowersyjny. Tworzę konkretne symulacje dla parametrów stanowych

proszę, drodzy czytelnicy, spróbujcie opanować swój gniew!

więc...

var validatedUserData = new UserData(); //we can use the real object for this
var emailService = new MockEmailService(); //a simple mock which saves sentEmails to a List<string>
var dataStore = new MockDataStore(); //a simple mock which saves ips to a List<string>

//run the test
target.createNewUser(validatedUserData, emailService, dataStore);

//check the results
Assert.AreEqual(1, emailService.EmailsSent.Count());
Assert.AreEqual(1, dataStore.IpsRecorded.Count());
Assert.AreEqual(1, dataStore.UsersSaved.Count());

Oddziela to szczegóły implementacji testowanej metody od pożądanego zachowania. Alternatywne wdrożenie:

function createNewUser(validatedUserData, emailService, dataStore) {
  userId = dataStore.bulkInsedrtUserRecords(new [] {validateduserData});
  emailService.addEmailToQueue(validatedUserData);
  emailService.ProcessQueue();
  dataStore.recordIpAddress(userId, validatedUserData.ip);
}

Nadal przejdzie test jednostkowy. Ponadto masz tę zaletę, że możesz ponownie użyć próbnych obiektów w testach, a także wstrzyknąć je do aplikacji na potrzeby interfejsu użytkownika lub testów integracyjnych.

Ewan
źródło
nie jest to test integracyjny tylko dlatego, że wspominasz nazwy 2 konkretnych klas ... testy integracyjne dotyczą testowania integracji z systemami zewnętrznymi, takimi jak dyskowe operacje we / wy, baza danych, zewnętrzne usługi sieciowe itd. wywoływanie pan.getCookies () jest w -pamięć, szybkość, sprawdzanie interesujących nas rzeczy itp. Zgadzam się, że metoda zwracania plików cookie wydaje się być lepszym projektem.
sara,
3
Czekać. Z tego, co wiemy, pan.getcookies wysyła wiadomość e-mail do kucharza z prośbą o wyjęcie ciasteczek z piekarnika, gdy tylko będą mieli okazję
Ewan
Myślę, że teoretycznie jest to możliwe, ale byłoby to dość mylące imię. kto kiedykolwiek słyszał o urządzeniach piekarniczych, które wysyłały e-maile? ale widzę twój punkt, to zależy. Zakładam, że te klasy współpracujące są obiektami liścia lub zwykłymi rzeczami w pamięci, ale jeśli robią podstępne rzeczy, konieczna jest ostrożność. Myślę jednak, że wysyłanie e-maili zdecydowanie powinno odbywać się na wyższym poziomie niż ten. wydaje się to nieudolną i nieprzyzwoitą logiką biznesową podmiotów.
sara
2
Było to pytanie retoryczne, ale: „kto kiedykolwiek słyszał o sprzęcie piekarnika, który wysyłał e-maile?” venturebeat.com/2016/03/08/…
clacke
Cześć Ewan, myślę, że ta odpowiedź jest jak najbliżej tego, o co naprawdę pytam. Myślę, że twoja uwaga na temat bakeCookieszwracania upieczonych ciasteczek jest na miejscu, a po opublikowaniu tego pomyślałem. Myślę więc, że to jeszcze nie jest świetny przykład. Dodałem jeszcze jedną aktualizację, która, mam nadzieję, stanowi bardziej realistyczny przykład motywacji mojego pytania. Doceniam twój wkład.
Jonah
0

Powinieneś także przetestować bakeCookies- co przyniosłoby / powinno e..g bakeCookies(egg, pan, oven)? Jajko sadzone czy wyjątek? Na własną rękę, ani panteż nie ovenbędą dbać o rzeczywistych składników, ponieważ żaden z nich nie mają, ale bakeCookiespowinny zazwyczaj wydajność ciasteczek. Bardziej ogólnie może to zależeć od tego, jak doughuzyskuje się i czy jest jakaś szansa, że staje się zwykłym egglub np waterzamiast.

Tobias Kienzler
źródło