Mockowanie systemu #1 – Wstęp

5
880

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.