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.

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