Wiedz, że coś się dzieje…

Ostatnimi czasy, mam okazję tworzyć dość duże ilości testów jednostkowych. Zwykle tworze je do cudzego kodu, co pozwala mi w pewnym sensie ocenić jego jakość. Zapytacie może, jak proces pisania testów jednostkowych do istniejącego kodu pozwala ocenić jego jakość ?

Otóż, pewna stara programistyczna fama głosi, że gdy kod jest łatwo testowalny, to prawdopodobnie jego jakość, a przynajmniej architektura będzie wysokiej jakości. Automatycznie na myśl przychodzi takie proste rozwiązanie, że skoro kod ma być łatwo testowalny, to najlepiej by było zacząć jego tworzenie od napisania testów. Takie podejście nazywa się TDD (Test Driven Development). Pewnie wielu z Was słyszało o takim podejściu, natomiast jeżeli pracujecie w typowej firmie wytwarzającej „stronki”, to pewnie nie mieliście wielu okazji by zastosować takie podejście.

Oczywiście nie koniecznie jest to grzech – w prostych projektach zastosowanie TDD może być dyskusyjne ze względu na narzut czasowy jaki generuje. Znając jednak życie, prosty projekt zwykle przeradza się w projekt skomplikowany, którego skali nikt nie przewidział.

Abstrahując jednak od TDD, chciałbym się podzielić z Wami, moimi spostrzeżeniami na temat typowych „wzorców” w kodzie, które znacząco obniżają jego testowalność. Przy okazji zaproponuje sposoby ich rozwiązania poprzez refaktoring kodu oraz omówię konsekwencje jakie niosą za sobą poszczególne antypatterny.

Tak więc, jeżeli spotkasz jeden z podanych poniżej przypadków – wiedz, że coś się dzieje 😉

  1. Testując dany obiekt sprawdzam jego stan po wywołaniu danej metody:
  2. Testując dany obiekt tworzymy mocki obiektów od których jest zależny w sposób łańcuchowy
  3. Testując dany obiekt łapiemy się na tym, że testując metodę publiczną w istocie chcielibyśmy przetestować metody prywatne z których ona korzysta

Kilka przykładów kodu, żeby było wiadomo o co chodzi:

Przykład nr. 1:

Mamy zdefiniowane dwie klasy – Bar, która ma property „i” oraz klasę TestSubject1 która jest testowana przy użyciu funkcji testCase1. Co jest złego w tego rodzaju kodzie ? Przede wszystkim następuje niejawna manipulacja obiektem klasy Bar. Poza tym, jako, że metoda ma typ void (w tym przypadku w Scali jest to Unit), programista, który chce użyć takiego kodu, tak naprawdę nie wie jaki jest efekt jego działania. Kod po refaktoringu:

Jak widać, metoda po refaktoringu zwraca jakąś konkretną wartość, poza tym zamiast zmieniać stan obiektu, tworze nowy ze zmienioną wartością, dzięki czemu unikam modyfikowania globalnego stanu.

Przykład nr.2:

W kolejnym przykładzie, klasa TestSubject2 wyłuskuje wartość „i” poprzez zawołanie kilku (widziałem nawet kilkanaście) getterów w łańcuchu, nie muszę oczywiście mówić jak utrudnia to testowanie – należy po kolei zamockować i ustawić obiekty zwracane przez każdy z getterów, co może zająć wiele linii kodu, jest irytujące i bardzo wk****jące ;). Oczywiście taki kod jest jawnym pogwałceniem zasady enkapsulacji i ukrywania wewnętrznej implementacji. Na szczęście jest na to proste rozwiązanie:

W powyższym kodzie przenieśliśmy metodę roboczą „doRealyFoo” do klasy na której tak naprawdę pracuje. Dzięki temu, z perspektywy klasy TestSubject2 nie interesują nas w ogóle „wnętrzności” klasy Baz, co zwalnia nas z łańcuchowego mockowania dziesiątek obiektów.

Czas na ostatni przypadek:

Mamy to kilka ciekawych rzeczy – metodę publiczną, która zwraca nam true/false w zależności od wyniku interakcji z webservice’m, oraz kilka metod oznaczonych jako protected – przygotowujących dane do wysłania do webservice’u. Dlaczego w ogóle potrzebujemy testować metody „doFoo1” oraz „doFoo2” ? Wynika to z faktu, że są one tak naprawdę głównym koniem roboczym tej publicznej metody. Gdyby metoda „callSomeWebService” wywoływała zamockowany webservice, który zawsze zwraca true, to tak naprawdę  nie jesteśmy w stanie jej sensownie przetestować. Oczywiście możemy je przetestować tak jak jest to pokazane w powyższym przykładzie – poprzez ustawienie modyfikatora dostępu protected. Nie jest to jednak rozwiązanie eleganckie, głównie dlatego, że wymaga naszej wiedzy o szczegółach implementacji metody „doFoo”.

Można to jednak bardzo łatwo zmienić:

Ekstrahujemy obie metody do osobnych klas, dzięki czemu są one niezależnie testowalne. Co więcej, dzięki temu zabiegowi, w przyszłości będziemy mogli małym nakładem pracy podmienić implementację obu metod gdyby się to okazało potrzebne.

  1. Rodzyn pisze:

    Ano i wszystko się sprowadza do dobrego „application logic decoupling” oraz „single responsibility principle” :)

    BTW.
    Mogłeś użyć w przykładach jakiś assercji zamiast if’ów i printów – mogłoby być czytelniej (a też chętnie bym zobaczył jak real testing w Scali wygląda :) )

  2. Ano mogłem 😛 ale chciałem, żeby to było jak najbardziej straightforward. Poza tym, solucji do testowania w Scali jest kilka, z tego co się orientuje, oprócz JUnit like, są też oparte na specyfikacjach takich jak w Rubym jest RSpec czy coś.

    Co do SRP i decouplingu to oczywiście masz racje – stosowanie tych technik daje zawsze dobry kod. Ja chciałem jednak pokazać dlaczego kod stworzony przy użyciu tych technik jest lepszy i jak wygląda typowy zły kod i co złego on robi :)

  3. wookieb pisze:

    O ile dobrze pamiętam to scala sama w sobie posiada narzędzie do testów.

  4. @wookieb – to fakt, jest paczka scala.testing i tam m.in SUnit :)

  5. Hitsu pisze:

    Ooo Scala, taki pomieszany Ruby z Javą. :)

    Te przykłady to napisałeś w Scali tak „4FUN”, mam rozumieć że nie korzystacie ze Scali w Sabre?

  6. @Hitsu – nie korzystamy ze Scali w Sabre. Natomiast ja osobiście bardzo intensywnie interesuje się tym językiem, dlatego uznałem, że był to dobry powód, żeby coś w Scali przy okazji po kodować :)

  1. There are no trackbacks for this post yet.

Leave a Reply

Informuj mnie o odpowiedziach poprzez e-mail. Możesz również subskrybować wpis bez zostawiania komentarza.