27 listopad 2006
Behaviour Driven Development
Podczas wczorajszego spotkania KRUG-u wygłosiłem króciutką prezentacje na temat wyżej wspomnianego BDD. Dla tych którzy nie mogli się stawić w zastępstwie naszykowałem krótki artykuł pokrywający się mniej więcej z jej treścią. Reszta może od razu przejść na koniec, pobrać “slajdy” które nie są slajdami, obejrzeć przykłady i przejrzeć dalsze zasoby. Może, ale nie musi, bo w artykule napisałem kilka rzeczy o których zapomniałem powiedzieć w prezentacji.
A więc cofnijmy się na chwilę do lat cirka abałt powiedzmy 1995-2000. OOP przeżywa prawdziwy boom (chociaż Smalltalk jest od kilkunastu lat na rynku), Sun ładuje kase w marketing Javy, powstają pierwsze design patterns, a dookoła kręcą się adwokaci Extreme Programming którzy też chcą zarobić trochę kasy - przy okazji jednak mają też kilka naprawdę niezłych pomysłow.
W tym samym okresie i częściowo Ci sami ludzie - Kent Beck twórca XP i Erich Gamma od patternów - piszą JUnit - pierwszy popularny framework do testowania modułowego. I tak to się wszystko zaczeło… Kogo ominął ten moment, może się cofnąć w czasie i poczytać artykuł od którego wielu zaczynało.
Okazało się, że takie “niewinne” testowanie niesie ze sobą całkiem spory szereg zalet:
- Mniej czasu spędzonego w debuggerze i na dopisywaniu “pomocniczego” kodu, o ile testy są dobrze napisane rzeczy typu błędne wartości zmiennych są podane na tacy przez framework testujący
- Pewność że raz poprawiony bug nie wkradnie się do systemu ponownie - pierwsza rzecz po znalezieniu buga to napisanie nie przechodzącego testu, dopiero potem naprawienie buga i upewnienie się że test przechodzi. Gdyby z jakichś przyczyn podobny błąd miałby wystąpić w tym samym miejscu ponownie, dowiemy się tego z testów.
- Dokumentacja i przykłady użycia “gratis” - testy stanowią pewną formę bardzo technicznej dokumentacji, pokazując w praktyczny sposób do czego służy nasz kod i jak powinniśmy go używać.
- Ułatwiają refactoring, czyli zmienianie struktury kodu bez zmieniania jego działania celem zwiększenia jego “czystości”. W trakcie takich “porządków” łatwo coś uszkodzić, a testy pozwalają nam upewnić się że pamiętaliśmy o wszystkich kawałkach kodu zależnych od tego, co w danej chwili refactorujemy
- Redukują czas potrzebny na ręczne na testowanie
Ludzie którzy ochoczo wdrożyli testowanie szybko dostrzegli kilka istotnych rzeczy. Okazało się na przykład że testowanie potrafi w znacznym stopniu wpłynąć na sposób w jaki projektujemy nasze oprogramowanie - kod który jest bardzo ściśle powiązany z innymi modułami jest dużo ciężej testować niż taki który jest “ortogonalny” - czyli w którym poszczególne części działają możliwe niezależnie od siebie. Niektórzy polubili to do tego stopnia, że pomyśleli dlaczegoby właściwie nie zacząć od pisania testów, a dopiero potem przystąpić do właściwego tworzenia oprogramowania.
Tak dochodzimy do Test Driven Development. Podstawowym pomysłem jest podzielenie procesu programowania na malutkie “cykle” - “test, code, refactor”. Zaczynamy od napisania malutkiego kawałku testu, który koniecznie musi nie działać. Następnie dopisujemy funkcjonalność, tak żeby dojść do momentu w którym test działa możliwie jak najmniejszym wysiłkiem. Na koniec “sprzątamy”, czyli od kodu który działa przechodzimy do kodu który działa i jakoś wygląda, cały czas po drodze upewniając się że test przechodzi. Potem następny taki cykl, i tak w kółko.
Jakie są zalety takiego podejścia? Po pierwsze, relaks. Pracujemy w małych kroczkach, cały czas robiąc widoczne postępy. To ważne nie tylko z technicznego punktu widzenia, ale także z psychologicznego - przejście z “czerwonego” paska do “zielonego” zawsze stanowi jakąś motywacje do dalszej pracy. W dodatku cały czas otrzymujemy “feedback” (brak mi tu dobrego polskiego odpowiednika tego słowa) na temat tego co nasz kod robi - docenia się to szczególnie przy tych co trudniejszych i bardziej orginalnych zadaniach programistycznych w których nie czujemy się zbyt pewnie. Dodatkowo zmuszeni jesteśmy do przemyślenia interfejsu tego czegoś co chcemy zaprogramować, warunków początkowych i końcowych jakie zakładamy itp. W rezultacie otrzymujemy dużo lepszy kod.
Z tradycyjnym TDD jest również pare problemów. Po pierwsze, tradycyjne architektury do testów modułowych pozostawiają dużą swobodę podejścia i wielu początkujących i średniozaawansowanych testerów nie rozumie na czym testowanie powinno polegać - wiele osób ogranicza się do pisania po jednym teście na metodę i to wszystko (co i ja na początku zresztą robiłem). Doświadczeni testerzy robią zupełnie co innego - testują poszczególne zachowania i grupy zachowań programu, nie skupiając się na korespondencji pomiędzy testami a wewnętrzną strukturą kodu.
Mamy tu też pewien paradoks - skoro “testujemy” kod jeszcze przed jego napisaniem to co właściwie “testujemy”? Próżnie? Kod który dopiero powstanie? Tradycyjny język jakim się posługujemy przy opisywaniu tych praktyk stał się nieaktualny wraz z ich rozwojem - testy z czasem przestały być testami.
Para naukowców Sapir-Whorf sformułowała kiedyś stosunkowo znaną hipotezę, jakoby język naturalny (polski, angielski, niemiecki itp.) którego używamy w naszej głowie do myślenia miał wpływ na to co myślimy i w jaki sposób. To bardzo ciekawe - czy pewne myśli da się wyrazić w jednych językach, a w innych nie? W końcu skoro pewne polskie słowa nie mają odpowiedników angielskim i vice versa, to samo pewnie można odnieść np. do konstrukcji gramatycznych. Mamy przecież także programowanie neurolingwistyczne które zajmuje się wpływem używanego języka na ludzi. Podbudowa naukowa NLP jest co prawda chybotliwa i kontrowersyjna, ale wielu ludzi jest w stanie przysiąc, że to działa. Szczególnie sprzedawcy książek o NLP i kolesie którzy neurolingwistycznie wyrywają laski :D
Analogiczne rozważania mają też sens przeniesione w świat języków programowania. To, że dobór języka programowania pasującego do domeny problemu ma olbrzymi wpływ na wydajność programisty stało się już mam nadzieje oczywiste. Tak samo zmiana języka w którym mówimy o tym, co zwykło nazywać się “testami” mogłaby przynieść nam sporo korzyści, albo przynajmniej w pewien sposób wymusić na początkujących podobne podejście, jak teraz stosują zaawansowani “testerzy”.
I tak dochodzimy do tego czym BDD jest - nowym sposobem opisywania i myślenia o “testach”, które teraz nazywają się bardziej zgodnie z prawdą specyfikacjami. Nie mamy już testów, mamy specyfikacje, a zamiast asercji stosujemy oczekiwania. Jako bonus zyskujemy możliwość łatwego zrozumienia tego co robimy także przez nietechniczną kadrę. Zmniejsza się zapotrzebowanie na narzędzia tego agiledox, bo specyfikacje same w sobie są łatwe do zrozumienia, jak za chwilę się przekonamy.
Zobaczmy o co chodzi na dość prostym przykładzie:
require 'net/http' require 'uri' require 'rexml/document' require 'rexml/xpath' class News attr_accessor :title, :description end class NewsList attr_accessor :newses, :feed_xml def initialize @newses = [] end def fetch(url) url = URI.parse(url) res = Net::HTTP.start(url.host, url.port) do |http| http.get(url.path) end @feed_xml = res.body end def parse document = REXML::Document.new(@feed_xml) REXML::XPath.each(document, "/rdf:RDF/item") do |element| news = News.new news.title = element.text("title") news.description = element.text("description") @newses << news end end end
Mamy tu dość prostą klasę (trudno żeby była skomplikowana, pisałem ją w dzień prezentacji po średnio przespanej, za to rozrywkowej nocy) - chodzi generalnie o to żeby ściągnąć feed RSS z Internetu i dokonać podstawowego parsownia, aby zamiast dokumentu XML mieć tablicę obiektów News z tytułem i treścią zagregowanej wiadomości. Przykład testu modułowego wygląda tak (zakładam że snippet wyżej rezyduje w pliku example.rb w tym samym katalogu):
require 'example' require 'test/unit' class NewsListTest < Test::Unit::TestCase def setup @news_list = NewsList.new end def test_fetch_document @news_list.fetch("http://rss.slashdot.org/Slashdot/slashdot") assert_not_nil @news_list.feed_xml end def test_parse_xml_feed @news_list.fetch("http://rss.slashdot.org/Slashdot/slashdot") @news_list.parse assert_not_equal [], @news_list.newses end def test_build_news_array_when_parsing @news_list.fetch("http://rss.slashdot.org/Slashdot/slashdot") @news_list.parse assert_not_equal nil, @news_list.newses[0].title assert_not_equal nil, @news_list.newses[0].description assert_not_equal "", @news_list.newses[0].title assert_not_equal "", @news_list.newses[0].description end end
Klasa jest zbyt prosta żeby dokładnie pokazać różnicę pomiędzy testami np. pisanymi per metodę, a takimi nastawionymi na zachowania, ale widać przynajmniej pewną konwencję - nazwy testów nie pochodzą od nazw metod, ale od tego co powinna robić klasa, którą testujemy.
Analogiczny test w rSpec, frameworku do BDD dla Ruby’ego wygląda tak:
require 'example' context "The RSS news list" do setup do @news_list = NewsList.new end specify "should fetch document" do @news_list.fetch("http://rss.slashdot.org/Slashdot/slashdot") @news_list.feed_xml.should.not.be.equal nil @news_list.feed_xml.should.not.be.equal "" end specify "should parse XML feed" do @news_list.fetch("http://rss.slashdot.org/Slashdot/slashdot") @news_list.parse @news_list.newses.should.not.be.equal [] end specify "should build news array when parsing" do @news_list.fetch("http://rss.slashdot.org/Slashdot/slashdot") @news_list.parse @news_list.newses[0].title.should.not.be.equal nil @news_list.newses[0].description.should.not.be.equal nil @news_list.newses[0].title.should.not.be.equal "" @news_list.newses[0].description.should.not.be.equal "" end end
Różnica pomiędzy “naturalnojęzykowym” opisem, a kodem Ruby się tu w pewien sposób zaciera. Jeśli uruchomimy taki kod za pomocą “spec example_test_rspec.rb -f s” otrzymamy taki oto dokument:
The RSS news list - should fetch document - should parse XML feed - build news array when parsing
Pomijając tu nawet zyski z samego BDD, jest to interesujący przykład tego co możemy zrobić w Rubym za pomocą technik metaprogramowania i pisania DSLi, czyli małych języków dla specyficznych zastosowań. Jeśli nad tym dobrze zastanowić, to nawet Rails jest w pewnym stopniu i między innymi takim właśnie językiem opisu aplikacji sieciowych stworzonym w Ruby. Ciężko byłoby przenieść takie rzeczy do innych języków programowania. Jak na razie broni się tylko Smalltalk, w którym sSpec jest dość podobny do wersji Ruby’owej. jBehave czyli BDD dla Javy dalekie jest już od takiej naturalności.
Nasz test ma jedną zasadniczą wadę - polega na zewnętrznym feedzie Slashdota. Z użyciem wbudowanego w BDD rozbudowanego “mockowania” obiektów móżemy przepisać naszą specyfikacje aby działała nawet bez połączenia z Internetem:
require 'example' @@feed =<News one Description one News two Description two - END context "The RSS news list" do setup do @news_list = NewsList.new @res = mock("response") @res.should_receive(:body).and_return(@@feed) Net::HTTP.stub!(:start).and_return(@res) end specify "should fetch document" do Net::HTTP.should_receive(:start).with("rssurl.com", 80).and_return @res @news_list.fetch("http://rssurl.com/feed.xml") @news_list.feed_xml.should.be.equal @@feed end specify "should parse XML feed" do @news_list.fetch("http://rssurl.com/feed.xml") @news_list.parse @news_list.should.have(3).newses end specify "build news array when parsing" do @news_list.fetch("http://rssurl.com/feed.xml") @news_list.parse @news_list.newses[0].title.should == "News one" @news_list.newses[0].description.should == "Description one" @news_list.newses[1].title.should == "News two" @news_list.newses[1].description.should == "Description two" @news_list.newses[2].title.should == "News three" @news_list.newses[2].description.should == "Description three" end end
News two Description three
Adnotacje:
- Pułapka czyha na chcących użyć “should.equal” do porównania dwóch stringów. Od wersji 0.7 rSpec używa w takim wypadku “equal?”, a nie “==”, co porównuje niejako referencje, a nie samą treść napisu. Trzeba więc pisać jak w przykładzie - “should ==”.
- Dziękuje mojemu bratu ciotecznemu i Bragiemu za znamienny wkład w moją testową edukację.
- Wpis sponsoruje gniazdko 230V w pociągu Intercity Kraków - Gdynia.
- Następnym razem będą prawdziwe, politycznie poprawne slajdy, obiecuje ;)
Zasoby:
Pseudoslajdy + przykłady z KRUGowej prezentacji są tutaj
Główna strona BDD: http://behaviour-driven.org/
Anglojęzyczna prezentacja Dave’a Astelsa o BDD z GoogleVideo: http://video.google.com/videoplay?docid=8135690990081075324
rSpec: http://rspec.rubyforge.org/
rSpec i Rails: http://rspec.rubyforge.org/tools/rails.html
rSpec i ZenTest: http://blog.nicksieger.com/articles/2006/09/13/auto-rspec
rSpec i rcov: http://rspec.rubyforge.org/tools/rcov.html
jBehave: http://jbehave.codehaus.org/
sSpec: http://www.squeaksource.com/SSpec/
Komentarze ():
Andrzej, 28 listopad 2006, 2:01 am
Widze, ze nie tylko ja lubie klase dobrze przetestowac/wyspecyfikowac zanim zaimplementuje
:-)
Swietny wpis. Zaluje, ze nie bylem na prezentacji.
Michał Kwiatkowski, 28 listopad 2006, 3:11 am
Trochę namieszałeś. Pisanie jednego testu na metodę jest rzeczywiście często niewystarczające, bo często prócz “typowych” danych warto uwzględnić przypadki wyjątkowe (ang. corner-cases). To jest zasada, która obowiązuje, gdy mówimy o testowaniu jednostkowym (ang. unit testing), tzn. testowaniu poszczególnych metod, klas, czy modułów. Przy pisaniu tego rodzaju testów możemy korzystać z wiedzy na temat struktury kodu i zależności pomiędzy obiektami systemu, co pozwala nam dokładniej i skrupulatniej przetestować każdy z “kawałków” z osobna. Przykładem środowisk do testowania jednostkowego są np. standardowy w Ruby Test::Unit, czy też wspomiany JUnit.
Istnieje również drugi biegun podejścia do testowania: testowanie funkcjonalne (ang. functional testing czy też black box testing). Ten sposób zakłada znajomość wyłącznie interfejsu testowanego systemu. Na wejście systemu podajemy dane i patrzymy na otrzymany na wyjściu wynik. Świetnym przykładem środowiska do pisania testów funkcjonalnych jest Selenium.
Nie można powiedzieć, że któraś z tych technik jest “lepsza”. Obie reprezentują różne podejścia do testowania systemu i najlepsze efekty przynosi ich skuteczne połączenie.
sztywny, 28 listopad 2006, 8:15 am
Wcale nie namieszałem. Specjaliści od testów modułowych (przez test modułowych rozumiem wszystko co jest pisane są pisane za pomocą frameworku do pisania testów modułowych np. runit czy junit, inne rozróżnienia są dość dziwne i mgliste) nie zalecają pisania jednego testu na metodę i nie chodzi wcale o corner cases. Jeśli obejrzysz prezentacje Dave’a Astelsa który jest jakimś tam autorytetem jeśli chodzi o testowanie, to będziesz tam miał jeden z argumentów - jeśli piszesz test per metodę, to w momencie w którym refaktorujesz musisz też przepisywać swój test. Ba, zaleca się nawet tylko jedną asercję na test:
http://www.artima.com/weblogs/viewpost.jsp?thread=35578
Jeśli nie wierzysz temu konkretnemu panu, podobne rzeczy znajdują się w każdej książce o TDD, niektóre z nich także można kupić w Polsce (ostatnio kupiłem np. “Programowanie Ekstremalne w C#” bodajże, ale właściwie jest to książka o testowaniu).
Testowanie funkcjonalne w rodzaju tego realizowanego przez Selenium jest zupełnie czymś innym i nie o nim mówimy.
Rrrodrigo, 28 listopad 2006, 11:22 am
Pułapka czyha a nie czycha ;-) Poza tym nie mam się do czego przyczepić :-(
dr_bonzo, 28 listopad 2006, 1:41 pm
Dobre wyjasnienie TDD i BDD i ich roznic.
Doczepie sie tylko do @newses :D powinno byc @news ale skoro stosujemy prawo najmniejszego zaskoczenia (LOLA) to nie znajac dobrze slownictwa/gramatyki angielskiego widze ze to liczba mnoga i tablica :)
sztywny, 28 listopad 2006, 2:35 pm
Byka ortograficznego poprawiłem, a @newses niech już sobie zostanie jak jest :D
dr_bonzo, 28 listopad 2006, 8:33 pm
Znowu ja, wiem czego braklo: opisania kontekstow jako stanow/kontekstow, ktore specyfikujemy, np. niedostepny feed, dostepny feed, bledny xml feeda, itp.
sztywny, 28 listopad 2006, 8:38 pm
Racja.
Michał Kwiatkowski, 28 listopad 2006, 11:34 pm
Nie zgodziłem się dokładnie z tym jednym zdaniem:
Dla mnie testowanie zachowań programu bez uwzględnienia tego co jest w środku to jest właśnie testowanie funkcjonalne. Testowanie modułowe zakłada znajomość wnętrza systemu (jak chociażby nazwy metod), jak i połączeń w środku (co czasami wymaga skorzystania z tzw. mock objects, by rzeczywiście przetestować pojedynczy kawałek kodu). Dlatego zdanie powyższe zrozumiałem jako “Doświadczeni testerzy testują tylko funkcjonalnie”. A z tym ciężko się zgodzić.
sztywny, 29 listopad 2006, 8:05 am
Poprzez ów “korespondencje” rozumiem takie układanie testów, żeby ich struktura odzwierciedlała strukturę kodu, a to już dawno “wyszło z mody”. Oczywiście uwzględniamy to co jest w środku, ale nie skupiamy się na tym jak to coś jest zbudowane, ale raczej na tym jak dany kawałek kodu powininen się zachowywać w danej sytuacji.
Poprzez testowanie funkcjonalne najczęściej rozumie się testy nie mające z kodem zupełnie nic wspólnego, w których jakiś automat np. wpisuje jakiś tekst w nasze input boxy i patrzy co się dzieje. Z drugiej strony widziałem już tą definicje odniesioną do wielu bardzo różnych rzeczy…
Ostatni przykład korzysta z mock objects w wydaniu BDD.
zuras, 17 styczeń 2007, 1:02 am
Drodzy koledzy, testy funkcjonalne i modulowe sie nie wykluczaja a wrecz przeciwnie - Uzupelniaja sie! To nie moze byc tak ze ktores sa lepsze. Moim zdaniem powinno sie stosowac obydwie metody (i nie tylko te).
A odnosnie wypracowania sztywnego to bardzo mi sie podoba. Testy modulowe czesto staja sie zbyt syntetyczne i oderwane od rzeczywistosci. Nie sprawdzaja rzeczywistych sytuacji a w skrajnych sytuacjach sa ‘odwalane’ aby tylko byly bo kod nie przejdzie inspekcji jak nie bedzie do niego testow. Testy modulowe wymagaja zaangazowania i wiary w ich skutecznosc.
Pozdro
Stifflog PL – Behaviour Driven Development – programowanie - dowiedz się więcej!, 4 maj 2010, 12:34 am
[...] Więcej: Stifflog PL – Behaviour Driven Development [...]
cleveland indians tickets, 29 czerwiec 2011, 1:43 pm
braklo: opisania kontekstow jako stanow/kontekstow, ktore specyfikujemy, np. niedostepny feed, dostepny feed, bledny xml feeda