Nadeszła pora na cykl publikacji: „Historia pewnej refaktoryzacji”. Część 3.
Dotychczasowe działania refaktoryzacyjne doprowadziły do wydzielenia refaktoryzowanego kodu do oddzielnej klasy, w oddzielnym pliku. Aby bezpiecznie przeprowadzić dalsze modyfikacje konieczne jest uzyskanie wiarygodnego mechanizmu weryfikującego ich poprawność. Po prostu potrzebne są testy. W jaki sposób przetestować metody importujące dane? Najlepiej dokonać za ich pomocą importu i sprawdzić czy zaimportowany plik daje oczekiwane dane. Przyglądając się poszczególnym metodom można zauważyć, że wszystkie koniec końców importowany plik przetwarzają do postaci XML o takiej samej strukturze. Czyli wystarczy po imporcie sprawdzić czy wygenerowany XML jest zgodny z oczekiwaniami. Jeśli dodatkowo wszystkie importowane pliki spreparujemy w ten sposób, by zawierały te same dane, to dla wszystkich metod postać XML będzie taka sama. Wygląda na to, że proces testów ma duże szanse na automatyzację już na tym etapie – nie trzeba będzie niczego porównywać „naocznie”, a skorzystać z prostego porównania wygenerowanego XML-a z XML-em oczekiwanym. Pora zatem przygotować plik dla importu CSV (o nazwie csv.csv):
jeden,1 dwa,2 trzy,3 cztery,4 pięć,5
teraz plik dla importu z tabulacją jako separatorem (o nazwie txt.txt):
jeden 1 dwa 2 trzy 3 cztery 4 pięć 5
i plik importu o stałej długości danych (o nazwie fix.fix):
jeden 1 dwa 2 trzy 3 cztery 4 pięć 5
na koniec wypada przygotować plik z danymi w postaci binarnej (o nazwie bin.bin). Ponieważ trudno jest pokazać taki plik na stronie WWW, załóżmy że został przygotowany (np. za pomocą krótkiego programu w C#, który go wygenerował) – plik ten na koniec cyklu będzie dostępny w zasobach tego bloga (wraz z solucją zawierającą pełną historię refaktoryzacji).
Czy to wszystko? Nie, potrzebna jest jeszcze oczekiwana postać XML, ją też umieścimy w pliku (o nazwie – a jakże 😉 – xml.xml):
<data><r s="jeden" q="1"/><r s="dwa" q="2"/><r s="trzy" q="3"/><r s="cztery" q="4"/><r s="pięć" q="5"/></data>
Wyposażeni w dane wejściowe i oczekiwany wynik przystąpmy do budowania testów. Tu jednak napotkamy pewną trudność. Otóż konstruktor klasy Obiektu Metody:
public MethodObject(ProgressBar progressBar, OpenFileDialog openFileDialog, Store store) { this.progressBar = progressBar; this.openFileDialog = openFileDialog; this.store = store; }
wymaga parametrów o typach: ProgressBar i OpenFileDialog. Oba te typy, to klasy związane z interfejsem użytkownika, a przecież testy mają być automatyczne – nikt nie będzie ani wybierał pliku (on powinien być po prostu wskazany literałem w kodzie), ani przyglądał się paskowi postępu, którego zresztą nie ma gdzie pokazać – jedyne co warto pokazać to wynik testu. Czy da się coś z tym zrobić? Oczywiście :). Należy napisać atrapy tych klas, wyposażone jedynie w taką funkcjonalność, która będzie niezbędna do przeprowadzenia testów. Potem trzeba będzie jeszcze usunąć odwołanie do przestrzeni nazw System.Windows.Forms zastępując je własną przestrzenią, dajmy na to: MethodObjectMocks. Konieczne będzie też zdefiniowanie typu wyliczeniowego DialogResult – wystarczą mu dwa elementy.
Na początek zaimplementujmy klasę ProgressBar, jest ona wykorzystywana w refaktoryzowanych metodach w mniejszym zakresie, więc powinna się łatwiej dać napisać – będzie w sam raz na rozgrzewkę:
public class ProgressBar { public int Value { get; set; } public int Maximum { get; set; } }
i tyle? I tyle! Klasa nie ma robić nic konkretnego, po prostu ma udawać pierwowzór – w tej postaci zrealizuje to wyśmienicie.
Ok, w takim razie teraz OpenFileDialog. Tutaj będzie trochę więcej zabawy. Ponieważ klasa zależy od DialogResult, najpierw zdefiniujemy ten typ wyliczeniowy:
public enum DialogResult {None, OK}
Wystarczą dwa elementy, aby można było zwracać z metody ShowDialog() poprawność jej wykonania (OK) albo brak poprawności (None).
Jak zaimplementować samą klasę OpenFileDialog? Np. tak:
#region właściwości public string Filter { set { SelectFileName(value); } } public string FileName { get { return fileName; } } #endregion #region metody public DialogResult ShowDialog() { if (fileName == "") return DialogResult.None; else return DialogResult.OK; } private void SelectFileName(string filter) { if (filter.Contains("|*.csv")) fileName = "d:\\csv.csv"; else if (filter.Contains("|*.txt")) fileName = "d:\\txt.txt"; else if (filter.Contains("|*.fix")) fileName = "d:\\fix.fix"; else if (filter.Contains("|*.bin")) fileName = "d:\\bin.bin"; else fileName = ""; } #endregion #region pola private string fileName = ""; #endregion
Przyjąłem tutaj, że podczas ustalania filtra, który pierwotnie służył do wyselekcjonowania plików określonego typu, w klasie imitującej będzie podstawiany właściwy plik dla tego typu danych. Nie wydaje się to zbytnio komplikować klasy, a upraszcza sam wybór plików na potrzeby testów – alternatywą byłoby użycie konstruktora, w którym określałoby się każdorazowo testowany plik zanim obiekt klasy OpenDialog byłby przekazany do metody importującej.
Wygląda na to, że doprowadziliśmy wreszcie kod do stanu umożliwiającego jego testowanie. I to prawda – wygląda, bo kodu nadal nie da się przetestować :). Okazuje się, że pomiędzy jego liniami ukryło się wywołanie statycznej metody Show() klasy MessageBox. Umknęła ona uwadze podczas wydobywania zależności metod poza klasę MethodObject i w wyniku przeświadczenia, że wszystkie zależności wyrugowano, dopiero teraz zagrała nam na nosie. Niestety – testy znowu odsunęły się w czasie. Trzeba zająć się tą niesforną klasą i dopiero przejść do testów. Ale to już w następnym odcinku cyklu.
Na koniec warto szczególnie zwrócić uwagę, na to co się stało. Wszyscy ci, którzy z zapałem przystępują do refaktoryzacji i nic ich nie powstrzyma zanim nie skończą – zobaczcie! To była tylko jedna – wydawałoby się bezpieczna zmiana – a jednak coś umknęło uwadze! Uświadomcie sobie – ogarnięci zapałem zmian, zaliczycie o wiele więcej takich wpadek. W związku z tym przypomnę pewne powiedzenie – pośpiech jest wskazany … przy łapaniu pcheł ;). W refaktoryzacji potrzebna jest rozwaga, precyzja, określony z góry plan i trzymanie się zasad. Jedna z nich to – nie tykaj kodu póki nie masz testów.
Do przeczytania w kolejnej części cyklu ;).
Uzupełnienie
Jak słusznie w komentarzach zauważył Wojtek, kwestia zmiany przestrzeni nazw w pliku MethodObject.cs może dezorientować. Brakuje tutaj dość istotnej informacji, tj. co tak naprawdę dzieje się z tym plikiem w momencie rozpoczęcia implementacji testów. Otóż jest on kopiowany do nowego projektu, który jest przeznaczony działaniom refaktoryzacyjnym. Operuję więc na jego kopii i mogę ją dowolnie modyfikować. Celem refaktoryzacji jest uzyskanie tej samej funkcjonalności, ale inaczej zaimplementowanej, zatem z punktu widzenia projektu, z którego wywodzi się kod nie ma znaczenie gdzie i jak będzie on podlegał zmianom. Istotne jest, aby w rezultacie działał tak, jak początkowo. Na koniec czeka nas jeszcze dołączenie powstałych plików do pierwotnego projektu i sprawdzenie, że skompilowana aplikacja działa nadal tak samo.