wtorek, 21 maja 2013

Więcej nie znaczy wolniej cz.5 - testowanie po raz drugi

i jeszcze jeden i jeszcze raz...

Komentarze (krytyka :) pod wczorajszym wpisem zmotywowały mnie do powałkowania tematu testowania jeszcze przez moment.

Na wstępie jednak pragnę zaznaczyć, że i tym razem nie będzie ani jednej linijki kodu. Ta decyzja wynika bezpośrednio z tego, o czym wczoraj pisałem w komentarzu pod wpisem, a mianowicie z samego założenia dotyczącego serii - nie ma to być opis technik/procesów, czy też opis samej ich idei, chodzi mi raczej o przedstawienie argumentów, które mogą pomóc Wam (oczywiście jeżeli będziecie mieli na to ochotę) przekonać managera czy też właściciela produktu do zastosowania opisywanych aktywności w projekcie.

Muszę pokornie pochylić głowę i przyznać, że rzeczywiście we wczorajszym poście zabrakło przykładów z życia wziętych. Był zbiór uwag (swoją drogą całkiem słusznych :), które jednak poza opisem nie zawierał żadnych odniesień do rzeczywistości, w której przyszło nam się zmagać z klientami, managerami, czy też innymi opornymi przedstawicielami idei minimalizmu w odniesieniu do ilości procesów w trakcie tworzenia oprogramowania.

krótka historia pewnego projektu

Dawno, dawno temu, gdy królowało przekonanie, że poza pisaniem kodu żadna inna aktywność w procesie tworzenia aplikacji nie jest wymagana powstał sobie system. System ów posiadał, poza całą swoją złożoną logiką, kilka raportów, które służyły do wyświetlania interesujących użytkownika danych i statystyk.
Po pierwszych tygodniach pracy, klient miał okazję zobaczyć pierwsze efekty pracy i to, co zobaczył spodobało mu się.

Ale to dopiero początek historii...

Pewnego dnia użytkownik stwierdził, że te wszystkie raporty bardzo fajne są i całkiem nieźle działają, ale przy tak wielkiej ilości danych koniecznością jest posiadanie filtrów na wszystkie pola tak, aby można było wyświetlić to (i tylko to), czym jest aktualnie zainteresowany.
Nic trudnego?
Osoba, która zaprojektowała mechanizm generowania raportów niestety jakiś czas temu odeszła z firmy, a wiedza na temat całej tej złożonej konstrukcji odeszła wraz z nią. Jednak taki drobny detal nie jest przecież w stanie zrazić wytrwałego i poszukującego ciągłych wyzwań zespołu deweloperów. Minęło kilka dni zapoznawania się z kodem i nadszedł w końcu czas na jego okiełznanie i dopisanie nowej funkcjonalności. Jak też postanowili, tak też zrobili. Po zaimplementowaniu nowości przeklikali GUI i wypuścili update dla klienta.
Zadowoleni z siebie czekali na kolejne wyzwania.

Dzień następny nie był jednak tak pozytywny, jak mogliby przypuszczać. Wynikły dwa problemy:
  • Biblioteka wykorzystywana do generowania raportów była używana w jeszcze jednym projekcie i okazało się, że zmiany przez nich wprowadzone sprawiły, że przy braku filtrów, mechanizm zaczyna zachowywać się dziwnie, co było równoznaczne ze skargami od klientów.
  • Użytkownicy aplikacji, nad którą pracował zespół zaczęli się skarżyć, że w niektórych, podstawowych przypadkach filtrowania, nie otrzymywali takich wyników, jakich mogli się spodziewać

Rozwiązanie problemu pierwszego było proste, a mianowicie przenieśli nowy kod do swojej aplikacji tworząc w ten sposób rozszerzenie biblioteki dla pojedynczego projektu.
Problem drugi okazał się bardziej skomplikowany i pochłonął dużo więcej czasu niż planowali. Poprawa działania funkcjonalności wymagała drobnych zmian, które jednak należało zrobić w głównej bibliotece. Tej niestety nie mogli ruszać, a więc kończyło się nadpisywaniem metod i modyfikowaniem ich działania. W dodatku każda zmiana i każdy update kończył się nowymi mailami od klienta skarżącego się na to, że coś zaczęło działać inaczej (efekt nadpisywania biblioteki).
Przy okazji walk z bugami zgłaszanymi przez klienta i analizie zapytań, które generowały raporty okazało się, że były w nich drobne błędy (czyli raport czasami generował niepoprawne wyniki). Najczęściej były to literówki, braki spacji itp., czyli typy błędów, które naprawdę łatwo popełnić.

Pewnego dnia klient wpadł na kolejny pomysł - nie chciał już dłużej statycznych stron i przeładowań za każdą zmianą, chciał żeby wszystko było dynamiczne.
Ech... cóż to jest zmienić widok?

Niestety okazało się to dużo bardziej problematyczne niż sądzili, ponieważ działanie biblioteki w dużej mierze zależało od komunikacji z widokiem, która odbywała się za pomocą różnorakich "magicznych" rozwiązań, co dodatkowo utrudniało debugowanie i próbę zrozumienia procesu komunikacji pomiędzy komponentami.
Po tygodniu walk z istniejącą biblioteką deweloperzy zdecydowali się, że najrozsądniej byłoby wprowadzić znaczne modyfikacje w jej kodzie. Jednak był jeszcze drugi projekt, który miał nadal korzystać ze starego widoku? Więc co teraz? Kolejne nadpisywanie per projekt? Napisanie od nowa?

W przedstawionej powyżej opowieści kilkukrotnie posiadanie testów ułatwiłoby wszystkim życie.

to, co? przeklikamy?

Po zaimplementowaniu nowej funkcjonalności przeklikali GUI
Niech pierwszy podniesie głos ten, kto nie spotkał się nigdy z taką formą "testowania" aplikacji. Jakie są podstawowe problemy z nią związane?
  • Skupiamy się przede wszystkim na nowej funkcjonalności. Oczywiście możemy również przeklikać wszystko w celu sprawdzenia, czy nie popsuliśmy czegoś po drodze, ale zazwyczaj byłoby to niezwykle czasochłonne, a po drugie, z racji tego, że funkcjonalność była implementowana sporo czasu temu, wiele rzeczy możemy pominąć.
  • Taki sposób testowania nie jest powtarzalny, czyli "testy" przeprowadzone dzisiaj będą najprawdopodobniej różniły się od tych przeprowadzanych jutro. W dodatku, z biegiem czasu, coraz mniejszą ilość (o ile w ogóle) będziemy ich przeprowadzać.

Gdyby były testy, obu powyższych problemów udałoby się uniknąć. Zawsze można byłoby odpalić zestaw istniejących testów, w celu skorygowania czy wszystko działa tak, jak przed zmianami, a w przypadku przyszłych modyfikacji można odpalić zestaw, który stworzyliśmy implementując aktualne zmiany. Dodatkowo, ich ilość raczej się nie zmniejszy, bardziej prawdopodobne, że się zwiększy wraz z odkrywaniem (lub raportowaniem przez użytkowników) nowych przypadków użycia.

problem z biblioteką

W przykładzie konsekwencje były tym bardziej dotkliwe, że biblioteka była współdzielona. Okazało się, że zmiany w niej wprowadzone sprawiają, że nie zachowuje się on poprawnie w przypadkach, które obsługiwała przed zmianami. Czyż testy nie uzmysłowiłyby tego programistom przed zrobieniem jakichkolwiek update'ów? W dodatku, o takich komplikacjach dowiedzieliby się z pewnością bardzo szybko, więc nie pisaliby więcej kodu, który okazał się problematyczny.

Kolejnym plusem wynikającym z pozyskania tej informacja odpowiednio wcześnie jest to, że mogłaby pociągnąć ponowne zaprojektowanie i implementację takiego rozwiązania, które pomimo nowej funkcjonalności działałoby poprawnie w obydwu projektach.
Tak się niestety nie stało, co wygenerowało dalsze problemy i utrudnienia:
  • Klasy i metody biblioteki zostały nadpisane, a więc nie posiadamy już tyle zaufania do jej interfejsów, ponieważ "po drodze" coś się mogło zmienić.
  • W przypadku, gdy zespół pracujący nad drugą aplikacją będzie chciał rozszerzyć funkcjonalność w ten sam sposób, będzie musiał on albo napisać rozszerzenie biblioteki dostosowane specjalnie pod projekt, albo przerobić kod z drugiego projektu (biorąc pod uwagę również zmiany w zachowaniu, które zostało nadpisane). Tak czy inaczej kończy się na dodatkowej pracy, której można było uniknąć.
    W przypadku zdecydowania się na zrobienia tego samego, co poprzednicy tworzą nam się dwie gałęzie rozwoju, które rozwiązują ten sam problem!

problem z funkcjonalnością

O ile brak obsługi pewnych przypadków nie jest żadnym kłopotem, bo jest to "jedynie" brakująca funkcjonalność, to problem zaczyna się wtedy, gdy byliśmy święcie przekonani, że je obsłużyliśmy. Mogło się okazać, że zrobiliśmy to nieprawidłowo, mogło się okazać, że tempo pracy sprawiło, że niektóre rzeczy przez nieuwagę zostały pominięte.

Jaki powód by to nie był, to gdyby kod był pokryty testami, to istnieje dużo większe prawdopodobieństwo, że przypomnielibyśmy sobie o zapomnianych scenariuszach, a w przypadku nieprawidłowych implementacji otrzymalibyśmy natychmiastową o tym informację.

klient nie informuje nas o skutkach naszych zmian

Wierzę, że niejeden z Was usłyszał chociaż raz zdanie w stylu: jeszcze wczoraj to działało i z pewnością uczucia, które Wam wtedy towarzyszyły nie należały do najprzyjemniejszych. W końcu klient chciał nowości, a oprócz nich dostał bugi w tym, co do tej pory dobrze działało.

Zestawy testów pozwalają nam zminimalizować ilość takich przypadków do bezpiecznego minimum. Oczywiście zawsze istnieje szansa, że otrzymamy od klienta taką niepożądaną wiadomość, ponieważ może poinformować nas o scenariuszu, którego nie testowaliśmy. Jednak z pewnością ilość takich informacji z jego strony spadnie do akceptowalnego poziomu.

naprawy - never ending story

W przykładzie, poprawki funkcjonalności pociągały za sobą coraz więcej zmian i odkrywały coraz więcej problemów (raporty generujące błędne dane). Każdy nowy update przynosił informacje o nowych błędach.
I niestety tak to często bywa. Jednym z powodów jest to, że użytkownicy po kilku nieudanych zmianach stają się bardziej krytyczni i na wszystko patrzą ostrożniej i dokładniej analizują, a co za tym idzie, rzeczy, które umknęłyby ich uwadze przy zwykłym użytkowaniu, wyrastają do rangi problemów, które trzeba rozwiązać natychmiast.
Istna nerwówka.

Oczywiście testy nie zapewnią, że funkcjonalność będzie zawsze w pełni (w rozumieniu klienta) zaimplementowana. Zazwyczaj czegoś brakuje i do tego trzeba się przyzwyczaić. Jednak błędy w istniejącej od dawna funkcjonalności, drobne, ale jakże znaczące, w większości przypadków udałoby się wyłapać nim klient miałby okazję ją wypróbować.

duża zmiana? Da się czy nie?

I ostatni punkt, czyli problem ze zmianą widoku wynikający ze zbyt wielu powiązań i relacji pomiędzy pozornie niezależnymi modułami.

Dobre testy naprawdę są w stanie pomóc Wam tworzyć łatwiejszy w rozwoju kod.
Jeżeli docieracie do momentu, gdy macie przetestować jedną klasę, ale zrobienie tego w odpowiedni sposób wymaga inicjowania setek innych obiektów i ustawiania środowiska, to znaczy, że warto byłoby coś w niej zmienić. Jakieś interfejsy? Podział odpowiedzialności?
Warto pamiętać, że testowanie jest również jednym z procesów, który zwiększa jakość projektu.

to już jest koniec?

Po wczorajszych komentarzach w mojej głowie powstała dłuższa historia, jednak patrząc na jej rozmiar zdecydowałem się na skrócenie jej o większą połowę. Postaram się jednak w najbliższym czasie napisać trochę więcej odnośnie testowania (najprawdopodobniej już z kodem :). Kilka pomysłów już mam:)

Chciałem jeszcze powiedzieć, że w powyższej opowieści czerpię ze swojego doświadczenia pełnymi garściami, więc nie jest ona wyssana z palca tylko po to, aby Was przekonać do tego, że testowanie jest ok. Wszystkie problemy rzeczywiście miały miejsce. Oczywiście pewne rzeczy zostały pozmieniane, ale tylko i wyłącznie te, które dla przykładu były całkowicie nieistotne.

Mam nadzieję, że tym razem udało mi się napisać trochę więcej konkretów i czekam na dalszą krytykę - motywuje ona tylko do tego aby pisać lepiej i więcej :)