Nadeszła pora na cykl publikacji: „Historia pewnej refaktoryzacji”. Część 4.
W poprzedniej części niniejszego cyklu mimo usilnych starań nie udało się doprowadzić do przygotowania testów mających kontrolować refaktoryzowany kod. W tej części – mogę to obiecać – testy wreszcie powstaną.
Przygotowania testów, zniweczyło występowanie we wszystkich czterech metodach wywołania statycznej metody Show() klasy MessageBox. Co począć z tym wywołaniem? Najbezpieczniejszym rozwiązaniem będzie rozszerzenie listy parametrów konstruktora o parametr klasy MessageBox, która – podobnie jak to się stało dla klas OpenFileDialog i ProgressBar – stanie się atrapą rzeczywistej klasy. Będzie się ona jednak różnić od pierwowzoru tym, że jej metoda nie będzie statyczna, więc klasa-atrapa będzie wymagać utworzenia instancji. Jest to oczywistym wyborem, bo uniezależnia kod od konkretnej klasy i – tak jak w pozostałych dwóch przypadkach – pozwala wstrzyknąć tę zależność. W klasie trzeba jeszcze dodać dodatkowe pole na potrzeby nowego parametru konstruktora. Kod klasy MessageBox będzie wyglądał tak:
public class MessageBox { #region właściwości public bool Correct { get { return correct; } } #endregion #region metody public void Show(string nothing) { correct = false; // skoro wywołano metodę, tzn. że kod wywołujący rozpoznał sytuację awaryjną } #endregion #region pola private bool correct = true; #endregion
i będzie się również znajdowała w przestrzeni nazw MethodObjectMocks.
Wygląda na to, że pozbyliśmy się ostatniej przeszkody na drodze do testów. W takim razie pora stworzyć pierwszy z nich. Do tego celu użyjemy mechanizmu testów jednostkowych dostarczanych wraz z Visual Studio 2010, będzie to najszybszy sposób uzyskania testów, bo nie będzie trzeba nic dodatkowego instalować. Dodajemy zatem do solucji kolejny projekt, wybierając z dostępnych szablonów Test Project -> Test Documents. Plik projektu nazywamy dowolnie, natomiast powstałemu plikowi CS zmieniamy nazwę na MethodObjectTest. Tak samo nazywamy wygenerowaną klasę, zaś jedyną jej metodę przemianowujemy na test_ImportCSV_method (podkreślenia w celu zwiększenia czytelności podczas przeglądania wyników testów). Główny fragment powstałego po tych modyfikacjach pliku powinien wyglądać tak:
[TestClass] public class MethodObjectTest { [TestMethod] public void test_ImportCSV_method() { } }
Zgodnie z zasadami pisania testów najpierw trzeba przygotować taki test, który nie daje poprawnego wyniku, następnie należy doprowadzić do tego, aby ten wynik był poprawny. No tak, ale w jaki sposób chcemy ustalać, że to co testujemy dało poprawny bądź niepoprawny wynik? Spójrzmy na kod metody, która będzie podlegała testowaniu:
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)) { string line; string[] values; progressBar.Value = 0; while (!reader.EndOfStream) { line = reader.ReadLine(); values = line.Split(','); if (values.Length == 2) xml += ""; 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 = "" + xml + ""; store(xml); }
Aha, ostatnim rozkazem jest utrwalenie zaimportowanych danych. No tak, naszym pomysłem na testy było porównywanie przekazywanego do metody store XML-a z oczekiwanym XML-em. Zatem to ta metoda będzie musiała współpracować z testem jednostkowym w celu uzyskania przez niego wyniku (porównania owych XML-i). Są tutaj dwa wyjścia – zapamiętywać przekazany do metody XML i porównywać go w metodzie testu, albo porównać XML-e na poziomie metody store i zapamiętać tylko wynik porównania. To drugie rozwiązanie podoba mi się bardziej, zatem zrealizuje je właśnie w ten sposób. Implementację metody store oraz niezbędne pole z rezultatem przechowam w klasie testującej, żeby już nie mnożyć bytów. Albowiem – warto wreszcie zwrócić na to uwagę – testy, które obecnie przygotowuję dla Obiektu Metody tak naprawdę w końcu zostaną zastąpione zupełnie innymi testami i koniec końców usunięte, nie warto zatem za bardzo się nad nimi pochylać, a jedynie doprowadzić do oczekiwanego stanu – weryfikatora poprawności refaktoryzacji.
Dodajmy zatem niezbędny kod do klasy MethodObjectTest:
#region metody /// <summary> /// implementacja metody utrwalania zaimportowanego pliku, tutaj służy do ustalania poprawności importu tegoż pliku /// </summary> ///dane z zaimportowanego pliku przekonwertowane do postaci XML public void Store(string xml) { correct = (expectedXml == xml); } #endregion #region pola private bool correct; private string expectedXml = ""; #endregion
Czego jeszcze brakuje? Inicjalizacji samej instancji klasy ObjectMethod. No i samej treści metody testującej. Zajmijmy się może najpierw nią, najlepiej gdyby wyglądała w ten sposób:
[TestMethod] public void test_ImportCSV_method() { Initialize(); MethodObject method = MethodObjectCreate(); method.ImportCSV(); Assert.IsTrue(correct); }
Hm, ale patrząc na treść metody ImportCSV() wyraźnie widać, że w pewnym przypadku – kiedy rozmiar importowanego pliku jest zerowy – kończy ona działanie przed czasem i nie dochodzi do wywołania metody store(). Owszem, ale tak samo dzieje się i wcześniej, jeśli nie uda się wybrać pliku (openFileDialog.ShowDialog() != DialogResult.OK). To jednak nie problem, albowiem wystarczy, że pole correct będzie właściwie zainicjowane przed wykonaniem testu (czyli ustawione na false – niepoprawne wykonanie), aby test zawsze miał wiarygodny przebieg. To z kolei skłania do weryfikacji klasy MessageBox – nie ma sensu, aby zawierała ona jakiekolwiek elementy diagnostyczne, trzeba ją zmodyfikować do następującej, o wiele prostszej, postaci:
public class MessageBox { public void Show(string nothing) { } }
Skoro wiadomo, jak ma wyglądać metoda testująca, to trzeba zaimplementować wywoływane w niej (jako pierwsze) dwie metody:
/// <summary> /// Inicjalizacja niezbędnych do testów bytów tj.: pobranie pliku z oczekiwanymi danymi importu i ustalenie wyniku testu jako niepoprawnego /// </summary> private void Initialize() { correct = false; expectedXml = File.ReadAllText("..\\..\\..\\xml.xml", Encoding.Default); } /// <summary> /// Utworzenie obiektu klasy MethodObject /// </summary> /// zwraca obiekt metody, która będzie testowana private MethodObject MethodObjectCreate() { ProgressBar progress = new ProgressBar(); MessageBox message = new MessageBox(); OpenFileDialog dialog = new OpenFileDialog(); MethodObject method = new MethodObject(progress, dialog, message, Store); return method; }
W międzyczasie wpadłem na pomysł, aby pliki z danymi do testowania, które utworzyłem w poprzedniej części włączyć do solucji, stąd ścieżka do pliku xml.xml jest ścieżką względną do folderu projektu testów jednostkowych. Tak samo muszę ścieżkę zdefiniować w metodzie SelectFileName klasy OpenFileDialog. Oto jej nowa postać (Folder jest stałą zdefiniowaną w tej klasie):
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 { fileName = ""; return; } fileName = Folder + fileName; }
Wreszcie nadeszła pora na pierwszy test – pamiętajmy, że ma on być na razie niepoprawny. Uzyskamy to eliminując na chwilę z metody testowej dwie środkowe komendy, tj.:
[TestMethod] public void test_ImportCSV_method() { Initialize(); //MethodObject method = MethodObjectCreate(); //method.ImportCSV(); Assert.IsTrue(correct); }
wyjątkowo eliminacja polega na zakomentowaniu kodu – jak tylko test zadziała natychmiast go odkomentujemy. Wybieramy z menu Test opcję Run – All tests in solution i naszym oczom u dołu okna Visual Studio ukazuje się wykonany test z niepoprawnym wynikiem.
Skoro test daje się wywołać, pora, aby zaczął pracować na naszą refaktoryzację. Usuwamy zatem komentarze i uruchamiamy test. Naszym oczom ukazuj się … Co? Niemożliwe! Import z pliku CSV nie przechodzi testów? O co chodzi? Cóż – rozwiązanie w kolejnej części cyklu – jeśli ktoś ma jakieś podejrzenia co do przyczyny, może się nimi podzielić w komentarzach. Na pewno można odrzucić takie przyczyny, jak brak pliku (bo byłby wyjątek, a nie nieprawidłowy wynik testu), czy zerową jego długość.