TDD jest be
Jakiś czas temu pisałem o moim stosunku do TDD. W skrócie – nie stosuję, ponieważ jak dotąd nie udało mi się znaleźć sensownego podejścia. W pobliżu nie widzę też żadnego guru TDD, który mógłby mnie oświecić w tej materii.
Tak samo mocno jak nie lubię TDD, lubię testy jednostkowe i testy w ogóle. Oczywiście te „w ogóle” to z takim mocnym „zależy od…”, ale testy jednostkowe są naprawdę w porządku. Jedyny zarzut, jaki mogę wystosować, niestety musi być adresowany do mnie – nie jestem jeszcze wystarczająco „płynny” w pisaniu kodu nadającego się do testowania jednostkowego. Raz na jakiś czas (coraz rzadziej, lecz jednak…) zdarza mi się zagwozdka. Szczególnie, jeżeli pracuję nad kodem zastanym, który bardzo ukierunkowuje myślenie i powoduje, że człowiek wpada w pewien, bardzo często błędny, schemat myślenia utarty przez programistę piszącego pierwotną wersję programu.
Co sprawia największy problem?
Problemy może sprawiać wszystko, ale jednym z największych wyzwań, z którymi musiałem się zmierzyć była interakcja z tzw. „systemem”. W jednym z odcinków podcastu „Ostra Piła” rozprawiano o abstrakcjach i mimo, że nie zgadzam się z niektórymi stwierdzeniami chłopaków, to poruszali oni tam właśnie problem, który dotknął także i mnie:
Zbyt sztywna interakcja z klasami systemowymi
Na przykład DirectoryInfo, FileInfo lub podobnymi. W tym wpisie chciałbym się jednak skupić na innym, nie mniej jednak ważnym elemencie (chciałem napisać klasie…):
DateTime
Na tematy związane z czasem, a szczególnie strefami czasowymi, można rozprawiać godzinami. Mędrsi ode mnie prelegowali na potęgę i nie zamierzam nawet próbować poruszać głębiej tego tematu, ponieważ idealnie pasuje do niego przenośnia, którą często stosuję w stosunku do kodu, z którym pracuję:
Wyobraź sobie, że ktoś poprosił Cię o wymianę deski w podłodze domku letniskowego. Pełen werwy i uzbrojony w niezbędne narzędzia idziesz na miejsce i zabierasz się do pracy. Chcesz to zrobić dokładnie, więc zamiast po prostu wymienić deskę, odrywasz ją i sprawdzasz stan legarów, na których się opiera. Po oderwaniu deseczki twoim oczom ukazują się przegniłe belki, na których spoczywała, a między nimi uwijające się w popłochu karaluchy. Ostrożnie odkładasz deskę na swoje miejsce, szybko przygotowujesz zamiennik, przybijasz go w miejsce starego elementu i cichutko wychodzisz z domku. Właściciel nie chce słyszeć o problemach, które ma.
I podobnie jest z oprogramowaniem, którego istotna część to np. współpraca z różnymi strefami czasowymi. Czasem nawet nie spodziewasz się, że rozpędzony taran zgłaszanych bugów za chwilę w ciebie uderzy – tylko dlatego, że nie wyobraziłeś sobie nawet połowy możliwych problemów – a te na pewno wystąpią.
Dlatego tak ważne jest testowanie
Testy mają dwojakie zadanie. Po pierwsze symulujesz nimi problemy, które wydają ci się możliwe do wystąpienia. Po drugie zaś będziesz nimi reprodukował problemy, które wystąpiły. Aby jednak do tego doszło, musisz w jakiś sposób rozwiązać…
Techniczne przeszkody uniemożliwiające testowanie
Spójrzmy na uproszczony przykład kodu, na który natknąłem się ostatnio:
Jak widać, kod jest bardzo prosty. W pierwszych linijkach sprawdzamy, czy czas określony przez zmienną TimeForNextPeriodicCheck jest jeszcze „w przyszłości”. Jeżeli tak, wychodzimy. Jeżeli nie, oznacza to że nadszedł czas na sprawdzenie mierników. Procedura ta zostanie przeprowadzona, do zmiennej TimeForNextPeriodicCheck zostanie przypisany nowy czas „w przyszłości” nastąpi wyjście z metody. Wątek powróci tutaj za jakiś czas, aby dokonać sprawdzenia po raz kolejny.
Niby nic ciekawego, ale nie gdy mamy zgłoszone problemy, że np. sprawdzenie mierników nie odbywa się wtedy, gdy powinno. W takim przypadku próbujemy napisać odpowiedni test:
Jest jednak pewien problem – ten test raz zadziała, a raz nie. Dlaczego? Ponieważ nie mamy żadnego wpływu na DateTime.Now. Co więcej – być może moglibyśmy wymusić uruchomienie kodu wewnątrz metody DoPeriodicCheckIfNeeded() umiejętnie dobierając wartość zmiennej TimeForNextPeriodicCheck (np. na DateTime.Now.AddMinutes(-1)), ale co jeżeli chcielibyśmy sprawdzić jakieś bardziej skomplikowane scenariusze? Jak np. zachowa się system w czasie. Przecież nie będziemy wołać metody Thread.Sleep…
Na ratunek – mockowanie DateTime
Do testowania właśnie takich przypadków potrzebujemy metod wpływu na wartość DateTime.Now. Jak to zrobimy? O tym już w następnym odcinku.
A co z Fakes albo TypeMock?
Jak na razie artykuł się jeszcze pisze. Akurat mam dość dużo zabawy z testowaniem zachowania w różnych godzinach i gdy tylko skończę postaram się to opisać. Myślałem o Fakes, z TypeMock nie pracowałem, ale sprawdzę. Dzięki za komentarz!
[…] chcemy i powinniśmy mockować DateTime ustaliliśmy w poprzednim odcinku. Dziś zajmiemy się odpowiedzią na […]
Cześć Pawle,
Tutaj kilka słów od Wujka Boba na temat poddawania się z TDD:
https://michalkulinski.blogspot.com/2017/06/testy-obywatele-pierwszej-kategorii.html
Myślę, że jeżeli masz pytania to Wujek odpowie na twoje pytania pod mailem: unclebob@cleancoder.com
Hej Michale, dzięki za komentarz 🙂
Że też chciało Ci się tłumaczyć tak długi elaborat Martina 🙂
Ale OK – to nadal niestety mnie nie przekonuje. W jednym ze swoich wpisów (chyba tym o książce Becka) zaznaczyłem, że wszyscy tak mówią o TDD, a jeszcze NIGDY w swojej nie tak wcale krótkiej karierze nie widziałem prawdziwego TDD w użyciu. Nigdy nie pracowałem z prawdziwym testowym wymiataczem, od którego mógłbym się tego nauczyć i przejąć jego nastawienie. Być może to jeszcze przede mną, a być może cały ten TDD to tylko mityczny garnek na końcu tęczy.
Oczywiście, że Martin propaguje gorąco TDD na wszelkie możliwe sposoby – w końcu tego typu działania to główne źródło jego zarobków – a moja sceptyczna dusza podpowiada mi różne scenariusze, gdy np. widzę pozytywne badania finansowane przez instytucję, która dany środek opracowała.
Czy Ty rzeczywiście zawsze korzystasz z TDD przy pisaniu oprogramowania? Z drugiej strony nawet nie wiem od czego zacząć wtedy pytania, których jest tak dużo i są tak bardzo związane ze specyfiką mojego projektu…