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ść.