o ile rozumiem, większość ludzi zgadza się, że prywatne metody nie powinny być testowane bezpośrednio, ale raczej za pomocą jakichkolwiek publicznych metod, które je nazywają. Rozumiem ich punkt widzenia, ale mam z tym pewne problemy, gdy próbuję postępować zgodnie z „Trzema prawami TDD” i stosować cykl „Czerwony - zielony - refaktor”. Myślę, że najlepiej to wyjaśnić przykładem:
W tej chwili potrzebuję programu, który może odczytać plik (zawierający dane rozdzielone tabulatorami) i odfiltrować wszystkie kolumny zawierające dane nienumeryczne. Sądzę, że prawdopodobnie są już dostępne proste narzędzia, ale sam zdecydowałem się na wdrożenie od podstaw, głównie dlatego, że pomyślałem, że może to być fajny i czysty projekt, aby uzyskać trochę praktyki z TDD.
Po pierwsze „zakładam czerwony kapelusz”, czyli potrzebuję testu, który się nie powiedzie. Pomyślałem, że potrzebuję metody, która znajdzie wszystkie pola nienumeryczne w linii. Więc piszę prosty test, oczywiście, że nie kompiluje się od razu, więc zaczynam pisać samą funkcję, a po kilku cyklach tam iz powrotem (czerwony / zielony) mam działającą funkcję i kompletny test.
Następnie kontynuuję funkcję „gatherNonNumericColumns”, która odczytuje plik, jeden wiersz na raz, i wywołuje moją funkcję „findNonNumericFields” w każdym wierszu, aby zebrać wszystkie kolumny, które ostatecznie muszą zostać usunięte. Kilka cykli czerwono-zielonych, i skończyłem, mając ponownie działającą funkcję i pełny test.
Teraz myślę, że powinienem dokonać refaktoryzacji. Ponieważ moja metoda „findNonNumericFields” została zaprojektowana tylko dlatego, że doszedłem do wniosku, że będę jej potrzebować podczas implementowania „gatherNonNumericColumns”, wydaje mi się, że rozsądne byłoby pozostawienie „findNonNumericFields” prywatnemu. To jednak przerwałoby moje pierwsze testy, ponieważ nie miałyby one już dostępu do metody, którą testowały.
W rezultacie otrzymuję prywatne metody i zestaw testów, które to sprawdzają. Ponieważ tak wiele osób doradza, że nie należy testować prywatnych metod, wydaje mi się, że zamalowałem się tutaj w kącie. Ale gdzie dokładnie zawiodłem?
Rozumiem, że mogłem zacząć na wyższym poziomie, pisząc test, który testuje, co ostatecznie stanie się moją publiczną metodą (to znaczy findAndFilterOutAllNonNumericalColumns), ale wydaje mi się to sprzeczne z całym punktem TDD (przynajmniej według wuja Boba) : Należy stale przełączać się między pisaniem testów a kodem produkcyjnym oraz, że w dowolnym momencie wszystkie testy zadziałały w ciągu ostatniej minuty. Ponieważ jeśli zacznę od napisania testu dla metody publicznej, minie kilka minut (lub godzin, a nawet dni w bardzo skomplikowanych przypadkach), zanim uzyskam wszystkie szczegóły w metodach prywatnych, aby test mógł przetestować metodę publiczną metoda mija.
Co więc robić? Czy TDD (z szybkim cyklem refaktoryzacji czerwony-zielony) po prostu nie jest zgodny z metodami prywatnymi? Czy jest to błąd w moim projekcie?
źródło
private
jakby miało to sens.Odpowiedzi:
Jednostki
Myślę, że mogę dokładnie wskazać, gdzie zaczął się problem:
Po tym należy natychmiast zadać sobie pytanie: „Czy będzie to osobna jednostka do testowania,
gatherNonNumericColumns
czy część tego samego?”Jeśli odpowiedź brzmi „ tak, osobno ”, wówczas twój sposób działania jest prosty: metoda ta musi być publiczna w odpowiedniej klasie, aby można ją było przetestować jako całość. Twoja mentalność przypomina: „Muszę przetestować jedną metodę, a także przetestować inną metodę”
Z tego, co mówisz, doszedłeś jednak do wniosku, że odpowiedź brzmi „ nie, część tego samego ”. W tym momencie twój plan nie powinien już obejmować pełnego pisania i testowania,
findNonNumericFields
a następnie pisaniagatherNonNumericColumns
. Zamiast tego powinno być po prostu pisaćgatherNonNumericColumns
. Na raziefindNonNumericFields
powinien być po prostu prawdopodobną częścią miejsca docelowego, o którym myślisz, wybierając następną czerwoną skrzynkę testową i dokonując refaktoryzacji. Tym razem twoja mentalność brzmi: „Muszę przetestować jedną metodę, a robiąc to, powinienem pamiętać, że moja ukończona implementacja prawdopodobnie będzie zawierać tę drugą metodę”.Utrzymanie krótkiego cyklu
Wykonanie powyższego nie powinno prowadzić do problemów, które opisujesz w przedostatnim akapicie:
W żadnym momencie ta technika nie wymaga napisania czerwonego testu, który zmieni kolor na zielony tylko po wdrożeniu całości
findNonNumericFields
od zera. O wiele bardziej prawdopodobne,findNonNumericFields
że zacznie się jako część kodu w publicznej metodzie, którą testujesz, która zostanie zbudowana w ciągu kilku cykli i ostatecznie wyodrębniona podczas refaktoryzacji.Mapa drogowa
Aby podać przybliżoną mapę drogową dla tego konkretnego przykładu, nie znam dokładnych przypadków testowych, z których korzystałeś, ale mówię, że pisałeś
gatherNonNumericColumns
jako metodę publiczną. Wtedy najprawdopodobniej przypadki testowe byłyby takie same jak te, dla których napisałeśfindNonNumericFields
, każdy z nich korzystałby z tabeli zawierającej tylko jeden wiersz. Kiedy ten jednorzędowy scenariusz został w pełni zaimplementowany i chciałeś napisać test, aby zmusić cię do wyodrębnienia metody, napiszesz dwurzędowy przypadek, który wymagałby dodania iteracji.źródło
Wiele osób uważa, że testy jednostkowe są oparte na metodach; to nie jest. Powinien być oparty na najmniejszej jednostce, która ma sens. W większości przypadków oznacza to, że klasa powinna być testowana jako całość. Nie poszczególne metody.
Teraz oczywiście będziesz wywoływał metody w klasie, ale powinieneś pomyśleć o testach jako o zastosowaniu do obiektu czarnej skrzynki, który masz, więc powinieneś być w stanie zobaczyć, że niezależnie od logicznych operacji, które zapewnia twoja klasa; to są rzeczy, które musisz przetestować. Jeśli twoja klasa jest tak duża, że operacja logiczna jest zbyt złożona, masz problem projektowy, który należy rozwiązać w pierwszej kolejności.
Klasa z tysiącem metod może wydawać się testowalna, ale jeśli testujesz każdą metodę osobno, tak naprawdę nie testujesz tej klasy. Niektóre klasy mogą wymagać określonego stanu przed wywołaniem metody, na przykład klasa sieci, która wymaga nawiązania połączenia przed wysłaniem danych. Metoda wysyłania danych nie może być rozpatrywana niezależnie od całej klasy.
Powinieneś więc zauważyć, że prywatne metody nie mają znaczenia dla testowania. Jeśli nie możesz ćwiczyć prywatnych metod przez wywołanie publicznego interfejsu klasy, wówczas te prywatne metody są bezużyteczne i i tak nie będą używane.
Myślę, że wiele osób próbuje przekształcić prywatne metody w testowalne jednostki, ponieważ wydaje się, że ich testowanie jest łatwe, ale przesadza to zbyt daleko. Martin Fowler mówi
co ma sens w przypadku systemu obiektowego, ponieważ obiekty są zaprojektowane jako jednostki. Jeśli chcesz przetestować poszczególne metody, być może powinieneś stworzyć system proceduralny, taki jak C, lub klasę składającą się wyłącznie z funkcji statycznych.
źródło
Fakt, że twoje metody gromadzenia danych są wystarczająco złożone, aby zasługiwać na testy i wystarczająco oddzielić się od twojego głównego celu, aby być metodami własnymi, a nie częścią niektórych punktów pętli rozwiązania: uczyń te metody nie prywatnymi, ale członkami innej klasy który zapewnia funkcje gromadzenia / filtrowania / tabelowania.
Następnie piszesz testy dla głupich aspektów klasy pomocniczej (np. „Rozróżnianie liczb od znaków”) w jednym miejscu, a testy dla twojego głównego celu (np. „Uzyskanie wyników sprzedaży”) w innym miejscu, a ty nie nie będziesz musiał powtarzać podstawowych testów filtrowania w testach dla normalnej logiki biznesowej.
Ogólnie rzecz biorąc, jeśli twoja klasa, która wykonuje jedną rzecz, zawiera obszerny kod do wykonywania innej czynności, która jest wymagana, ale odrębna od jej podstawowego celu, kod ten powinien żyć w innej klasie i być wywoływany za pomocą metod publicznych. Nie powinien być ukryty w prywatnych zakątkach klasy, która tylko przypadkowo zawiera ten kod. Zwiększa to jednocześnie testowalność i zrozumiałość.
źródło
Osobiście uważam, że poszedłeś daleko w kierunku myślenia o implementacji, kiedy pisałeś testy. Państwo założyć musisz pewnych metod. Ale czy naprawdę potrzebujesz ich do robienia tego, co klasa ma robić? Czy klasa poniosłaby porażkę, gdyby ktoś przyszedł i dokonał ich wewnętrznej renowacji? Jeśli korzystałeś z tej klasy (i moim zdaniem powinien to być sposób myślenia testera), możesz naprawdę mniej się przejmować, jeśli istnieje wyraźna metoda sprawdzania liczb.
Powinieneś przetestować publiczny interfejs klasy. Prywatne wdrożenie jest prywatne z jakiegoś powodu. Nie jest to część publicznego interfejsu, ponieważ nie jest potrzebny i można go zmienić. To szczegół implementacji.
Jeśli napiszesz testy na interfejsie publicznym, nigdy nie dostaniesz problemu, na który natrafiłeś. Albo możesz utworzyć przypadki testowe dla interfejsu publicznego, które obejmują twoje prywatne metody (świetnie), albo nie możesz. W takim przypadku nadszedł czas, aby zastanowić się poważnie nad metodami prywatnymi i być może zeskrobać je całkowicie, jeśli i tak nie będzie można do nich dotrzeć.
źródło
Nie wykonujesz TDD w oparciu o to, czego oczekujesz, że klasa zrobi wewnętrznie.
Twoje przypadki testowe powinny być oparte na tym, co klasa / funkcjonalność / program ma do czynienia ze światem zewnętrznym. W twoim przykładzie, czy użytkownik będzie kiedykolwiek dzwonił do klasy czytelnika z
find all the non-numerical fields in a line?
Jeśli odpowiedź brzmi „nie”, to w pierwszej kolejności jest to zły test. Chcesz napisać test na funkcjonalność na poziomie klasy / interfejsu - a nie na poziomie „co będzie musiała zaimplementować metoda, aby to zadziałało”, czyli test.
Przepływ TDD wynosi:
NIE ma to robić „ponieważ będę potrzebował X w przyszłości jako metody prywatnej, pozwól mi go wdrożyć i przetestować najpierw”. Jeśli to robisz, robisz etap „czerwony” niepoprawnie. To wydaje się być twoim problemem tutaj.
Jeśli często piszesz testy dla metod, które stają się metodami prywatnymi, robisz jedną z kilku rzeczy:
źródło
Występuje powszechne nieporozumienie związane z testowaniem w ogóle.
Większość osób, które dopiero zaczynają testowanie, zaczyna myśleć w ten sposób:
i tak dalej.
Problem polega na tym, że w rzeczywistości nie masz testu jednostkowego dla funkcji H. Test, który ma przetestować H, faktycznie testuje jednocześnie H, G i F.
Aby rozwiązać ten problem, musisz zdać sobie sprawę, że testowalne jednostki nigdy nie mogą zależeć od siebie, a raczej od ich interfejsów . W twoim przypadku, gdy jednostki są prostymi funkcjami, interfejsy są tylko ich sygnaturami połączeń. Musisz zatem zaimplementować G w taki sposób, aby można go było używać z dowolną funkcją mającą taki sam podpis jak F.
To, jak dokładnie można to zrobić, zależy od języka programowania. W wielu językach możesz przekazywać funkcje (lub wskaźniki do nich) jako argumenty dla innych funkcji. Umożliwi to przetestowanie każdej funkcji w izolacji.
źródło
Testy, które piszesz podczas Test Driven Development, mają zapewnić, że klasa poprawnie implementuje swój publiczny interfejs API, jednocześnie upewniając się, że ten publiczny interfejs API jest łatwy do przetestowania i użycia.
Możesz za wszelką cenę użyć prywatnych metod do implementacji tego API, ale nie ma potrzeby tworzenia testów poprzez TDD - funkcjonalność zostanie przetestowana, ponieważ publiczny API będzie działał poprawnie.
Załóżmy teraz, że twoje prywatne metody są na tyle skomplikowane, że zasługują na samodzielne testy - ale nie mają sensu jako część publicznego interfejsu API twojej klasy. Cóż, prawdopodobnie oznacza to, że powinny to być publiczne metody innej klasy - takiej, z której korzysta Twoja oryginalna klasa w swojej własnej implementacji.
Testując tylko publiczny interfejs API, znacznie ułatwisz modyfikowanie szczegółów implementacji w przyszłości. Niepomocne testy będą Cię drażnić dopiero później, gdy będą musiały zostać przepisane, aby obsługiwać eleganckie refaktoryzowanie, które właśnie odkryłeś.
źródło
Myślę, że właściwą odpowiedzią jest wniosek, do którego doszedłeś, rozpoczynając od metod publicznych. Zacząłbyś od napisania testu, który wywołuje tę metodę. Nie powiedzie się, więc utworzysz metodę o tej nazwie, która nic nie robi. Być może masz rację w teście, który sprawdza wartość zwracaną.
(Nie jestem do końca jasne, co robi twoja funkcja. Czy zwraca ciąg z zawartością pliku z usuniętymi wartościami nienumerycznymi?)
Jeśli metoda zwraca ciąg znaków, sprawdzasz tę zwracaną wartość. Więc po prostu dalej go budujesz.
Myślę, że wszystko, co dzieje się w metodzie prywatnej, powinno być w metodzie publicznej w pewnym momencie procesu, a następnie zostać przeniesione do metody prywatnej tylko w ramach etapu refaktoryzacji. O ile mi wiadomo, refaktoryzacja nie wymaga testów negatywnych. Potrzebujesz tylko nieudanych testów podczas dodawania funkcjonalności. Musisz tylko przeprowadzić testy po refaktorze, aby upewnić się, że wszystkie pomyślnie przejdą.
źródło
Jest stara przysłowie.
Wydaje się, że ludzie myślą, że kiedy TDD, po prostu usiądziesz, napiszesz testy, a projekt zacznie się magicznie. To nie jest prawda. Musisz mieć plan wysokiego poziomu. Odkryłem, że najlepsze wyniki uzyskuję od TDD, kiedy najpierw projektuję interfejs (publiczne API). Osobiście tworzę rzeczywisty,
interface
który definiuje klasę jako pierwszy.dyszę Napisałem trochę „kodu” przed napisaniem jakichkolwiek testów! Więc nie. Ja nie. Napisałem umowę, której należy przestrzegać, projekt . Podejrzewam, że podobne wyniki można uzyskać, zapisując diagram UML na papierze milimetrowym. Chodzi o to, że musisz mieć plan. TDD nie jest licencją na złośliwe hakowanie fragmentu kodu.
Naprawdę czuję, że „Test First” to myląca nazwa. Pierwszy projekt następnie Test.
Oczywiście, postępuj zgodnie z radami udzielonymi przez innych na temat wydobywania kolejnych klas z twojego kodu. Jeśli zdecydowanie odczuwasz potrzebę przetestowania elementów wewnętrznych klasy, wyodrębnij te elementy wewnętrzne do łatwo testowanej jednostki i wstrzyknij je.
źródło
Pamiętaj, że testy można również refaktoryzować! Jeśli ustawisz metodę jako prywatną, zmniejszysz publiczny interfejs API, a zatem całkowicie dopuszczalne jest odrzucenie niektórych odpowiednich testów dla tej „utraconej funkcjonalności” (zmniejszona złożoność AKA).
Inni twierdzą, że twoja prywatna metoda zostanie wywołana jako część innych testów API lub będzie nieosiągalna, a zatem możliwa do usunięcia. W rzeczywistości rzeczy są bardziej szczegółowe, jeśli myślimy o ścieżkach wykonania .
Na przykład, jeśli mamy metodę publiczną, która wykonuje podział, możemy chcieć przetestować ścieżkę, która daje podział według zera. Jeśli ustawimy metodę na prywatną, otrzymamy wybór: albo możemy rozważyć ścieżkę dzielenia przez zero, albo możemy wyeliminować tę ścieżkę, biorąc pod uwagę, jak jest wywoływana przez inne metody.
W ten sposób możemy wyrzucić niektóre testy (np. Podzielić przez zero) i przefakturować inne pod względem pozostałego publicznego API. Oczywiście w idealnym świecie istniejące testy zajmują się wszystkimi pozostałymi ścieżkami, ale rzeczywistość jest zawsze kompromisem;)
źródło
Są chwile, kiedy prywatna metoda może być publiczną metodą innej klasy.
Na przykład możesz mieć prywatne metody, które nie są bezpieczne dla wątków i pozostawić klasę w stanie tymczasowym. Metody te można przenieść do osobnej klasy, która jest prowadzona prywatnie przez pierwszą klasę. Więc jeśli twoja klasa jest kolejką, możesz mieć klasę InternalQueue, która ma metody publiczne, a klasa kolejki prywatnie utrzymuje instancję InternalQueue. Pozwala to przetestować kolejkę wewnętrzną, a także wyjaśnia, jakie są poszczególne operacje na kolejce wewnętrznej.
(Jest to najbardziej oczywiste, gdy wyobrażasz sobie, że nie ma klasy List i jeśli próbowałeś zaimplementować funkcje List jako prywatne metody w klasie, która ich używa).
źródło
Zastanawiam się, dlaczego twój język ma tylko dwa poziomy prywatności, w pełni publiczny i całkowicie prywatny.
Czy możesz ustalić, że twoje niepubliczne metody są dostępne dla pakietów lub coś w tym rodzaju? Następnie umieść testy w tym samym pakiecie i ciesz się testowaniem wewnętrznych mechanizmów, które nie są częścią publicznego interfejsu. Twój system kompilacji wykluczy testy podczas budowania wersji binarnej wydania.
Oczywiście czasami trzeba mieć naprawdę prywatne metody, niedostępne tylko dla klasy definiującej. Mam nadzieję, że wszystkie takie metody są bardzo małe. Ogólnie rzecz biorąc, utrzymanie małych metod (np. Poniżej 20 wierszy) bardzo pomaga: testowanie, konserwacja i samo zrozumienie kodu staje się łatwiejsze.
źródło
Czasami mam do czynienia z prywatnymi metodami, które mają być chronione, aby umożliwić bardziej szczegółowe testy (ściślejsze niż udostępniony publiczny interfejs API). Powinien to być (mam nadzieję, że bardzo rzadki) wyjątek, a nie reguła, ale może być pomocny w określonych przypadkach, z którymi możesz się spotkać. Jest to również coś, czego w ogóle nie należy brać pod uwagę przy tworzeniu publicznego interfejsu API, więcej „oszukiwania”, które można wykorzystać w oprogramowaniu do użytku wewnętrznego w tych rzadkich sytuacjach.
źródło
Doświadczyłem tego i poczułem twój ból.
Moje rozwiązanie polegało na:
przestań traktować testy jak budowanie monolitu.
Pamiętaj, że kiedy napisałeś zestaw testów, powiedzmy 5, aby wypróbować jakąś funkcjonalność, nie musisz trzymać tych wszystkich testów , zwłaszcza gdy staje się to częścią czegoś innego.
Na przykład często mam:
więc mam
Jeśli jednak dodam teraz funkcje wywołujące wyższy poziom, które go wywołują i mają wiele testów, być może uda mi się teraz zmniejszyć liczbę testów niskiego poziomu, tak aby były:
Diabeł tkwi w szczegółach, a możliwość zrobienia tego będzie zależeć od okoliczności.
źródło
Czy słońce krąży wokół Ziemi, czy Ziemia wokół Słońca? Według Einsteina odpowiedź brzmi tak, lub oba, ponieważ oba modele różnią się tylko pod względem punktu widzenia, podobnie enkapsulacja i rozwój oparty na testach są tylko w konflikcie, ponieważ naszym zdaniem są. Siedzimy tutaj jak Galileusz i papież, rzucając sobie nawzajem obelgi: głupcze, czy nie widzisz, że prywatne metody również wymagają testowania; heretyku, nie przerywaj enkapsulacji! Podobnie, gdy uznamy, że prawda jest większa, niż myśleliśmy, możemy spróbować czegoś takiego jak podsumowanie testów dla prywatnych interfejsów, aby testy dla publicznych interfejsów nie przerywały enkapsulacji.
Spróbuj tego: dodaj dwie metody: jedna, która nie ma danych wejściowych, tylko zwraca liczbę prywatnych testów, a druga przyjmuje numer testu jako parametr i zwraca pozytywny / negatywny wynik.
źródło