Jak obiecałem – dziś uzupełnimy dotychczasowy zestaw testów o nowe testy. Co będziemy testować? Jeśli ktoś uważnie śledzi ten cykl zapewne oczekuje, że – zgodnie z wcześniejszymi zapowiedziami – przygotujemy test sprawdzający uruchamianie wszystkich metod biorących udział w przetwarzaniu pliku oraz dostosujemy stare testy sprawdzające powiadamianie o postępie przetwarzania. Tak – takie testy zostaną napisane. Ale najpierw utworzymy testy, których do tej pory nie było. Proszę zauważyć, że powstał nam zestaw klas realizujących wyodrębnianie wartości. Klasy te są niezależne od procesu przetwarzania plików – tym samym zyskaliśmy większą swobodę ich użycia – czyli także testowania. Nie jesteśmy obecnie zmuszeni pobierać pliku, aby sprawdzić czy mechanizm wyodrębniania wartości funkcjonuje zgodnie z oczekiwaniami. Wystarczy bowiem napisać tak:

[TestMethod]
public void test_SeparatedTextValuesExtractor()
{
	SeparatedTextValuesExtractor extractor = new SeparatedTextValuesExtractor(',');
	extractor.Content = "1, 2, 3, 4, 5, 6, abc, def";
	extractor.Process();
	actual = "";
	foreach (AnyValue item in extractor.Values)
		actual += item.ToString();
	Assert.AreEqual("1 2 3 4 5 6 abc def", actual);
}

i już mamy test badający klasę interpretującą wartości rozdzielane separatorem. Takich testów należy przygotować kilka, tj. używających różnych separatorów oraz różnej ilości separowanych wartości. Ja ograniczę się tylko do jednego testu.

W podobny sposób można przetestować pozostałe dwie klasy wyodrębniające wartości. A ponieważ wszystkie implementują wspólny interfejs, skorzystamy z tego udogodnienia i przygotujemy wspólną metodę testującą (podobnie jak w przypadku testu plików). Oto jak będą zatem przedstawiały się trzy metody testujące trzy klasy wyodrębniające wartości:

#region testy jednostkowe klasy wyodrębniających wartości
[TestMethod]
public void test_SeparatedTextValuesExtractor()
{
	SeparatedTextValuesExtractor extractor = new SeparatedTextValuesExtractor(',');
	extractor.Content = "1,2,3,4,5,6,abc,def";
	AnyExtract(extractor, "123456abcdef");
}

[TestMethod]
public void test_FixedTextValuesExtractor()
{
	FixedTextValuesExtractor extractor = new FixedTextValuesExtractor(1, 1, 1, 1, 2, 3, 4);
	extractor.Content = "abc1 2  3   4";
	AnyExtract(extractor, "abc1234");
}

[TestMethod]
public void test_String20AndIntValuesExtractor()
{
	String20AndIntValuesExtractor extractor = new String20AndIntValuesExtractor();
	extractor.Content = new byte[] 
	{
		0x31, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
		0x02, 0x00, 0x00, 0x00
	};
	AnyExtract(extractor, "12");
}
#endregion

private void AnyExtract(IValuesExtractor extractor, string expected)
{
	actual = "";
	extractor.Process();
	foreach (AnyValue item in extractor.Values)
		actual += item.ToString();
	Assert.AreEqual(expected, actual);
}

Warto zwrócić uwagę, że dla wartości separowanych zachowywane są występujące w nich spacje, natomiast dla tych o ustalonej długości spację są usuwane. Dlatego pierwszy przykład testu posiada w oczekiwanym łańcuchu spacje.

Wraz z powyższymi testami w naszych zautomatyzowanych testach zagościły rasowe testy jednostkowe. Każdy z tych testów sprawdza bowiem niezależną jednostkę programu – niezależną ponieważ nie wymaga ona do pracy żadnych dodatkowych bytów (na danym poziomie abstrakcji). W przypadku naszych dotychczasowych testów sprawdzana była całość funkcjonalności, każdy z wchodzących w jej skład elementów mógł być zatem źródłem błędów – testy te można właściwie potraktować jako testy integracyjne – tj. sprawdzające całość mechanizmu integrującego poszczególne, prostsze mechanizmy.

Ktoś może zapytać: czy w takim razie analogicznie nie powinniśmy napisać testów jednostkowych dla klas odczytujących pliki? Teoretycznie tak, ale … de facto te testy i tak są realizowane przy okazji testowania funkcjonalności importu. Oczywiście można by przygotować atrapę klasy wyodrębniającej dane i testować sam odczyt, ale moim zdaniem byłby to przerost formy nad treścią. Jeśli testy klas wyodrębniających dadzą wynik pozytywny, to w przypadku negatywnych testów funkcjonalności importu wiadomym będzie, że błędu należy szukać w kodzie klas czytających plik. Jest to swoisty kompromis pomiędzy podejściem purystycznym, a ignorowaniem testów w ogóle – taki programistyczny pragmatyzm. Oczywiście, gdyby klasy importu były bardziej złożone, wówczas taki kompromis nie byłby już kompromisem, ale proszeniem się o kłopoty. Pamiętajmy – wszystko zależy od kontekstu, otoczenia – wszystko jest względne ;). Zatem zawsze trzeba się do kontekstu dostosować. Nie umycie rąk przed pieleniem grządek nie jest tym samym co nie umycie rąk przed jedzeniem ;).

Do napisania zostały jeszcze – przypomniane na początku – testy. Zacznijmy od testów powiadamiania o postępie. I tutaj miłe zaskoczenie – dokonana dekompozycja klas sprawiła, że nie trzeba pisać kilku testów (dla każdego importu), a jedynie jeden. Aby test mógł zostać napisany będziemy potrzebowali atrapy klasy realizującej odczyt pliku (dziedziczącej FileOfValuesReader). Będziemy także potrzebowali atrapy implementującej interface IValuesExtractor, ponieważ wymaga go klasa odczytująca plik. Najpierw przedstawię tę drugą:

public class ValuesExtractorMock : IValuesExtractor
{
	#region właściwości
	public AnyValue[] Values { get; protected set; }
	#endregion
	#region metody
	public virtual void Process()
	{
	}
	public void Transfer(AnyValue[] values)
	{
	}
	#endregion
}

Jak widać nie robi ona kompletnie nic, ale właśnie taka jest wystarczająca dla pierwszej klasy-atrapy. Kod tejże będzie miał następującą postać:

public class FileOfValuesReaderMock : FileOfValuesReader
{
	#region konstruktory
	public FileOfValuesReaderMock(int initialSize, ValuesExtractorMock extractor, Transfer transfer, IProgressNotifier progressNotifier)
		: base("", extractor, transfer, progressNotifier)
	{
		size = initialSize;
		coundown = size;
	}
	#endregion
	#region właściwości
	protected override int Size { get { return size; } }
	protected override int Readed { get { return readed; } }
	#endregion
	#region metody
	protected override void Open()
	{
	}

	protected override void Read()
	{
		if (coundown > 0)
			readed = 1;
		else
			readed = 0;
		coundown--;
	}

	protected override void Close()
	{
	}
	#endregion
	#region pola
	private int readed, size, coundown;
	#endregion
}

W przypadku tej klasy puste będą metody: otwierająca plik i zamykająca go – żaden plik w testach nie będzie bowiem brał udziału. Nie testujemy bowiem przetwarzania pliku, ale jedynie prawidłowe powiadamianie o postępie tegoż przetwarzania. Ponieważ postęp wymaga poprawnych wartości właściwości zwracającej rozmiar pliku i ilość odczytanych jednostek danych, zatem te właściwości muszą być zaimplementowane. Cały mechanizm pozwalający przetestować postęp zawarty jest w metodzie Read(). Odlicza ona do zera zadany w konstruktorze rozmiar i dopóki ta wartość nie zostanie osiągnięta udaje, że odczytano jedną jednostkę danych. Sam test będzie zaimplementowany następująco:

[TestMethod]
public void test_FileOfValuesReader_Progress_Notification()
{
	ValuesExtractorMock extractor = new ValuesExtractorMock();
	ProgressMock progress = new ProgressMock();
	FileOfValuesReaderMock readerMock = new FileOfValuesReaderMock(10, extractor, extractor.Transfer, progress);
	readerMock.Process();
	Assert.AreEqual(progress.Expected, progress.Completed);
}

Jak zrealizować test sprawdzający wykonanie wszystkich metod używanych przez metodę Process() klasy FileOfValuesReader? W podobny sposób jak właśnie testowaliśmy poprzednią funkcjonalność – tu także wykorzystamy atrapy. Ponieważ metody Transfer() i Extract() klasy FileOfValuesReader nie są wirtualne, nie będzie można sprawdzić ich uruchamiania bezpośrednio. Ale ponieważ wiemy, że druga wykorzystuje interfejs IValuesExtractor, zatem wykorzystamy metodę Process() tego interfejsu, zaś w przypadku pierwszej przekażemy metodę, która potwierdzi, że Transfer() została wywołana. Pozostaje jeszcze kwestia, w jaki sposób potwierdzić wykonanie wszystkich oczekiwanych metod. W klasie udającej odczyt pliku utworzymy właściwości typu logicznego odpowiadające sprawdzanym metodom, a którym zostanie nadaną wartość prawda jeżeli dana metoda zostanie wywołana. Test musi po prostu sprawdzić, że wszystkie te właściwości zawierają prawdę.

Skoro klasa-atrapa ma udawać odczyt, to posiadamy już takową – właśnie przed chwilą utworzyliśmy ją na potrzeby testów powiadamiania o postępie przetwarzania. Wykorzystamy ją zatem czyniąc bazową dla obecnej (czyli będziemy po niej dziedziczyć)

public class FileOfValuesReaderExecutionTest : FileOfValuesReaderMock
{
	#region konstruktory
	public FileOfValuesReaderExecutionTest(FileOfValuesReaderMockAdditions mockAdditions, Transfer transfer, IProgressNotifier progressNotifier)
		: base(1, mockAdditions, transfer, progressNotifier)
	{
		mockAdditions.FileOfValuesReaderMock = this;
	}
	#endregion
	#region właściwości
	public bool OpenExecuted { get; set; }
	public bool ReadExecuted { get; set; }
	public bool ExtractExecuted { get; set; }
	public bool TransferExecuted { get; set; }
	public bool CloseExecuted { get; set; }
	#endregion
	#region metody
	protected override void Open()
	{
		OpenExecuted = true;
		base.Open();
	}

	protected override void Read()
	{
		ReadExecuted = true;
		base.Read();
	}

	protected override void Close()
	{
		CloseExecuted = true;
		base.Close();
	}
	#endregion
}

Jak widać w konstruktorze zadajemy rozmiar udawanego pliku jako 1 – dzięki temu wykonany zostanie tylko jeden przebieg całości metody i tym samym wszystkie oczekiwane uruchomienia metod powinny nastąpić. W pierwszym parametrze konstruktora używana jest klasa pomocnicza, oto jak jest ona zaimplementowana:

public class FileOfValuesReaderMockAdditions : ValuesExtractorMock
{
	#region właściwości
	public FileOfValuesReaderExecutionTest FileOfValuesReaderMock { set { fileOfValuesReaderMock = value; } }
	#endregion
	#region metody
	public override void Process()
	{
		fileOfValuesReaderMock.ExtractExecuted = true;
	}
	public void MockTransfer(AnyValue[] values)
	{
		fileOfValuesReaderMock.TransferExecuted = true;
	}
	#endregion
	#region pola
	FileOfValuesReaderExecutionTest fileOfValuesReaderMock;
	#endregion
}

Klasa ta realizuje za jednym zamachem dwie rzeczy: implementuje interfejs wyodrębniania wartości oraz metodę przekazywania danych. Za pomocą właściwości wskazywany jest jej obiekt klasy udającej odczyt pliku, tak aby można było w jej właściwościach zapisać fakt wywołania metod Transfer() oraz Extract().
Sam test będzie wyglądał następująco:

[TestMethod]
public void test_FileOfValuesReader_Process_Method()
{
	FileOfValuesReaderMockAdditions mockAdditions = new FileOfValuesReaderMockAdditions();
	FileOfValuesReaderExecutionTest readerMock = new FileOfValuesReaderExecutionTest(mockAdditions, mockAdditions.MockTransfer, new ProgressMock());
	readerMock.Process();
	Assert.IsTrue(readerMock.OpenExecuted && readerMock.ReadExecuted && readerMock.ExtractExecuted && readerMock.TransferExecuted && readerMock.CloseExecuted);
}

Sprawdzane są wszystkie właściwości – jeżeli wszystkie metody zostały wywołane przynajmniej raz test będzie pozytywny.

Na koniec chciałbym tylko uzupełnić, że choć pisaliśmy dla naszych testów atrapy to nie jest to zawsze konieczne. Istnieją są specjalne biblioteki upraszczające tę czynność. Informacje na ich temat można np. znaleźć na blogu Macieja Aniserowicza. My napisaliśmy atrapy, aby pokazać co składa się na proces testowania, w tym zrozumieć czym są atrapy i nauczyć się jak je tworzyć.