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.
źródło
func(X)
wróci"Morning"
, to zastąpienie wszystkich wystąpieńfunc(X)
ze"Morning"
nie zmieni programu (czyli powołanie.func
Nie coś innego niż Zwraca wartość zrobić). Idempotencja oznacza, że albofunc(func(X)) == X
(co nie jest poprawne pod względem typu), albofunc(X); func(X);
wywołuje takie same skutki uboczne jakfunc(X)
(ale tutaj nie ma żadnych skutków ubocznych)Odpowiedzi:
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.
źródło
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:
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.
źródło
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.źródło
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.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.
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.
źródło
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ł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.
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.źródło
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ż
GetTimeOfDay
jest bardziej testowalny, jesteś z powrotem w tej samej łodzi, testując nowyIDateTimeProvider
. 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.
źródło
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.
źródło
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ę...
źródło
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:
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):
Jeśli Python obsługuje sekundy przestępne, kod testowy wyglądałby następująco:
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:Kod testowy:
Daje to kilka korzyści:
time_of_day
niezależnie od jego zależności.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_day
zacznie używać innego źródła przez pewien czas.źródło
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ć.
źródło
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.
źródło
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 .
źródło