W poprzedniej części kursu “Kurs TDD część 4: Nasz pierwszy test jednostkowy”) omówiłem w jaki sposób ustawić środowisko Visual Studio aby móc pisać i uruchamiać testy. W tej części omówię jak wykonać kilka prostych technik, tj. jak:
[TestCase]
,Na tapetę idzie przykład dzielenia; chcemy napisać funkcjonalność i testy mając na uwadze, że:
Divide
należy do klasy Calculator
,Divide
przyjmuje dwa parametry wejściowe — obydwa typu int; zwracanym typem jest float,CalculatedEvent
,DivideByZeroException
.I tyle! Początkowy szkielet klasy Calculator wyglądać będzie tak:
class Calculator
{
public float Divide(int dividend, int divisor)
{
throw new NotImplementedException();
}
public event EventHandler CalculatedEvent;
protected virtual void OnCalculated()
{
var handler = CalculatedEvent;
if (handler != null) handler(this, EventArgs.Empty);
}
}
Pamiętając, że na wejściu mamy dwa inty, a na wyjściu float możemy rozpisać tablicę przypadków, dla których chcemy sprawdzić poprawność naszego wyniku. Mogą to być następujące testy:
Aby uniknąć pisania sześciu metod (można, ale da się to zrobić lepiej) czy też pisania wszystkich asercji w jednym teście (co, jak już wiemy, jest złym wzorcem) możemy skorzystać z NUnitowego atrybutu [TestCase]
. Jako parametry atrybutu podajemy dane wejściowe oraz (opcjonalnie) wartość oczekiwaną, natomiast definicja poszczególnych elementów zawarta jest w parametrach testu jednostkowego. Nasz test będzie wyglądać tak:
[TestCase(4, 2, 2.0f)]
[TestCase(-4, 2, -2.0f)]
[TestCase(4, -2, -2.0f)]
[TestCase(0, 3, 0.0f)]
[TestCase(5, 2, 2.5f)]
[TestCase(1, 3, 0.333333343f)]
public void Divide_ReturnsProperValue(int dividend, int divisor, float expectedQuotient)
{
var calc = new Calculator();
var quotient = calc.Divide(dividend, divisor);
Assert.AreEqual(expectedQuotient, quotient);
}
Dzięki atrybutowi [TestCase]
mamy 6 testów w jednej metodzie. W okienku rezultatów testu, wszystkie pojawiają się jako podrzędne do głównego testu. Wygląda to tak:
Bardzo ważna kwestia jaka tutaj się pojawiła to przypadek dzielenia 1 ÷ 3. Dzięki arytmetyce liczb zmiennoprzecinkowych uzyskamy wynik 0.333333343f. Wyjaśnienie skąd się wział taki wynik znajduje się w literaturze zamieszczonej w przypisach. Najważniejsza jest jednak świadomość tego faktu i uwzględnienie go w testach.
W przypadku dzielenia musimy obsłużyć przypadek dzielenia przez zero. Założyliśmy, że w takim przypadku wyrzucamy błąd typu System.DivideByZeroException
. W NUnicie testowanie wyjątków możemy wykonać przez wywołanie [Assert.Throws
]. Tutaj również podanie typu jest opcjonalne. Jako parametr przekazujemy delegat kodu, który chcemy wykonać.
[Test]
public void Divide_DivisionByZero_ThrowsException()
{
var calc = new Calculator();
Assert.Throws<DivideByZeroException>(() => calc.Divide(2, 0));
}
Wyrażenie lambda możemy zastąpić anonimową metodą:
Assert.Throws(delegate { calc.Divide(2, 0); });
Założyliśmy, że po wykonaniu obliczeń, wołamy zdarzenie CalculatedEvent
. Sam NUnit nie wspiera natywnie testowania zdarzeń, jednak możemy zastosować prosty trik—po wywołaniu zdarzenia zmieniamy wartość flagi. Asercji dokonujemy na podstawie wartości tej flagi. Jeśli zdarzenie zostało wywołane, test przechodzi pozytywnie:
[Test]
public void Divide_OnCalculatedEventIsCalled()
{
var calc = new Calculator();
bool wasEventCalled = false;
calc.CalculatedEvent += (sender, args) => wasEventCalled = true;
calc.Divide(1, 2);
Assert.IsTrue(wasEventCalled);
}
Wyrażenie lambda możemy zastąpić anonimową metodą:
calc.CalculatedEvent += delegate { wasEventCalled = true; };
Po napisaniu testów do naszego kodu, możemy przystąpić do napisania implementacji metody dzielenia. Zachęcam do napisania implementacji we własnym zakresie!
Ostateczna postać klasy wygląda tak:
class Calculator
{
public float Divide(int dividend, int divisor)
{
if (divisor == 0) throw new DivideByZeroException();
float result = (float)dividend / divisor;
OnCalculated();
return result;
}
public event EventHandler CalculatedEvent;
protected virtual void OnCalculated()
{
var handler = CalculatedEvent;
if (handler != null) handler(this, EventArgs.Empty);
}
}
Wszystkie testy są zielone. W tak prostym przykładzie nie trzeba nic refaktoryzować! Fin!
W tej części kursu poznaliśmy:
[TestCase]
, który niewielkim kosztem generuje przypadek testowy.Ponadto dowiedliśmy że float, ze względu na arytmetykę liczb zmiennoprzecinkowych, nie jest odpowiednim typem jako typ zwracany przy dzieleniu. Lepszym okazałby się decimal. Wybrałem jednak float, aby pokazać naturę testów. Oczekujemy nie do końca prawidłowej (z punktu widzenia matematycznego) wartości (przypadek dzielenia 1 ÷ 3) i dzięki temu pojawienie się oczekiwanego wyniku nie powinno nas zaskoczyć. Dzięki TDD wykrylibyśmy taki błąd przy zmianie typu zwracanego: np. z decimal na float. Dobranie typu parametrów wejściowych jako int też jest celowe, choć w praktyce bardzo niebezpieczne. Na szczególną uwagę zasługuje linijka:
float result = (float)dividend / divisor;
Jaki wynik otrzymalibyśmy bez rzutowania zmiennej dividend na float? Zachęcam do eksperymentowania.
[1] Dlaczego 1 ÷ 3 = 0,333333343f? Czytaj więcej na ten temat:
Część I: Testy jednostkowe – wstęp