Nadeszła pora na cykl publikacji: „Historia pewnej refaktoryzacji”. Część 5.
W tej części cyklu przygotujemy testy dla wszystkich metod klasy MethodObject, ale najpierw musimy znaleźć powód niezaliczania pierwszego z przygotowanych testów – testu metody ImportCSV().
Najszybszym i najprostszym sposobem będzie skorzystanie z uruchamiania krokowego, skoro test nie jest zaliczany, to pierwszym, co należy sprawdzić jest kryterium jego zaliczania. Sprawdzimy zatem czy generowany XML nie różni się od XML-a oczekiwanego. Po ustawieniu pułapki w metodzie Store() klasy MethodObjectTest, uruchamiamy projekt testów jak normalną aplikację. Po zatrzymaniu uruchamiania w miejscu, gdzie następuje porównanie (expectedXml == xml) okazuje się, że rzeczywiście między XML-ami występuje różnica, zamiast oczekiwanego „pięć” jest „pi??”.
Oczekiwany XML (dla czytelności pominięto zgodne fragmenty):
<data><r s="jeden" q="1"/><r ... /><r s="pięć" q="5"/></data>
Uzyskany XML:
<data><r s="jeden" q="1"/><r ... /><r s="pi??" q="5"/></data>
To od razu nakierowuje na powód rozbieżności – nie jest uwzględniane kodowanie znaków w pliku, z którego generowany jest XML (system znakami zapytania daje do zrozumienia, że nie wie jakie to znaki). W związku z tym dokonujemy stosownej modyfikacji metody ImportCSV(), czyli w konstruktorze tworzącym strumień do pliku dodajemy dodatkowy parametr definiujący kodowanie (zakładamy, że będzie ono domyślne – tj. zgodne z tym w systemie operacyjnym).
public void ImportCSV() { openFileDialog.Filter = "Wartości separowane przecinkiem (*.csv)|*.csv"; if (openFileDialog.ShowDialog() != DialogResult.OK) return; FileInfo fileInfo = new FileInfo(openFileDialog.FileName); progressBar.Maximum = (int)fileInfo.Length; string xml = ""; using (StreamReader reader = new StreamReader(openFileDialog.FileName, Encoding.Default)) { string line; string[] values; progressBar.Value = 0; while (!reader.EndOfStream) { line = reader.ReadLine(); values = line.Split(','); if (values.Length == 2) xml += "<r s=\"" + values[0] + "\" q=\"" + values[1] + "\"/>"; progressBar.Value = progressBar.Value + line.Length + 2/*znak powrotu do początku linii + znak przejścia do nowej linii*/; } } if (xml.Length == 0) { messageBox.Show("Nie udało się przetworzyć pliku!"); return; } xml = "<data>" + xml + "</data>"; store(xml); }
Tym razem test metody zostaje zaliczony:
W tym momencie warto zwrócić uwagę na tę zaletę testów – czasami wyłapie się zupełnie nieoczekiwany błąd. Błąd, który mógł się objawić dopiero u klienta i to o wiele, wiele później. Gdyby sprawdzić dlaczego klient korzystający z tego importu do tej pory nie zgłosił takiego błędu, okaże się, że stosowane przez niego pliki nigdy nie zawierały znaków narodowych. Tutaj, zupełnie przypadkowo, odkryto wrażliwość metody na takie znaki. Ten przypadek należy jednak od razu przekuć na wiedzę i dodać do listy zaleceń budowy testów:
- przy porównywaniu tekstów zawsze powinien istnieć wariant porównywania takich, które zawierają znaki narodowe.
Pamiętając o tym, że metody są wynikiem „kopypasteryzmu” od razu poprawiamy w ten sam sposób wywołania konstruktorów w pozostałych dwu (bo trzecia używa innego sposobu odczytu, który jak się okazuje jest już przygotowany na znaki narodowe) i przygotowujemy dla nich testy. Najpierw dla metody ImportTabSeparated().
[TestMethod] public void test_ImportTabSeparated_method() { Initialize(); Assert.IsTrue(correct); }
Na razie brak tutaj właściwego testu – należy pamiętać, że test najpierw musi być niepoprawny. I od razu może wyjaśnię dlaczego. Dopóki testów jest niewiele po dodaniu kolejnego i wykonaniu wszystkich, nie będzie problemu z jego odnalezieniem na liście wykonanych testów, aby potwierdzić, że z pewnością jest obejmowany przez mechanizm testów. Ale kiedy testów jest więcej zaczyna być to coraz trudniejsze. Należy też pamiętać, że testy są zautomatyzowane, czyli mogą być uruchamiane w sposób całkowicie automatyczny i jedynym sygnałem ich przebiegu będzie informacja, czy wykonały się poprawnie, czy nie. Zatem dodanie nowego testu, który od razu generuje niepowodzenie jest odpowiednim sygnałem, że jest on wykonywany. Po co to w ogóle sprawdzać ktoś spyta? Jak to – może być niewykonany? Ano takto:
public void test_ImportTabSeparated_method() { Initialize(); Assert.IsTrue(correct); }
Czy widać czym różni się ta metoda od poprzedniej? Jednym drobnym szczegółem, który może umknąć uwadze. Brakuje tutaj atrybutu [TestMethod], a to wystarczy, aby test nie został uwzględniony podczas uruchamiania wszystkich testów. Zatem – pisząc test, który nie jest poprawny, sprawdzamy – paradoksalnie – poprawność jego włączenia do systemu testów ;).
Uruchamiamy zatem nasz zestaw testów (obecnie tyko dwa) i widzimy, że jeden przeszedł, a drugi nie. Spokojni, że zestaw testów jest kompletny przechodzimy do zaimplementowania poprawnego testu.
[TestMethod] public void test_ImportTabSeparated_method() { Initialize(); MethodObject method = MethodObjectCreate(); method.ImportTabSeparated(); Assert.IsTrue(correct); }
Jak widać test jest bardzo podobny do poprzedniego, kolejny też taki będzie – ale niech nikogo nie kusi, żeby od razu napisać obydwa. Zasada jest taka, że jeden test, jedno testowanie, kolejny test, kolejne testowanie. To tak jak z jedzeniem zupy, nie używa się dwóch łyżek, żeby szybciej zjeść ;). Wyposażeni w tę wiedzę uruchamiany zatem kolejną sesję testów i z przyjemnością stwierdzamy, że wszystkie testy zostały zaliczone. Jest to tym większa przyjemność, że zmiany wprowadzone do pozostałych metod związane z kodowanie znaków narodowych rzeczywiście były zasadne (to oczywistość, ale to oczywistości zazwyczaj umykają naszym … oczom ;)).
Tam sam scenariusz powtarzamy dla testu metody ImportFixed() czyli: najpierw nieprzechodzący test, potem przechodzący. Metoda dla tego drugiego przyjmie postać:
[TestMethod] public void test_ImportFixed_method() { Initialize(); MethodObject method = MethodObjectCreate(); method.ImportFixed(); Assert.IsTrue(correct); }
Ona też jest podobna do poprzednich dwóch metod testujących i podobnie jak one zaliczeniem testu potwierdza zasadność uwzględnienia kodowania znaków w metodzie testowanej.
Pora domknąć zestaw testów o test ostatniej metody, tj.: ImportBinary(). Rutynowo (tak – to już się robi rutyną ;)) najpierw przygotowujemy test dający negatywny wynik (dla wszystkich przypadków zawartość metody testującej nieprawidłowo jest identyczna, co akurat ułatwia zadanie), następnie – bardzo podobny do poprzednich testów ostatni, poprawny test.
[TestMethod] public void test_ImportBinary_method() { Initialize(); MethodObject method = MethodObjectCreate(); method.ImportBinary(); Assert.IsTrue(correct); }
Uruchamiamy zestaw testów, aby zobaczyć wszystkie zaliczone testy i … test nie przechodzi! Dlaczego? To jakaś klątwa? Zaczynaliśmy testy i nie działały, więc skoro kończymy to też muszą nie działać? Gusła i zabobony na bok ;), skoro poprzednio sobie poradziliśmy to i teraz trzeba zastosować tę samą metodę, która powiodła nas do sukcesu. Ustawiamy ponownie pułapkę w metodzie Store(). Dezaktywujemy pozostałe testy w ten sposób, że eliminujemy dwa środkowe rozkazy (tworzymy testy z negatywnym wynikiem) – aby testy te nie przedłużały sesji debugowania. I uruchamiamy projekt testów jak normalną aplikację. Tu mała uwaga. Zdecydowanie lepiej jest stworzyć sobie oddzielny projekt, w którym będziemy śledzić takie problematyczne przypadki, dzięki czemu nie zmieniamy raz napisanych i użytych testów – bo to może doprowadzić do nieprzewidzianych skutków (zmieniamy testy – przecież nie będziemy dla nich pisać testów ;)). W tym wypadku – żeby nie przedłużać – zrobimy wyjątek.
Po uruchomieniu okazuje się, że program zakończył działanie nie wchodząc do metody Store(). W takim razie ustawiamy pułapkę w metodzie ImportBinary(). Okazuje się, że kończy ona swoje działanie już w momencie emulowania działania klasy OpenFileDialog. Przyglądamy się metodzie SelectFileName() klasy emulującej, zerkamy z powrotem na metodę ImportBinary(). Widać różnicę? Tak – ImportBinary() używa filtra (*.dmp), który nie jest uwzględniony w SelectFileName(). Mając zafiksowane w głowie przez nazwę metody (ImportBinary), że importujemy pliki binarne, nie sprawdziliśmy, że mają one jednak rozszerzenie inne niż bin. Należy więc dodać taki filtr do rzeczonej metody, dokonać kopii pliku bin.bin na dmp.dmp i załączyć go od projektu.
private void SelectFileName(string filter) { if (filter.Contains("|*.csv")) fileName = "csv.csv"; else if (filter.Contains("|*.txt")) fileName = "txt.txt"; else if (filter.Contains("|*.fix")) fileName = "fix.fix"; else if (filter.Contains("|*.bin")) fileName = "bin.bin"; else if (filter.Contains("|*.dmp")) fileName = "dmp.dmp"; else { fileName = ""; return; } fileName = Folder + fileName; }
Nie usuwamy przypadku dla *.bin, być może się przyda – jeść w każdym bądź razie nie woła ;). Przywracamy pełną postać wszystkich testów i uruchamiamy zestaw. Obecnie wszystkie cztery testy przechodzą – możemy sobie wreszcie pogratulować – nasz kod jest gotowy do refaktoryzacji.
Na koniec warto uściślić kilka spraw. Po pierwsze – używamy tutaj mechanizmu testów, ale wykonywane testy nie są testami jednostkowymi. Test jednostkowy musi być rzeczywiście jednostkowy, testować jakąś jednostkę programu, tutaj tych jednostek jest więcej – nie możemy metody importu traktować w ten sposób. Zauważmy, że oddzielnie powinny być testowane przypadki typu – nieznaleziony plik (co wyszło przy ostatnim teście), wrażliwość na różnego typu dane wejściowe, itd. My testujemy całość – kilka jednostek, bo te testy są nam potrzebne, aby kontrolować przebieg refaktoryzacji.
Po drugie – zauważmy, że w toku przygotowywania testów powstawał ich kod oraz kod wspomagający testy i każdy z tych kodów podlegał modyfikacjom. Trzeba sobie zdawać sprawę, że takie zmiany mogą prowadzić do błędów. Oczywiście – jak już wspomniałem – nie ma sensu testować kodu testów. Po prostu błędy w tymże kodzie wyjdą przy okazji wykonywania samych testów, po prostu po sprawdzeniu, że błędu nie ma w testowanych metodach, poszuka się go w metodach testujących. Na szczęście – kiedy już testy będą działać poprawnie – dopóki nie zmodyfikujemy kodu testów, dopóty ich jakość nie ulegnie pogorszeniu i jedyne miejsce, w którym będzie można oczekiwać błędów, to testowany przez nie kod.
W tym wpisie to tyle, w kolejnym w końcu przyjrzymy się samym metodom importu i spróbujemy jakoś je zunifikować oraz przebudować. Zmieni się ich kod, powstanie nowy, dla nowo powstałego powstaną kolejne testy – zapowiada się mnóstwo dobrej zabawy ;).