Czy kod do testowania jest lepszym kodem?


103

Staram się przyzwyczaić do regularnego pisania testów jednostkowych za pomocą mojego kodu, ale najpierw przeczytałem, że ważne jest, aby napisać testowalny kod . To pytanie dotyczy SOLIDNYCH zasad pisania kodu testowalnego, ale chcę wiedzieć, czy te zasady projektowania są korzystne (a przynajmniej nie szkodliwe), nie planując w ogóle pisania testów. Aby wyjaśnić - rozumiem znaczenie pisania testów; nie jest to pytanie o ich przydatność.

Aby zilustrować moje zamieszanie, w artykule, który zainspirował to pytanie, pisarz podaje przykład funkcji, która sprawdza aktualny czas i zwraca pewną wartość w zależności od czasu. Autor wskazuje na to jako zły kod, ponieważ produkuje dane (czas), które wykorzystuje wewnętrznie, co utrudnia testowanie. Wydaje mi się jednak, że przesadzanie z czasem jest argumentem. W pewnym momencie wartość należy zainicjować, a dlaczego nie najbliżej konsumpcji? Ponadto moim zdaniem celem tej metody jest zwrócenie pewnej wartości w oparciu o bieżący czas , czyniąc z tego parametr, który sugerujesz, że ten cel można / należy zmienić. To i inne pytania prowadzą mnie do zastanowienia się, czy testowalny kod był synonimem „lepszego” kodu.

Czy pisanie testowalnego kodu jest nadal dobrą praktyką nawet przy braku testów?


Czy kod do testowania jest rzeczywiście bardziej stabilny? został zaproponowany jako duplikat. To pytanie dotyczy jednak „stabilności” kodu, ale szerzej pytam o to, czy kod jest lepszy z innych powodów, takich jak czytelność, wydajność, łączenie itd.


24
Istnieje specjalna właściwość funkcji, która wymaga przejścia w czasie zwanym idempotency. Taka funkcja będzie generować ten sam wynik za każdym razem, gdy zostanie wywołana z określoną wartością argumentu, co nie tylko sprawia, że ​​jest ona bardziej testowalna, ale również łatwiejsza do opanowania i łatwiejsze do uzasadnienia.
Robert Harvey

4
Czy potrafisz zdefiniować „lepszy kod”? masz na myśli „konserwowalny” ?, „łatwiejszy w użyciu-bez-IOC-kontener-magia”?
k3b

7
Wydaje mi się, że test nigdy nie zakończył się niepowodzeniem, ponieważ wykorzystał rzeczywisty czas systemowy, a następnie przesunięcie strefy czasowej uległo zmianie.
Andy

5
To jest lepsze niż nieczytelny kod.
Tulains Córdova

14
@RobertHarvey Nie nazwałbym tego idempotentność, powiedziałbym, że to więzy przezroczystości : jeśli func(X)wróci "Morning", to zastąpienie wszystkich wystąpień func(X)ze "Morning"nie zmieni programu (czyli powołanie. funcNie coś innego niż Zwraca wartość zrobić). Idempotencja oznacza, że ​​albo func(func(X)) == X(co nie jest poprawne pod względem typu), albo func(X); func(X);wywołuje takie same skutki uboczne jak func(X)(ale tutaj nie ma żadnych skutków ubocznych)
Warbo

Odpowiedzi:


116

W odniesieniu do wspólnej definicji testów jednostkowych powiedziałbym, że nie. Widziałem prosty kod zawikłany z powodu potrzeby jego zmiany w celu dopasowania do frameworka testowania (np. Interfejsy i IoC wszędzie utrudniają śledzenie warstw wywołań interfejsów i danych, które powinny być oczywiste przekazywane magią). Biorąc pod uwagę wybór między kodem łatwym do zrozumienia lub kodem łatwym do testowania jednostkowego, zawsze korzystam z kodu, który można utrzymać.

Nie oznacza to, że nie należy testować, ale dopasować narzędzia do własnych potrzeb, a nie na odwrót. Istnieją inne sposoby testowania (ale trudny do zrozumienia kod to zawsze zły kod). Na przykład, możesz stworzyć testy jednostkowe, które są mniej szczegółowe (np. Podejście Martina Fowlera , że jednostka jest ogólnie klasą, a nie metodą), lub możesz zamiast tego trafić swój program za pomocą automatycznych testów integracyjnych. To może nie być tak ładne, jak twoja platforma testowa świeci zielonymi paskami, ale jesteśmy po przetestowanym kodzie, a nie gamifikacji procesu, prawda?

Możesz sprawić, by kod był łatwy w utrzymaniu i nadal był dobry do testów jednostkowych, definiując dobre interfejsy między nimi, a następnie pisząc testy wykorzystujące publiczny interfejs komponentu; lub możesz uzyskać lepszą platformę testową (taką, która zastępuje funkcje w czasie wykonywania, aby się z nich kpić, zamiast wymagać kompilacji kodu z wprowadzonymi próbkami). Lepsza struktura testów jednostkowych pozwala zastąpić systemową funkcję GetCurrentTime () własną, w czasie wykonywania, więc nie trzeba wprowadzać do tego sztucznych opakowań tylko w celu dopasowania do narzędzia testowego.


3
Komentarze nie są przeznaczone do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
Inżynier światowy

2
Myślę, że warto zauważyć, że znam przynajmniej jeden język, który pozwala ci robić to, co opisuje twój ostatni akapit: Python with Mock. Ze względu na sposób importowania modułów prawie wszystko oprócz słów kluczowych można zastąpić próbnym, nawet standardowymi metodami / klasami API / itp. Jest to możliwe, ale może wymagać zaprojektowania języka w sposób zapewniający taką elastyczność.
jpmc26

5
Wydaje mi się, że istnieje różnica między „testowalnym kodem” a „kodem [skręconym] w celu dopasowania do środowiska testowego”. Nie jestem pewien, dokąd zmierzam z tym komentarzem, poza stwierdzeniem, że zgadzam się, że „przekręcony” kod jest zły, a „testowalny” kod z dobrymi interfejsami jest dobry.
Bryan Oakley,

2
Wyraziłem swoje przemyślenia w komentarzach do artykułu (ponieważ nie są tu dozwolone komentarze rozszerzone), sprawdź to! Dla jasności: jestem autorem wspomnianego artykułu :)
Sergey Kolodiy

Muszę się zgodzić z @BryanOakley. „Testowany kod” sugeruje, że Twoje obawy są rozdzielone: ​​możliwe jest przetestowanie aspektu (modułu) bez ingerencji z innych aspektów. Powiedziałbym, że różni się to od „dostosowywania konwencji testowania specyficznych dla obsługi projektu”. Jest to podobne do wzorców projektowych: nie należy ich wymuszać. Kod, który właściwie wykorzystuje wzorce projektowe, byłby uważany za silny kod. To samo dotyczy zasad testowania. Jeśli uczynienie kodu „testowalnym” powoduje nadmierne skręcenie kodu projektu, robisz coś złego.
Vince Emigh,

68

Czy pisanie testowalnego kodu jest nadal dobrą praktyką nawet przy braku testów?

Po pierwsze, brak testów jest znacznie większym problemem niż testowanie kodu. Brak testów jednostkowych oznacza, że ​​nie skończyłeś z kodem / funkcją.

Poza tym nie powiedziałbym, że ważne jest pisanie testowalnego kodu - ważne jest pisanie elastycznego kodu. Nieelastyczny kod jest trudny do przetestowania, więc podejście do niego nakłada się na siebie i jak to nazywają ludzie.

Więc dla mnie zawsze jest zestaw priorytetów w pisaniu kodu:

  1. Spraw, by działał - jeśli kod nie działa tak, jak powinien, jest bezwartościowy.
  2. Uczyń go konserwowalnym - jeśli kodu nie da się utrzymać, szybko przestanie działać.
  3. Uelastycznij - jeśli kod nie jest elastyczny, przestanie działać, gdy biznes nieuchronnie nadejdzie i zapyta, czy kod może wykonać XYZ.
  4. Zrób to szybko - poza podstawowym akceptowalnym poziomem wydajność jest po prostu ciężka.

Testy jednostkowe pomagają w utrzymaniu kodu, ale tylko do pewnego momentu. Jeśli sprawisz, że kod będzie mniej czytelny lub bardziej kruchy, aby testy jednostkowe działały, staje się to nieproduktywne. „Testowalny kod” jest ogólnie elastycznym kodem, więc jest dobry, ale nie tak ważny jak funkcjonalność lub łatwość konserwacji. Dla czegoś takiego jak obecny czas, uelastycznianie jest dobre, ale szkodzi w utrzymywaniu, czyniąc kod trudniejszym w użyciu i bardziej złożonym. Ponieważ łatwość konserwacji jest ważniejsza, zwykle popełniam błąd w kierunku prostszego podejścia, nawet jeśli jest ono mniej sprawdzalne.


4
Podoba mi się związek, który wskazujesz między testowalnością a elastycznością - dzięki temu cały problem jest dla mnie bardziej zrozumiały. Elastyczność pozwala na dostosowanie kodu, ale niekoniecznie czyni go bardziej abstrakcyjnym i mniej intuicyjnym do zrozumienia, ale jest to warte poświęcenia dla korzyści.
WannabeCoder

3
to powiedziawszy, często widzę metody, które powinny być prywatne, zmuszane do publicznego lub poziomu pakietu, aby środowisko testów jednostkowych mogło uzyskać do nich bezpośredni dostęp. Daleko od idealnego podejścia.
jwenting

4
@WannabeCoder Oczywiście warto dodać elastyczność tylko wtedy, gdy oszczędza to czas. Dlatego nie piszemy każdej metody przeciwko interfejsowi - przez większość czasu po prostu łatwiej jest przepisać kod niż wprowadzać zbyt dużą elastyczność od samego początku. YAGNI wciąż jest niezwykle potężną zasadą - upewnij się tylko, że cokolwiek byś nie potrzebował, dodanie tego z mocą wsteczną nie da średnio więcej pracy niż wdrożenie go z wyprzedzeniem. Z mojego doświadczenia wynika, że kod niezgodny z YAGNI ma najwięcej problemów z elastycznością.
Luaan,

3
„Brak testów jednostkowych oznacza, że ​​nie ukończyłeś już kodu / funkcji” - nieprawda. „Definicja ukończenia” jest czymś, o czym decyduje zespół. Może, ale nie musi, obejmować pewien stopień pokrycia testowego. Ale nigdzie nie ma ścisłego wymogu, który mówi, że funkcji nie można „wykonać”, jeśli nie ma na nią żadnych testów. Zespół może zdecydować o wymaganiu testów lub nie.
aroth

3
@Telastyn W ciągu ponad 10 lat rozwoju nigdy nie miałem zespołu, który zleciłby platformę testów jednostkowych, a tylko dwa, które nawet miały jeden (oba miały słabe pokrycie). Jedno miejsce wymagało dokumentu słownego, w jaki sposób przetestować pisaną funkcję. Otóż ​​to. Może mam pecha? Nie jestem testem antyjednostkowym (poważnie, modyfikuję stronę SQA.SE, jestem bardzo pro testem jednostkowym!), Ale nie znalazłem ich tak rozpowszechnionych, jak twierdzi twoje oświadczenie.
corsiKa

50

Tak, to dobra praktyka. Powodem jest to, że testowalność nie służy wyłącznie testom. Ze względu na jasność i zrozumiałość niesie ze sobą.

Nikt nie dba o same testy. To smutny fakt, że potrzebujemy dużych zestawów testów regresji, ponieważ nie jesteśmy wystarczająco błyskotliwi, aby napisać idealny kod bez ciągłego sprawdzania naszych podstaw. Gdybyśmy mogli, koncepcja testów byłaby nieznana, a wszystko to nie stanowiłoby problemu. Z pewnością chciałbym móc. Ale doświadczenie pokazuje, że prawie wszyscy nie potrafimy, dlatego testy obejmujące nasz kod są dobre, nawet jeśli zabierają czas na pisanie kodu biznesowego.

W jaki sposób przeprowadzanie testów poprawia nasz kod biznesowy niezależnie od samego testu? Zmuszając nas do podzielenia naszej funkcjonalności na jednostki, które można łatwo wykazać, że są prawidłowe. Jednostki te są również łatwiejsze do zrobienia niż te, które w innym przypadku chcielibyśmy napisać.

Twój przykład czasu jest dobrym punktem. Tak długo, jak masz tylko funkcję zwracającą bieżący czas, możesz myśleć, że nie ma sensu programować go. Jak trudno może być to dobrze? Ale nieuchronnie twój program użyje tej funkcji w innym kodzie i zdecydowanie chcesz przetestować ten kod w różnych warunkach, w tym w różnych momentach. Dlatego dobrym pomysłem jest możliwość manipulowania czasem powrotu funkcji - nie dlatego, że nie ufasz swojemu połączeniu z jedną linią currentMillis(), ale dlatego, że musisz zweryfikować osoby wywołujące to połączenie w kontrolowanych okolicznościach. Widzicie więc, że testowanie kodu jest przydatne, nawet jeśli samo w sobie, nie zasługuje na tak wiele uwagi.


Innym przykładem jest, jeśli chcesz wyciągnąć część kodu jednego projektu w inne miejsce (z dowolnego powodu). Im bardziej różne części funkcjonalności są od siebie niezależne, tym łatwiej jest wydobyć dokładnie taką funkcjonalność, jakiej potrzebujesz i nic więcej.
valenterry

10
Nobody cares about the tests themselves-- Ja robię. Uważam, że testy są lepszą dokumentacją tego, co robi kod niż jakiekolwiek komentarze lub pliki readme.
jcollum

Od jakiegoś czasu powoli czytam o praktykach testowania (jakoś, kto w ogóle jeszcze nie przeprowadza testów jednostkowych) i muszę powiedzieć, że ostatnia część dotyczy weryfikacji połączenia w kontrolowanych okolicznościach i bardziej elastyczny kod, który pochodzi z to sprawiło, że wszystkie rzeczy kliknęły na swoje miejsce. Dziękuję Ci.
plast1k

12

W pewnym momencie wartość należy zainicjować, a dlaczego nie najbliżej konsumpcji?

Ponieważ może być konieczne ponowne użycie tego kodu z inną wartością niż wygenerowana wewnętrznie. Możliwość wstawienia wartości, której zamierzasz użyć jako parametru, gwarantuje, że możesz wygenerować te wartości w dowolnym momencie, a nie tylko „teraz” (z „teraz”, gdy wywołujesz kod).

Uczynienie kodu testowalnym w efekcie oznacza uczynienie kodu, który może (od samego początku) być używany w dwóch różnych scenariuszach (produkcyjnym i testowym).

Zasadniczo, chociaż można argumentować, że nie ma zachęty do testowania kodu w przypadku braku testów, pisanie kodu wielokrotnego użytku ma wielką zaletę, a oba są synonimami.

Ponadto, celem tej metody jest zwrócenie pewnej wartości na podstawie bieżącego czasu, czyniąc z tego parametr, który sugerujesz, że ten cel można / należy zmienić.

Można również argumentować, że celem tej metody jest zwrócenie pewnej wartości na podstawie wartości czasu i potrzebujesz jej do wygenerowania tej wartości na podstawie „teraz”. Jedna z nich jest bardziej elastyczna i jeśli przyzwyczaisz się do wyboru tego wariantu, z czasem zwiększy się szybkość ponownego użycia kodu.


10

To może wydawać się głupie, aby powiedzieć to w ten sposób, ale jeśli chcesz być w stanie przetestować swój kod, wtedy tak, pisanie testowalnego kodu jest lepsze. Ty pytasz:

W pewnym momencie wartość należy zainicjować, a dlaczego nie najbliżej konsumpcji?

Właśnie dlatego, że w przykładzie, do którego się odwołujesz, kod ten jest niestabilny. Chyba że uruchamiasz tylko podzbiór testów o różnych porach dnia. Lub resetujesz zegar systemowy. Lub jakieś inne obejście. Wszystkie są gorsze niż po prostu uelastycznienie kodu.

Oprócz tego, że ta mała metoda jest nieelastyczna, ma dwa obowiązki: (1) uzyskanie czasu systemowego, a następnie (2) zwrócenie na tej podstawie pewnej wartości.

public static string GetTimeOfDay()
{
    DateTime time = DateTime.Now;
    if (time.Hour >= 0 && time.Hour < 6)
    {
        return "Night";
    }
    if (time.Hour >= 6 && time.Hour < 12)
    {
        return "Morning";
    }
    if (time.Hour >= 12 && time.Hour < 18)
    {
        return "Afternoon";
    }
    return "Evening";
}

Sensowne jest dalsze dzielenie obowiązków, aby część poza kontrolą ( DateTime.Now) miała najmniejszy wpływ na resztę kodu. W ten sposób powyższy kod stanie się prostszy, a efektem ubocznym będzie systematyczne testowanie.


1
Musisz więc przetestować wcześnie rano, aby sprawdzić, czy otrzymujesz wynik „Nocy”, kiedy chcesz. To jest trudne. Załóżmy teraz, że chcesz sprawdzić, czy obsługa dat jest prawidłowa 29 lutego 2016 r. ... A niektórzy programiści iOS (i prawdopodobnie inni) są nękani błędem początkującego, który popsuwa wszystko na krótko przed lub po początku roku. przetestuj to. Z doświadczenia sprawdziłbym obsługę dat 2 lutego 2020 r.
gnasher729

1
@ gnasher729 Dokładnie mój punkt. „Uczynienie tego kodu testowalnym” to prosta zmiana, która może rozwiązać wiele (testowych) problemów. Jeśli nie chcesz zautomatyzować testowania, to przypuszczam, że kod jest możliwy do przereagowania w obecnej postaci. Ale byłoby lepiej, gdy będzie „testowalny”.
Eric King

9

Z pewnością ma to swój koszt, ale niektórzy programiści są tak przyzwyczajeni do płacenia, że ​​zapomnieli, że jest to koszt. Na przykład masz teraz dwie jednostki zamiast jednej, potrzebujesz kodu wywołującego, aby zainicjować i zarządzać dodatkową zależnością, i chociaż GetTimeOfDayjest bardziej testowalny, jesteś z powrotem w tej samej łodzi, testując nowy IDateTimeProvider. Po prostu jeśli masz dobre testy, korzyści zwykle przewyższają koszty.

Ponadto, do pewnego stopnia, pisanie testowalnego kodu zachęca do zaprojektowania kodu w sposób łatwiejszy w utrzymaniu. Nowy kod zarządzania zależnościami jest denerwujący, więc jeśli chcesz, zgrupuj wszystkie swoje funkcje zależne od czasu. Może to pomóc w złagodzeniu i naprawieniu błędów, takich jak na przykład ładowanie strony bezpośrednio na granicy czasu, gdy niektóre elementy są renderowane przy użyciu przedczasu, a niektóre przy użyciu po czasie. Może także przyspieszyć twój program, unikając powtarzających się wywołań systemowych w celu uzyskania aktualnego czasu.

Oczywiście te ulepszenia architektoniczne są w dużym stopniu zależne od tego, czy ktoś dostrzeże możliwości i je wykorzysta. Jednym z największych niebezpieczeństw skupiania się tak blisko na jednostkach jest utrata widoku z szerszego obrazu.

Wiele ram testów jednostkowych pozwala małpować łatać fałszywy obiekt w czasie wykonywania, co pozwala uzyskać korzyści z testowalności bez bałaganu. Widziałem to nawet w C ++. Spójrz na tę umiejętność w sytuacjach, w których wygląda na to, że koszt testowalności nie jest tego wart.


+1 - musisz poprawić projekt i architekturę, aby ułatwić pisanie testów jednostkowych.
BЈовић

3
+ - liczy się architektura Twojego kodu. Łatwiejsze testowanie to po prostu szczęśliwy efekt uboczny.
gbjbaanb

8

Możliwe, że nie każda cecha, która przyczynia się do testowalności, jest pożądana poza kontekstem testowalności - mam problem z wymyśleniem niezwiązanego z testem uzasadnienia na przykład cytowanego parametru czasu - ale ogólnie mówiąc, właściwości, które przyczyniają się do testowalności przyczyniają się również do dobrego kodu niezależnie od testowalności.

Mówiąc ogólnie, kod testowalny jest kodem ciągliwym. Jest w małych, dyskretnych, spójnych fragmentach, więc poszczególne bity można wezwać do ponownego użycia. Jest dobrze zorganizowany i dobrze nazwany (aby móc przetestować niektóre funkcje, większą wagę przywiązujesz do nazewnictwa; jeśli nie pisałeś testów, nazwa funkcji jednorazowego użytku miałaby mniejsze znaczenie). Zwykle jest bardziej parametryczny (jak twój przykład czasu), więc jest otwarty do użycia z innych kontekstów niż pierwotnie zamierzony cel. Jest SUCHY, więc mniej zagracony i łatwiejszy do zrozumienia.

Tak. Dobrą praktyką jest pisanie testowalnego kodu, nawet niezależnie od testowania.


nie zgadzam się co do tego, że jest to DRY - owijanie GetCurrentTime w metodzie MyGetCurrentTime bardzo często powtarza wywołanie systemu operacyjnego bez żadnej korzyści oprócz pomocy przy testowaniu narzędzi. To tylko najprostsze przykłady, w rzeczywistości stają się znacznie gorsze.
gbjbaanb

1
„repatowanie wywołania systemu operacyjnego bez korzyści” - dopóki nie skończysz na serwerze z jednym zegarem, rozmawiając z serwerem aws w innej strefie czasowej, a to złamie kod, a następnie będziesz musiał przejść cały kod i zaktualizuj go, aby używał MyGetCurrentTime, który zamiast tego zwraca UTC. ; przekrzywienie zegara, czas letni i istnieją inne powody, dla których ślepe zaufanie do połączenia z systemem operacyjnym może nie być dobrym pomysłem, lub przynajmniej mieć jeden punkt, w którym można oddać inny zamiennik.
Andrew Hill,

8

Pisanie testowalnego kodu jest ważne, jeśli chcesz być w stanie udowodnić, że Twój kod faktycznie działa.

Zwykle zgadzam się z negatywnymi odczuciami dotyczącymi wypaczania kodu w haniebne zniekształcenia, aby dopasować go do konkretnego środowiska testowego.

Z drugiej strony, wszyscy tutaj, w pewnym momencie, musieli poradzić sobie z tą magiczną funkcją o długości 1000 linii, która jest po prostu ohydna, z którą trzeba sobie poradzić, praktycznie nie można jej dotknąć bez złamania jednego lub więcej niejasnych, nie- oczywiste zależności gdzie indziej (lub gdzieś w samym sobie, gdzie zależność jest prawie niemożliwa do zwizualizowania) i jest z definicji praktycznie nie do przetestowania. Pojęcie (które nie jest bezwartościowe), że ramy testowe zostały przesadzone, nie powinno być uważane za darmową licencję na pisanie słabej jakości, nie sprawdzalnego kodu, moim zdaniem.

Na przykład oparte na testach ideały programistyczne często popychają cię do pisania procedur z jedną odpowiedzialnością, co jest zdecydowanie dobrą rzeczą. Osobiście mówię, że kupuj za jedną odpowiedzialność, jedno źródło prawdy, kontrolowany zakres (bez cholernych zmiennych globalnych) i ogranicz kruche zależności do minimum, a twój kod będzie testowalny. Testowalny przez określone ramy testowe? Kto wie. Ale może to środowisko testowe musi dostosować się do dobrego kodu, a nie na odwrót.

Ale dla jasności kod, który jest tak sprytny lub tak długi i / lub współzależny, że nie jest łatwo zrozumiany przez inną istotę ludzką, nie jest dobrym kodem. Nie jest to również kod, który można łatwo przetestować.

Czy zbliżając się do mojego podsumowania, czy kod do testowania jest lepszy ?

Nie wiem, może nie. Ludzie tutaj mają kilka ważnych punktów.

Ale wierzę, że lepszy kod jest również kodem testowalnym .

A jeśli mówisz o poważnym oprogramowaniu do użytku w poważnych przedsięwzięciach, wysyłanie nieprzetestowanego kodu nie jest najbardziej odpowiedzialną rzeczą, jaką możesz zrobić z pieniędzmi pracodawcy lub klientów.

Prawdą jest również to, że część kodu wymaga bardziej rygorystycznych testów niż inny kod i trochę głupio jest udawać, że jest inaczej. Jak chciałbyś być astronautą na promie kosmicznym, gdyby nie przetestowano systemu menu, który łączył cię z ważnymi systemami na promie? A może pracownik elektrowni jądrowej, w której nie testowano systemów oprogramowania monitorujących temperaturę w reaktorze? Z drugiej strony, czy odrobina kodu generującego prosty raport tylko do odczytu wymaga kontenera pełnego dokumentacji i tysiąca testów jednostkowych? Mam nadzieję, że nie. Tylko mówię...


1
„lepszy kod jest również kodem testowalnym” To jest klucz. Uczynienie go testowalnym nie poprawi go. Ulepszanie go często sprawia, że ​​jest on testowalny, a testy często dostarczają informacji, które możesz wykorzystać, aby go ulepszyć, ale sama obecność testów nie implikuje jakości i istnieją (rzadkie) wyjątki.
anaximander

1
Dokładnie. Zastanów się, co jest przeciwne. Jeśli jest to kod nie do przetestowania, nie jest testowany. Jeśli nie jest testowany, skąd wiesz, czy działa, czy nie inaczej niż w sytuacji na żywo?
pjc50,

1
Wszystkie testy dowodzą, że kod przechodzi testy. W przeciwnym razie kod testowany jednostkowo byłby wolny od błędów i wiemy, że tak nie jest.
wobbily_col

@anaximander Dokładnie. Istnieje przynajmniej możliwość, że sama obecność testów jest przeciwwskazaniem, co skutkuje gorszym kodem jakości, jeśli wszystko skupia się tylko na zaznaczeniu pól wyboru. „Przynajmniej siedem testów jednostkowych dla każdej funkcji?” "Czek." Ale naprawdę wierzę, że jeśli kod jest kodem jakości, łatwiej będzie go przetestować.
Craig,

1
... ale biurokracja na podstawie testów może być całkowitym marnotrawstwem i nie może dać użytecznych informacji ani wiarygodnych wyników. Bez względu; Na pewno chciałbym, aby ktoś przetestował błąd SSL Heartbleed , tak? czy błąd Apple nie powiódł się ?
Craig

5

Wydaje mi się jednak, że przesadzanie z czasem jest argumentem.

Masz rację, a dzięki kpinie możesz sprawić, że kod będzie testowalny i unikniesz upływu czasu (intencja słów nieokreślona). Przykładowy kod:

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')

Powiedzmy teraz, że chcesz przetestować, co dzieje się podczas sekundy przestępnej. Jak mówisz, aby przetestować to w sposób nadmierny, musisz zmienić kod (produkcyjny):

def time_of_day(now=None):
    now = now if now is not None else datetime.datetime.utcnow()
    return now.strftime('%H:%M:%S')

Jeśli Python obsługuje sekundy przestępne, kod testowy wyglądałby następująco:

def test_handle_leap_second(self):
    actual = time_of_day(
        now=datetime.datetime(year=2015, month=6, day=30, hour=23, minute=59, second=60)
    expected = '23:59:60'
    self.assertEquals(actual, expected)

Możesz to przetestować, ale kod jest bardziej złożony niż to konieczne, a testy nadal nie mogą w niezawodny sposób wykonać gałęzi kodu, z której będzie korzystać większość kodu produkcyjnego (to znaczy, nie przekazując wartości now). Obejdź to, używając makiety . Począwszy od oryginalnego kodu produkcyjnego:

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')

Kod testowy:

@unittest.patch('datetime.datetime.utcnow')
def test_handle_leap_second(self, utcnow_mock):
    utcnow_mock.return_value = datetime.datetime(
        year=2015, month=6, day=30, hour=23, minute=59, second=60)
    actual = time_of_day()
    expected = '23:59:60'
    self.assertEquals(actual, expected)

Daje to kilka korzyści:

  • Testujesz time_of_day niezależnie od jego zależności.
  • Testujesz tę samą ścieżkę kodu, co kod produkcyjny.
  • Kod produkcyjny jest tak prosty, jak to możliwe.

Na marginesie, należy mieć nadzieję, że przyszłe frameworki ułatwią takie rzeczy. Na przykład, ponieważ musisz odwoływać się do wyśmiewanej funkcji jako łańcucha, nie możesz łatwo sprawić, aby IDE zmieniły ją automatycznie, gdy time_of_dayzacznie używać innego źródła przez pewien czas.


FYI: domyślny argument jest nieprawidłowy. Zostanie zdefiniowany tylko raz, więc twoja funkcja zawsze zwróci czas jej pierwszej oceny.
ahruss

4

Jakość dobrze napisanego kodu polega na tym, że można go łatwo zmieniać . Oznacza to, że kiedy pojawia się zmiana wymagań, zmiana kodu powinna być proporcjonalna. Jest to ideał (i nie zawsze osiągany), ale pisanie testowalnego kodu pomaga nam zbliżyć się do tego celu.

Dlaczego pomaga nam to przybliżyć? Podczas produkcji nasz kod działa w środowisku produkcyjnym, w tym integruje się i współdziała z całym naszym innym kodem. W testach jednostkowych usuwamy większość tego środowiska. Nasz kod jest teraz odporny na zmiany, ponieważ testy są zmianą . Używamy jednostek na różne sposoby, z różnymi danymi wejściowymi (symulacje, złe dane wejściowe, które mogą nigdy nie zostać przekazane itp.), Niż używamy ich w produkcji.

To przygotowuje nasz kod na dzień, w którym nastąpi zmiana w naszym systemie. Powiedzmy, że nasze obliczanie czasu musi zająć inny czas w zależności od strefy czasowej. Teraz mamy możliwość przejścia w tym czasie i nie musimy wprowadzać żadnych zmian w kodzie. Gdy nie chcemy minąć czasu i chcemy wykorzystać bieżący czas, możemy po prostu użyć domyślnego argumentu. Nasz kod jest odporny na zmiany, ponieważ można go przetestować.


4

Z mojego doświadczenia wynika, że ​​jedną z najważniejszych i najdalej idących decyzji podejmowanych podczas tworzenia programu jest sposób podziału kodu na jednostki (gdzie „jednostki” są używane w najszerszym tego słowa znaczeniu). Jeśli używasz języka OO opartego na klasach, musisz rozbić wszystkie wewnętrzne mechanizmy zastosowane do wdrożenia programu na pewną liczbę klas. Następnie musisz podzielić kod każdej klasy na pewną liczbę metod. W niektórych językach wybór polega na rozbiciu kodu na funkcje. Lub jeśli wykonasz SOA, musisz zdecydować, ile usług zbudujesz i co wejdzie do każdej usługi.

Wybrany przez ciebie podział ma ogromny wpływ na cały proces. Dobry wybór ułatwia pisanie kodu i powoduje mniej błędów (nawet przed rozpoczęciem testowania i debugowania). Ułatwiają zmianę i utrzymanie. Co ciekawe, okazuje się, że gdy znajdziesz dobrą awarię, zwykle łatwiej jest ją również przetestować niż kiepską.

Dlaczego tak jest? Nie sądzę, że potrafię zrozumieć i wyjaśnić wszystkie powody. Ale jednym z powodów jest to, że dobry podział niezmiennie oznacza wybór umiarkowanej „wielkości ziarna” dla jednostek implementacji. Nie chcesz wrzucać zbyt dużej funkcjonalności i logiki do jednej klasy / metody / funkcji / modułu / etc. To sprawia, że ​​kod jest łatwiejszy do odczytania i łatwiejszy do napisania, ale także ułatwia testowanie.

Ale to nie tylko to. Dobry projekt wewnętrzny oznacza, że ​​oczekiwane zachowanie (wejścia / wyjścia / itp.) Każdej jednostki wdrożenia można zdefiniować w jasny i precyzyjny sposób. Jest to ważne przy testowaniu. Dobry projekt zwykle oznacza, że ​​każda jednostka implementacji będzie miała umiarkowaną liczbę zależności od innych. Ułatwia to innym czytanie i zrozumienie kodu, ale także ułatwia testowanie. Powody są ciągłe; może inni potrafią podać więcej powodów, których nie mogę.

W odniesieniu do przykładu w twoim pytaniu nie uważam, że „dobry projekt kodu” jest równoznaczny z powiedzeniem, że wszystkie zależności zewnętrzne (takie jak zależność od zegara systemowego) powinny zawsze być „wstrzykiwane”. To może być dobry pomysł, ale jest to kwestia odrębna od tego, co tu opisuję i nie będę zagłębiał się w jego zalety i wady.

Nawiasem mówiąc, nawet jeśli wykonujesz bezpośrednie wywołania funkcji systemowych, które zwracają bieżący czas, działają na systemie plików itp., Nie oznacza to, że nie możesz przetestować kodu w izolacji. Sztuką jest użycie specjalnej wersji standardowych bibliotek, która pozwala sfałszować zwracane wartości funkcji systemowych. Nigdy nie widziałem, aby inni wspominali o tej technice, ale jest to dość proste w przypadku wielu języków i platform programistycznych. (Mam nadzieję, że środowisko uruchomieniowe języka jest otwarte i łatwe do zbudowania. Jeśli wykonanie kodu wymaga kroku odsyłacza, mam nadzieję, że łatwo jest kontrolować, z którymi bibliotekami jest połączony).

Podsumowując, testowalny kod niekoniecznie jest „dobrym” kodem, ale „dobry” kod jest zwykle testowalny.


1

Jeśli stosujesz zasady SOLID , będziesz po dobrej stronie, szczególnie jeśli rozszerzysz to o KISS , DRY i YAGNI .

Jednym z brakujących punktów jest dla mnie złożoność metody. Czy jest to prosta metoda pobierająca / ustawiająca? W takim razie pisanie testów spełniających wymagania ram testowych byłoby stratą czasu.

Jeśli jest to bardziej złożona metoda, w której manipulujesz danymi i chcesz mieć pewność, że zadziała, nawet jeśli będziesz musiał zmienić logikę wewnętrzną, byłoby to świetne wezwanie do metody testowej. Wiele razy musiałem zmieniać fragment kodu po kilku dniach / tygodniach / miesiącach i bardzo cieszyłem się z tego przypadku testowego. Podczas pierwszego opracowywania metody przetestowałem ją metodą testową i byłem pewien, że zadziała. Po zmianie mój podstawowy kod testowy nadal działał. Byłem więc pewien, że moja zmiana nie zepsuła starego kodu podczas produkcji.

Innym aspektem pisania testów jest pokazanie innym programistom, jak korzystać z tej metody. Wiele razy programista szuka przykładu, jak użyć metody i jaka będzie zwracana wartość.

Tylko moje dwa centy .

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.