2012-06-13 12 views
41

Wygląda na to, że istnieją dwa zupełnie różne podejścia do testowania i chciałbym zacytować oba.Testowanie: jak skupić się na zachowaniu zamiast na implementacji bez utraty prędkości?

Chodzi o to, że te opinie zostały wypowiedziane 5 lat temu (2007) i jestem zainteresowany, co zmieniło się od tego czasu i w którą stronę powinienem jechać.

Brandon Keepers:

W teorii jest to, że testy mają być agnostykiem realizacji . Prowadzi to do mniej kruchych testów, a właściwie wyników (lub zachowania).

Dzięki RSpec, czuję, że wspólne podejście do kpienia z modeli w celu przetestowania kontrolerów kończy się zmusza do zbytniego spojrzenia na zbyt dużą liczbę do implementacji kontrolera.

To samo w sobie nie jest takie złe, ale problem polega na tym, że zbyt wiele uwagi poświęca kontrolerowi, aby określić, w jaki sposób model jest używany. Dlaczego ma znaczenie, jeśli mój kontroler wywoła Thing.new? Co się stanie, jeśli mój kontroler zdecyduje, że przejmie Thing.create! i trasa ratunkowa? Co się stanie, jeśli mój model ma specjalną metodę inicjowania , np. Thing.build_with_foo? Moja specyfikacja dla zachowania nie powinna zawieść, jeśli zmienię implementację.

Ten problem jest jeszcze gorszy, gdy masz zagnieżdżone zasoby i jest , tworząc wiele modeli na kontroler. Niektóre z moich metod konfiguracji kończą się na 15 lub więcej liniach i są BARDZO delikatne.

Intencją RSpec jest całkowite odizolowanie logiki sterownika od Twoich modeli, co brzmi dobrze w teorii, ale prawie działa w stosunku do ziarna dla zintegrowanego stosu, takiego jak szyny. Zwłaszcza jeśli ćwiczysz dyscyplinę chudego kontrolera/modelu tłuszczu, ilość logiki w sterowniku staje się bardzo mała, a konfiguracja staje się ogromna.

Co więc chce zrobić ktoś z BDD? Cofając się, zachowanie, które naprawdę chcę przetestować, nie polega na tym, że mój kontroler nazywa Thing.new, ale , który podał parametry X, tworzy nową rzecz i przekierowuje do niego.

David Chelimsky:

Chodzi o kompromisy.

Fakt, że AR wybiera dziedziczenie zamiast delegacja stawia nas w się wiążą testów - musimy być połączony z bazą danych lub musimy być bardziej intymny z realizacją. Akceptujemy ten wybór projektu , ponieważ czerpiemy korzyści z ekspresji i SUCHOŚCI.

Łamiąc się z dylematem, wybrałem szybsze testy kosztem nieco bardziej kruche. Wybierasz mniej kruche testy kosztem z nich działa nieco wolniej. To kompromis w obie strony.

W praktyce biegnę testów setki, jeśli nie tysiące razy dni (używam autotest i podjąć kroki bardzo ziarnistych) i zmienić czy używam „nowy” lub „tworzyć” prawie nigdy. Również ze względu na szczegółowe kroki, nowe modele , które pojawiają się na początku są dość niestabilne. Podejście valid_thing_attrs minimalizuje ból z tego powodu, ale wciąż oznacza, że ​​każde nowe wymagane pole oznacza, że ​​muszę zmienić valid_thing_attrs.

Ale jeśli twoje podejście działa dla ciebie w praktyce, to jest dobre! W rzeczywistości, , zdecydowanie zaleciłbym opublikowanie wtyczki z generatorami , które produkują przykłady tak, jak lubisz. Jestem pewien, że wiele osób odniosłoby z tego korzyści.

Ryan Bates:

Z ciekawości, jak często używacie mocks w badaniach/specyfikacji? Być może robię coś złego, ale znajduję to poważnie ograniczające. Od przejścia na rSpec ponad miesiąc temu, robiłem , co zalecają w dokumentach, w których kontroler i warstwy widoku nie trafiają w bazę danych, a modele są całkowicie wyśmiewane obecnie. Daje to miłe przyspieszenie i sprawia, że ​​niektóre rzeczy łatwiej, , ale uważam, że wady to znacznie przewyższają zalety. Od za pomocą makiet, moje specyfikacje zamieniły się w koszmar utrzymania. Specyfikacje służą do testowania zachowania, a nie jego implementacji. Nie obchodzi mnie , jeśli została wywołana metoda Chcę po prostu upewnić się, że wynik wyjściowy jest poprawny. Ponieważ szyderstwo sprawia, że ​​specyfikacja jest wybredna w stosunku do implementacji , to sprawia, że ​​proste refaktoryzacje (które nie zmieniają zachowania ) są niemożliwe bez konieczności ciągłego wracania i "poprawiania" specyfikacji. Jestem bardzo przekonany o tym, co powinien zawierać opis/testy: . Test powinien się rozpaść tylko wtedy, gdy aplikacja się zepsuje. Jest to jeden z powodów, dla których nie testuję warstwy widoku, ponieważ uważam ją za zbyt sztywną. Często prowadzi to do przerwania testów bez przerywania aplikacji, gdy zmienia się małe rzeczy w widoku. Ten sam problem występuje z mockami . Do tego wszystkiego, właśnie zdałem sobie sprawę, że dzisiaj szyderstwa/karczowania metoda klasy (czasami) trzyma się pomiędzy specyfikacjami. Specyfikacje powinny być samodzielne i nie powinny być pod wpływem innych specyfikacji. To łamie tę regułę i prowadzi do trudnych błędów. Czego nauczyłem się z tego wszystkiego? Bądź ostrożny, gdy używasz szyderstwa. Stubbing nie jest tak zły, ale wciąż ma kilka takich samych problemów.

Wziąłem w ciągu ostatnich kilku godzin i usunąłem prawie wszystkie mocks z mojej specyfikacji. Połączyłem również kontroler i wyświetl specyfikacje w jeden przy użyciu "integracją_wyników" w specyfikacji kontrolera. Ładuję również wszystkie urządzenia dla każdej specyfikacji kontrolera, więc istnieje kilka danych testowych do wypełnienia widoków. Wynik końcowy? Moje specyfikacje są krótsze, prostsze, bardziej spójne, mniej sztywne i testują cały stos razem (model, widok, kontroler), aby żadne błędy nie mogły się zepsuć. Jestem nie mówiąc, że jest to "właściwa" droga dla wszystkich. Jeśli Twój projekt wymaga bardzo surowego przypadku spec, to może nie być dla ciebie, ale w moim przypadku jest to lepsze na świecie niż to, co miałem przed użyciem makiet.Wciąż myślę, że stubbing jest dobrym rozwiązaniem w kilku miejscach, więc nadal robię .

+0

To jest dobre pytanie; Widziałem wiele kruchych testów jednostkowych w wyniku całego szyderstwa, które się dzieje.Testy jednostek JavaScript mogą być gorsze. –

+0

Człowieku, podoba mi się te wszystkie myśli! Największym problemem jest prawdopodobnie Rails. W GOOS mówią, że nie powinieneś kpić z kodu strony trzeciej (która zawierałaby ActiveRecord), ponieważ nie możesz wprowadzać zmian w projekcie, które mają na to wpływ. Zasadniczo jest to problem, David zauważa, że ​​wolałby raczej używać kompozycji, ale nie może b/c AR wybrać dziedziczenia (jeśli chce robić rzeczy tak, jak robiono to tradycyjnie w Railsach). Osobiście mam wiele pomysłów i nie wiem, które z nich są właściwe. Dobre pytanie. –

Odpowiedz

16

Myślę, że wszystkie trzy opinie są nadal w pełni aktualne. Ryan i ja zmagaliśmy się z łatwowiernością szyderstwa, podczas gdy David czuł, że opłacalność utrzymania jest warta wzrostu prędkości.

Ale te kompromisy są symptomami głębszego problemu, o którym Dawid wspomniał w 2007 roku: ActiveRecord. Projekt ActiveRecord zachęca do tworzenia obiektów bogów, które robią za dużo, wiedzą za dużo o reszcie systemu i mają za dużo powierzchni. Prowadzi to do testów, które mają zbyt wiele do przetestowania, zbyt wiele wiedzą o reszcie systemu i są albo zbyt wolne, albo kruche.

Jakie jest rozwiązanie? Oddziel jak najwięcej aplikacji od frameworka. Napisz wiele małych klas, które modelują twoją domenę i nie dziedziczą niczego. Każdy obiekt powinien mieć ograniczoną powierzchnię (nie więcej niż kilka metod) i jawne zależności przekazywane przez konstruktor.

Przy takim podejściu pisałem tylko dwa rodzaje testów: izolowane testy jednostkowe i testy systemowe z pełnym stosunkiem. W testach izolacyjnych szydzę lub koduję wszystko, co nie jest testowanym obiektem. Testy te są niesamowicie szybkie i często nie wymagają nawet ładowania całego środowiska Rails. Testy pełnego stosu ćwiczą cały system. Są boleśnie powolne i dają nieprzydatne informacje zwrotne, gdy zawodzą. Piszę tak mało, jak to konieczne, ale wystarczająco dużo, aby dać mi pewność, że wszystkie moje dobrze przetestowane obiekty dobrze się integrują.

Niestety, nie mogę wskazać przykładowego projektu, który dobrze to robi (jeszcze). Opowiem o tym trochę w mojej prezentacji na Why Our Code Smells, oglądam prezentację Coreya Hainesa pod tytułem Fast Rails Tests i gorąco polecam lekturę Growing Object Oriented Software Guided by Tests.

+3

Nadal uważam, że kompromis jest warta zwiększonej prędkości w specyfikacjach kontrolera, ponieważ jesteśmy stubbing/kpiny z innej warstwy. Mimo że 'new' i' create' pochodzą z ActiveRecord, jeśli są wywoływane ze sterowników, są one efektywnymi publicznymi API w _Twoim_ modelu. Jednak uważam, że nie warto myśleć o kompromisach w specyfikacjach modeli, ponieważ musisz aktywować detale niższego poziomu ActiveRecord. –

+0

Ogólnie zgadzam się z tym. Chociaż ostatnio rzadko piszę specyfikacje kontrolera, ponieważ zwykle są to tylko kilka linii kodu. Jeśli wydłuży się, przeciągnę logikę na inny obiekt, który można przetestować. – bkeepers

9

Dziękuję za kompilację cytatów z 2007 roku. Fajnie jest spojrzeć za siebie.

Moje obecne podejście do testowania jest opisane w this RailsCasts episode, z czego byłem bardzo zadowolony. Podsumowując, mam dwa poziomy testów.

  • Wysoki poziom: używam żądania widowisko w RSpec, Kapibara, a magnetowidem. Testy mogą być oflagowane, aby wykonać JavaScript w razie potrzeby. W ten sposób unika się szyderstwa, ponieważ celem jest przetestowanie całego stosu. Każda akcja kontrolera jest testowana co najmniej raz, może kilka razy.

  • Niski poziom: Tutaj testowana jest cała złożona logika - głównie modele i pomocnicy. Ja także unikam drwin. Testy trafiają do bazy danych lub otaczających obiektów, gdy jest to konieczne.

Zauważ, że nie ma specyfikacji kontrolera ani widoku. Uważam, że są one odpowiednio uwzględnione w specyfikacji zamówienia.

Skoro jest mało kpiny, w jaki sposób mogę szybko przeprowadzić testy? Oto kilka porad.

  • Unikaj nadmiernej logiki rozgałęzień w testach wysokiego poziomu. Każda złożona logika powinna zostać przeniesiona na niższy poziom.

  • Podczas generowania rekordów (np. W programie Factory Girl), należy najpierw użyć numeru build i przełączać się w razie potrzeby na numer create.

  • Użyj Guard z Spork, aby pominąć czas uruchamiania Rails. Odpowiednie testy są często wykonywane w ciągu kilku sekund po zapisaniu pliku. Użyj znacznika :focus w protokole RSpec, aby ograniczyć testy uruchamiane podczas pracy w określonym obszarze. Jeśli jest to duży zestaw testów, ustaw all_after_pass: false, all_on_start: false w Guardfile tak, aby uruchamiał je wszystkie tylko wtedy, gdy są potrzebne.

  • Używam wielu asercji na test. Wykonywanie tego samego kodu konfiguracji dla każdego potwierdzenia znacznie wydłuży czas testu. RSpec wydrukuje linię, która się nie powiodła, więc łatwo ją zlokalizować.

Uważam, że kpiny dodają kruchości badaniom, dlatego go unikam. To prawda, że ​​może być świetny jako pomoc w projektowaniu OO, ale w strukturze aplikacji Railsowej nie jest to tak skuteczne. Zamiast tego polegam głównie na refaktoryzacji i pozwolę, aby sam kod podpowiadał mi, jak powinien wyglądać projekt.

To podejście sprawdza się najlepiej w małych i średnich aplikacjach Railsowych bez rozległej, złożonej logiki domeny.

7

Świetne pytania i wspaniała dyskusja. @ryanb i @bkeepers wspominają, że piszą tylko dwa rodzaje testów. Podejmuję podobne podejście, ale mam trzeci typ testu:

  • Testy jednostkowe: testy pojedyncze, zazwyczaj, ale nie zawsze, w stosunku do zwykłych obiektów z rubinem. Moje testy jednostkowe nie obejmują wywołań interfejsu API, 3rd party API ani żadnych innych zewnętrznych elementów.
  • Testy integracyjne: nadal koncentrują się na testowaniu jednej klasy; Różnice polegają na tym, że integrują tę klasę z zewnętrznymi rzeczami, których unikam w swoich testach jednostkowych. Moje modele często będą miały zarówno testy jednostkowe, jak i testy integracyjne, gdzie jednostka testuje ostrość w czystej logice, która może być przetestowana bez udziału DB, a testy integracyjne będą obejmować DB. Ponadto staram się testować zewnętrzne owijacze API z testami integracyjnymi, używając VCR, aby testy były szybkie i deterministyczne, ale pozwalając moim kompilacjom CI tworzyć żądania HTTP na prawdziwe (aby wychwycić wszelkie zmiany API).
  • Testy akceptacyjne: testy end-to-end, dla całej funkcji. Nie chodzi tylko o testowanie interfejsu użytkownika za pośrednictwem kapibary; Robię to samo w moich klejnotach, które mogą wcale nie mieć interfejsu HTML. W takich przypadkach wykonuje to wszystko, co klejnot wykonuje od końca do końca. Zwykle używam magnetowidu w tych testach (jeśli wykonują zewnętrzne żądania HTTP) i jak w moich testach integracyjnych, moja kompilacja CI jest skonfigurowana tak, aby żądania HTTP były prawdziwe.

Jeśli chodzi o kpiny, nie mam podejścia "jeden rozmiar dla wszystkich". W przeszłości zdecydowanie się przejmowałem, ale nadal uważam, że jest to bardzo przydatna technika, zwłaszcza gdy używamy czegoś takiego jak rspec-fire. Ogólnie oszukuję współpracowników grających dowolnie role (szczególnie jeśli je posiadam, i są to obiekty usługowe) i staram się ich unikać w większości innych przypadków.

Prawdopodobnie największa zmiana w moich testach w ciągu ostatniego roku została zainspirowana przez DAS: podczas gdy miałem spec_helper.rb, który ładuje całe środowisko, teraz jawnie ładuję tylko test pod kątem klasy (i wszelkie zależności). Poza poprawioną szybkością testu (co robi ogromną różnicę!) Pomaga mi to określić, kiedy mój test podrzędny pociąga za sobą zbyt wiele zależności.

Powiązane problemy