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
- Testując dany obiekt sprawdzam jego stan po wywołaniu danej metody:
- Testując dany obiekt tworzymy mocki obiektów od których jest zależny w sposób łańcuchowy
- 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:
class Bar(var i: Int) {}class TestSubject1 { def doFoo(bar: Bar){ bar.i = 10 } }def testCase1() { val bar = new Bar(0) val testedObject = new TestSubject1 testedObject.doFoo(bar) if(bar.i == 10){ println("true") } else { println("false") } }
view raw
gistfile1.scala
This Gist brought to you by
GitHub.
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:
class TestCase1refactored { def doFoo(bar: Bar): Bar = { val result = new Bar(bar.i + 10) return result } }def testCase1refactored() { val bar = new Bar(0) val testedObject = new TestSubject1refactored() val result = testedObject.doFoo(bar) if(result.i == 10){ println("true") } else { println("false") } }
view raw
gistfile1.scala
This Gist brought to you by
GitHub.
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:
Czytaj dalej tutaj (rozwija treść wpisu)
Czytaj dalej na blogu autora...
Zwiń
Czytaj na blogu autora...