Odkryłem TDD

Niedawno zacząłem praktyki, gdzie biorę udział w eksperymencie porównania 3 metodyk pisania oprogramowania: TDD, ITL i NUT. TDD spodobało mi się szczególnie, więc postanowiłem napisać tego posta.

TDD, ITL, NUT, ABW, PSD…

Czym są te wszystkie skróty? Dwa ostatnie to formaty plików programów AbiWord i PhotoShop, ale nie o nich ten post ;)

TDD

Test Driven Developement - metodyka kierująca się zasadą Test-First. Oznacza to, że zanim napiszemy jakikolwiek kawałek kodu, piszemy test, który opisze żądaną funkcjonalność. Następnie piszemy minimalną, najprostszą implementację, która ten test przechodzi. Wtedy bierzemy się za kolejny test, itd.

ITL

Iterative Test Last - metodyka trochę przeciwna do TDD, bo kierująca się zasadą Test-Last. Tylko ważne jest tu słowo iterative, które mówi nam, że testy piszemy wprawdzie na koniec, ale cyklicznie. Czyli piszemy jakąś funkcjonalność, piszemy testy, jak nie przechodzą to poprawiamy, jak przechodzą to kończymy cykl i zabieramy się za kolejną funkcjonalność.

NUT

No Unit Tests - nazwa mówi sama za siebie, nie piszemy testów jednostkowych.

Co fajnego w TDD

Zalet jest całkiem sporo. Po pierwsze zmniejszamy szansę na wystąpienie błędów w kodzie (pod warunkiem że testy są ok). Po drugie nie mamy nie potrzebnego kodu, bo możemy napisać tylko to co jest potrzebne do przejścia testów. Zmienia się też workflow i człowiek inaczej zaczyna patrzeć na kod. A i czytałem ze zespoły używające TDD kończą projekt 30% szybciej w porównaniu do “zwykłej” pracy (jaka by ona nie miała być).

Jakiś przykład?

Jest bardzo fajna strona TDD problems z zadaniami do ćwiczenia TDD. Oczywiście można na nich ćwiczyć dowolną metodykę.

Weźmy na warsztat Reload Countdown. Mamy napisać klasę wyglądającą mniej więcej tak:

public class Countdown
{
	public bool IsRunning { get; }
	public void Start(int seconds);
	public void Decrease(int seconds);
}

Zacznijmy od pustej klasy Countdown i testu dla IsRunning:

public void CanCallIsRunning()
{
	var countdown = new Countdown();

	var result = countdown.IsRunning;
}

Ponieważ klasa jest pusta, to ten test się nawet nie skompiluje. Więc dodajemy

public bool IsRunning { get; set; }

Teraz test przechodzi. Kolejnym krokiem jest upewnienie się, że zaraz po utworzeniu IsRunning zwraca false. Żeby nie doprowadzić do nadmiernej ilości testów, które są częścią innych zmodyfikujemy CanCallIsRunning():

public void WhenCreated_IsRunningReturnsFalse()
{
	var countdown = new Countdown();

	var result = countdown.IsRunning;

	Assert.IsFalse(result);
}

Ten test od razu przechodzi, ponieważ false jest domyślną wartością dla zmiennej typu bool.

Teraz weźmy Start(). Pominę pierwszy test, żeby ten post nie był za długi. Ale w związku z nim otrzymujemy:

public void Start(int seconds)
{
}

Jest to minimalny kod aby pierwszy test przeszedł. Teraz czas na taki test:

public void WhenStartedWithMoreThanZero_IsRunningReturnsTrue()
{
	var countdown = new Countdown();
	var time = 1;

	countdown.Start(time);

	var result = countdown.IsRunning;
	
	Assert.IsTrue(result);
}

Żeby go przejść wystarczy dodać do Start()

IsRunning = true;

Więc teraz będziemy mieli drugi test, który sprawdzi co jeśli int seconds jest niedodatni:

public void WhenStartedWithMoreLessThanOne_IsRunningReturnsTrue()
{
	var countdown = new Countdown();
	var time = 0;

	countdown.Start(time);

	var result = countdown.IsRunning;
	
	Assert.IsFalse(result);
}

I aby oba testy przechodziły modyfikujemy Start() w taki sposób:

if(seconds >= 1)
	IsRunning = true;
else
	IsRunning = false;

Ok, mamy już co nieco. Teraz chcemy sprawdzić stan licznika. Ale nie chcemy robić z niego własności publicznej. Jest na to sposób. Po pierwsze stan licznika będzie miał widoczność internal. Po drugie w AssemblyInfo.cs dodajemy

[assembly: InternalsVisibleTo("Countdown.Tests")]

Gdzie Countdown.Tests to nazwa naszego projektu z testami. Jest kilka uwag, ale nie są one ważne w tym momencie.

Teraz w naszych testach będziemy mogli się odwoływać do elementów Countdown z widocznością internal.

public void WhenStarted_VerifyTheSecondsField()
{
	var countdown = new Countdown();
	var time = 5;

	countdown.Start(time);

	Assert.AreEqual(time, countdown.seconds);
}

Ten test spełnimy tak:

internal int seconds;

public void Start(int seconds)
{
	if(seconds >= 1)
		IsRunning = true;
	else
		IsRunning = false;

	this.seconds = seconds;
}

Następnie zrobimy test, że jeśli IsRunning == false to seconds == 0 i otrzymamy taki kod metody Start()

public void Start(int seconds)
{
	if(seconds >= 1)
	{
		this.seconds = seconds;
		IsRunning = true;
	}
	else
	{
		this.seconds = 0;
		IsRunning = false;
	}
}

Ten test będzie miał dwa warianty: gdy nie wykonano Start() i gdy go wykonano z wartością <= 0.

Podobnie będziemy postępowali z pisaniem metody Decrease().

Słowem końca

Mam nadzieję, że trochę przybliżyłem Wam TDD. Być może użycie go sprawi wam tyle frajdy ile sprawiło mi. Jeśli macie jakieś wskazówki lub pytania, piszcie w komentarzach.