Pora po raz kolejny napisać testy dla uzyskanego kodu. Zapewne niektórzy zaczynają być znużeni tą ciągłą potrzebą pisania testów. Cóż – jest to jedyny sposób na zapewnienie odpowiedniej jakości kodu. A pisząc „odpowiedniej” mam na myśli jedynie jego poprawność.

Na pocieszenie uchylę rąbka tajemnicy – nasz kod jest coraz lepszy, coraz bardziej elastyczny, a to przekłada się także na pisanie testów. W poprzednich testach udało się wykorzystać jedynie dane użyte w testach wcześniejszych. Obecnie uda się wykorzystać także w dużej mierze kod – wystarczą jedynie drobne modyfikacje i uzyskamy całkiem przyzwoity zestaw testów.

Ponieważ w części dziesiątej cyklu odkryłem, że kod odpowiedzialny za powiadamianie o postępie przetwarzania był nieprawidłowy, w obecnych testach dodamy również takie, które zapobiegną podobnym nieprawidłowościom w przyszłości.

Skoro w obecnych testach wykorzystamy kod testów poprzednich – przypomnijmy go sobie:

[TestClass]
public class SeparatedFileReaderTest
{
	#region metody testowe
	[TestMethod]
	public void test_Comma_Separated_File_Import()
	{
		ImportTest("csv.csv", ',');
	}

	[TestMethod]
	public void test_Tab_Separated_File_Import ()
	{
		ImportTest("txt.txt", '\t');
	}

	[TestMethod]
	public void test_Semicolon_Separated_File_Import()
	{
		ImportTest("ssv.ssv", ';');
	}
	#endregion
	#region metody
	private void ImportTest(string fileName, char separator)
	{
		actual = "";
		ProgressMock progress = new ProgressMock();
		SeparatedFileReader reader = new SeparatedFileReader(Folder + fileName, separator, StoreContent, progress);
		reader.Read();
		Assert.AreEqual(expected, actual);
	}
	public void StoreContent(params string[] values)
	{
		actual += values[0] + values[1];
	}
	#endregion
	#region pola
	private string actual;
	#endregion
	#region stałe
	const string expected = "jeden1dwa2trzy3cztery4pięć5";
	const string Folder = @"..\..\..\";
	#endregion
}

Obecnie mamy całkowicie inne klasy, ale schemat testu jest identyczny. Na czym polega test? Na odczycie pliku i jego porównaniu z wartością oczekiwaną. Realizuje to następujący fragment kodu:

actual = "";
reader.Read();
Assert.AreEqual(expected, actual);

Obecnie kod ten przyjmie postać:

private void AnyImport(FileOfValuesReader reader)
{
	actual = "";
	reader.Process();
	Assert.AreEqual(expected, actual);
}

Jak będzie wyglądała metoda ImportTest, jeśli użyjemy klasy FileOfSeparatedValuesReader i powyższe metody (AnyImport)?

private void ImportSeparated(string fileName, char separator)
{
	AnyImport(new FileOfSeparatedValuesReader(Folder + fileName, separator, StoreContent, new ProgressMock()));
}

Testy poszczególnych plików (różniących się separatorem) będą zatem zaimplementowane następująco:

[TestMethod]
public void test_Comma_Separated_File_Import()
{
	ImportSeparated("csv.csv", ',');
}

[TestMethod]
public void test_Tab_Separated_File_Import()
{
	ImportSeparated("txt.txt", '\t');
}

[TestMethod]
public void test_Semicolon_Separated_File_Import()
{
	ImportSeparated("ssv.ssv", ';');
}

W przypadku pozostałych formatów, ich testy będą równie proste. Plik o ustalonej długości będzie miał taki:

[TestMethod]
public void test_Fixed_File_Import()
{
	AnyImport(new FileOfFixedValuesReader(Folder + "fix.fix", StoreContent, new ProgressMock()));
}

a plik binarny taki:

[TestMethod]
public void test_Fixed_File_Import()
{
	AnyImport(new FileOfFixedValuesReader(Folder + "fix.fix", StoreContent, new ProgressMock()));
}

[TestMethod]
public void test_Binary_File_Import()
{
	AnyImport(new BinaryFileOfValuesReader(Folder + "dmp.dmp", StoreContent, new ProgressMock()));
}

Patrząc na powyższe metody uderza w oczy ich przejrzystość – sprowadzają się zaledwie do jednej linii kodu, która jest prosta i czytelna. Wynika z niej, że aby wykonać dowolny import należy przekazać poprzez parametr (wstrzyknięcie do metody) do metody importujące obiekt konkretnej klasy importującej. Tu warto przypomnieć kolejna zaletę testów – są one doskonałą dokumentacją sposobu użycia klas, nie potrzeba żadnych dokumentacji, skrupulatnie opisujących jak używać klasy – test pokaże to o wiele dokładniej w myśl maksymy: „jeden przykład wart tysiąca słów”. Zatem chcesz pokazać mi jak używać kodu – pokaż mi jego testy ;).

Kolejną zaletą testów jest także ocenienie ergonomii testowanych klas. Jeśli test łatwo jest napisać, jego kod jest czytelny, to klasa jest ergonomiczna (czyli wygodna w użyciu). W przypadku powyższych testów ergonomia jest zadowalająca. Oczywiście to nie jedyne kryterium, jakie powinna spełniać dobrze napisana klasa, ale wiemy już, że akurat to jedno jest już spełnione.

Odkrywając zalety testów, nie zapominajmy o ich podstawowym przeznaczeniu – mają sprawdzać poprawność napisanego kodu. Uruchamiamy je więc – z pewnością każdy zostanie zaliczony ;).

Udało nam się wykorzystać kod z poprzednich testów. Jednak testy dotyczące raportowania postępu przetwarzania pliku trzeba będzie dopiero napisać. Na szczęście będą one bardzo podobne do testów samego importu. Trzeba będzie przeprowadzić – tak jak w dotychczasowych testach – import, ale tym razem nie badać jego poprawności, ale poprawność przetworzenie oczekiwanej ilości danych. Oznacza to tylko tyle, że kryterium testu będzie sprawdzenie, czy wartość podana we właściwości Expected klasy ProgressMock, jest równa wartości właściwości Completed tejże klasy, czyli:

reader.Process();
Assert.AreEqual(progress.Expected, progress.Completed);

Napiszmy więc testy dla wszystkich rodzajów importu:

[TestMethod]
public void test_Separated_File_Import()
{
	ProgressMock progress = new ProgressMock();
	FileOfValuesReader reader = new FileOfSeparatedValuesReader(Folder + "csv.csv", ',', StoreContent, new ProgressMock());
	reader.Process();
	Assert.AreEqual(progress.Expected, progress.Completed);
}

[TestMethod]
public void test_Fixed_File_Import_Progress()
{
	ProgressMock progress = new ProgressMock();
	FileOfValuesReader reader = new FileOfFixedValuesReader(Folder + "fix.fix", StoreContent, new ProgressMock());
	reader.Process();
	Assert.AreEqual(progress.Expected, progress.Completed);
}

[TestMethod]
public void test_Binary_File_Import_Progress()
{
	ProgressMock progress = new ProgressMock();
	FileOfValuesReader reader = new BinaryFileOfValuesReader(Folder + "dmp.dmp", StoreContent, progress);
	reader.Process();
	Assert.AreEqual(progress.Expected, progress.Completed);
}

Powyższe testy okrywają mankament testowanych klas w obecnej ich postaci. Aby móc przetestować ich poboczną funkcjonalność – jaką jest raportowanie postępu, zmuszeni jesteśmy testować niejako sam import. Dobrze byłoby, gdyby import był udawany, dzięki czemu jego ewentualne błędy nie miałyby wpływu na właściwy test. Dokonując dalszej refaktoryzacji kodu klas należy mieć na uwadze tę niedogodność.

Testy oczywiście dodajemy do klasy testującej kolejno i uruchamiany po dodaniu każdego z osobna (pamiętając, aby tuż po dodaniu – przy pierwszym teście – dawał on wynik negatywny). Okazuje się, że pierwsze dwa testy dają wynik pozytywny, niestety ostatni nie jest zaliczany. Wg informacji zwracanych przez test, ilość przetworzonych danych jest większa od ilości oczekiwanych. Zerkamy zatem do metody klasy BinaryFileOfValuesReader odpowiedzialnej za naliczanie przetworzonych danych:

protected override void Read()
{
	buffer = new byte[20 + 4];
	endOf = (reader.Read(buffer, 0, buffer.Length) == 0);
	completed += buffer.Length;
}

To co w niej uderza, to fakt, że w przypadku uzyskania pozytywnej wartości pola endOf i tak nastąpi naliczenie kolejnej porcji przetworzonych bajtów. Jest to niedopuszczalne, zatem zmieniamy kod na następujący:

protected override void Read()
{
	buffer = new byte[20 + 4];
	endOf = (reader.Read(buffer, 0, buffer.Length) == 0);
	if (endOf)
		return;
	completed += buffer.Length;
}

Ponownie uruchamiamy testy i okazuję się, że obecnie wszystkie są zaliczane. Skoro tak, to w następnej części postaramy się doprowadzić wreszcie mechanizm importu do postaci końcowej. A na koniec podaje jeszcze kompletny kod klasy testującej:

[TestClass]
public class FileOfValuesReaderTests
{
	#region metody testowe
	[TestMethod]
	public void test_Comma_Separated_File_Import()
	{
		ImportSeparated("csv.csv", ',');
	}

	[TestMethod]
	public void test_Tab_Separated_File_Import()
	{
		ImportSeparated("txt.txt", '\t');
	}

	[TestMethod]
	public void test_Semicolon_Separated_File_Import()
	{
		ImportSeparated("ssv.ssv", ';');
	}

	[TestMethod]
	public void test_Fixed_File_Import()
	{
		AnyImport(new FileOfFixedValuesReader(Folder + "fix.fix", StoreContent, new ProgressMock()));
	}

	[TestMethod]
	public void test_Binary_File_Import()
	{
		AnyImport(new BinaryFileOfValuesReader(Folder + "dmp.dmp", StoreContent, new ProgressMock()));
	}

	[TestMethod]
	public void test_Separated_File_Import()
	{
		ProgressMock progress = new ProgressMock();
		FileOfValuesReader reader = new FileOfSeparatedValuesReader(Folder + "csv.csv", ',', StoreContent, new ProgressMock());
		reader.Process();
		Assert.AreEqual(progress.Expected, progress.Completed);
	}

	[TestMethod]
	public void test_Fixed_File_Import_Progress()
	{
		ProgressMock progress = new ProgressMock();
		FileOfValuesReader reader = new FileOfFixedValuesReader(Folder + "fix.fix", StoreContent, new ProgressMock());
		reader.Process();
		Assert.AreEqual(progress.Expected, progress.Completed);
	}

	[TestMethod]
	public void test_Binary_File_Import_Progress()
	{
		ProgressMock progress = new ProgressMock();
		FileOfValuesReader reader = new BinaryFileOfValuesReader(Folder + "dmp.dmp", StoreContent, progress);
		reader.Process();
		Assert.AreEqual(progress.Expected, progress.Completed);
	}
	#endregion
	#region metody
	private void ImportSeparated(string fileName, char separator)
	{
		AnyImport(new FileOfSeparatedValuesReader(Folder + fileName, separator, StoreContent, new ProgressMock()));
	}

	private void AnyImport(FileOfValuesReader reader)
	{
		actual = "";
		reader.Process();
		Assert.AreEqual(expected, actual);
	}

	public void StoreContent(params string[] values)
	{
		actual += values[0] + values[1];
	}
	#endregion
	#region pola
	private string actual;
	#endregion
	#region stałe
	const string expected = "jeden1dwa2trzy3cztery4pięć5";
	const string Folder = @"..\..\..\";
	#endregion
}

public class ProgressMock : IProgressNotifier
{
	public int Expected { get; set; }
	public int Completed { get; set; }
}