Ćwiczenie
Celem ćwiczenia było zapoznanie się z testami jednostkowymi (UnitTest) w C#. W ramach laboratorium mieliśmy stworzyć aplikację okienkową w postaci kalkulatora obsługującego podstawowe operacje takie jak dodawanie i dzielenie. Program miał być odporny na wprowadzane przez użytkownika błędne dane wejściowe oraz odporny na błąd dzielenia przez zero. Dodatkowo stworzyliśmy do naszego kalkulatora testy jednostkowe sprawdzające poprawność funkcjonowania metod operacji arytmetycznych.

Uzupełnienie teoretyczne
Test jednostkowy (ang. unit test) to w programowaniu metoda testowania tworzonego oprogramowania poprzez wykonywanie testów weryfikujących poprawność działania pojedynczych elementów (jednostek) programu np. metod lub obiektów.

Testy jednostkowe muszą spełniać kilka założeń.

Niezależność
Testy powinny być niezależne. Oznacza to, że dwa dowolne testy nie wpływają na siebie nawzajem. Nie istnieje konieczność utrzymania sztywnej kolejności wywołania testów. Nie zawsze można uzyskać taki stan, ale warto zadbać o jak największą niezależność testów.

Powtarzalność
Testy powinny być powtarzalne, łatwe do uruchomienia w dowolnym momencie i gwarantujące stabilność. Oznacza to, że pisząc test jednostkowy możemy w dowolnym momencie go uruchomić bez konieczności uruchamiania dodatkowych elementów. Powtarzalność oznacza też, że test daje taki sam wynik za każdym razem dla danego zestawu danych.

Jednoznaczność
Test jednostkowy powinien być jednoznaczny, czyli jasno odpowiadać na pytanie o poprawność działania testowanej funkcjonalności.

Jednostkowość
Test jednostkowy to test, który testuje jedną rzecz na raz. Nie wolno pisać testu w którym staramy się sprawdzić dwie funkcjonalności. Nie należy też sprawdzać w jednym teście kilku zestawów danych. Taki test w przypadku niepowodzenia nie zwraca jednoznacznych wyników.

Test-driven development (TDD) jest techniką tworzenia oprogramowania zaliczaną do metodyk zwinnych (Agile). Technika została stworzona przez Kenta Becka. Pierwotnie była częścią programowania ekstremalnego (ang. extreme programming), lecz obecnie stanowi samodzielną technikę. Polega na wielokrotnym powtarzaniu kilku kroków:

1. Najpierw programista pisze automatyczny test sprawdzający dodawaną funkcjonalność. Test w tym momencie nie powinien się udać.
2. Później następuje implementacja funkcjonalności. W tym momencie wcześniej napisany test powinien się udać.
3. W ostatnim kroku, programista dokonuje refaktoryzacji napisanego kodu, żeby spełniał on oczekiwane standardy.

Behavior-driven development (BDD) jest techniką tworzenia oprogramowania zaliczaną do metodyk zwinnych (Agile). Jest wykorzystywana do współpracy pomiędzy developerami, a osobami nietechnicznymi jak partnerzy biznesowi. BDD pojawiło się w 2003 za sprawą Dan North jak odpowiedź na TDD.
W BDD podstawą do kontroli poprawności działania aplikacji lub funkcjonalności są specyfikacja i scenariusze działania. Behaviour Driven Development, jak sama nazwa wskazuje opiera się na pewnych zachowaniach, które wynikają ze specyfikacji lub scenariuszy. W przeciwieństwie do TDD czy innych metod testowania aplikacji nie sprawdzamy w jaki sposób i z wykorzystaniem jakich metod działają funkcjonalności, ale jesteśmy zorientowani na cel.
Tak naprawdę, w BDD nie piszemy testów, tylko przeprowadzamy dowody na słuszność pewnych hipotez, jakie stawiamy względem produkcyjnego kodu. Jest to podejście o wiele bardziej naturalne niż klasyczne TDD.

Program Kalkulator
Nasz program kalkulator ma postać aplikacji okienkowej:

Kod programu:
Plik: Form1.cs

using System;

using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace Kalkulator
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void btAdd_Click(object sender, EventArgs e)
{
label1.Text = MyStringMath.Add(textBox1.Text, textBox2.Text);
}

private void btDiv_Click(object sender, EventArgs e)
{
label1.Text = MyStringMath.Div(textBox1.Text, textBox2.Text);
}

private void btSub_Click(object sender, EventArgs e)
{
label1.Text = MyStringMath.Sub(textBox1.Text, textBox2.Text);
}

private void btMul_Click(object sender, EventArgs e)
{
label1.Text = MyStringMath.Mul(textBox1.Text, textBox2.Text);
}

}
}

Plik: MyStringMath.cs

using System;

using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Kalkulator
{
public static class MyStringMath
{
public static string Add(string val1, string val2)
{
double value1, value2;

if(!double.TryParse(val1, out value1))
{
return "Składnik pierwszy jest niepoprawny.";
}

if(!double.TryParse(val2, out value2))
{
return "Składnik drugi jest niepoprawny.";
}

return (value1 + value2).ToString();
}

public static string Div(string val1, string val2)
{
double value1, value2;

if(!double.TryParse(val1, out value1))
{
return "Dzielna jest niepoprawna.";
}

if(!double.TryParse(val2, out value2))
{
return "Dzielnik jest niepoprawny.";
}

if (value2 != 0)
{
return (value1 / value2).ToString();
}
else
{
//Nie wolno dzielic przez zero
try
{
return Error();
}
catch (Exception e)
{
return e.Message;
}
}

}

public static string Error()
{
throw new DivideByZeroException();
}

public static string Sub(string val1, string val2)
{
double value1, value2;

if (!double.TryParse(val1, out value1))
{
return "Odjemna jest niepoprawna.";
}

if (!double.TryParse(val2, out value2))
{
return "Odjemnik jest niepoprawny.";
}

return (value1 - value2).ToString();
}

public static string Mul(string val1, string val2)
{
double value1, value2;

if (!double.TryParse(val1, out value1))
{
return "Czynnik pierwszy jest niepoprawny.";
}

if (!double.TryParse(val2, out value2))
{
return "Czynnik drugi jest niepoprawny.";
}

return (value1 * value2).ToString();
}
}
}

Testy jednostkowe (UnitTest)
Plik: UnitTest1.cs

using System;

using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Kalkulator; // dodano

namespace KalkulatorTest
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestAdd()
{
string value1 = "1";
string value2 = "2";

string result = MyStringMath.Add(value1, value2);
Assert.AreEqual("3", result);
}

[TestMethod]
public void TestDiv()
{
// Sprawdzamy dzielenie dwóch liczb
string value1 = "6";
string value2 = "2";

string result = MyStringMath.Div(value1, value2);
Assert.AreEqual("3", result);
}

[TestMethod]
public void TestDivByZero()
{
// Sprawdzamy dzielenie przez zero
string value1 = "1";
string value2 = "0";

string result = MyStringMath.Div(value1, value2);

try
{
MyStringMath.Error();
}
catch (Exception e)
{
Assert.AreEqual(e.Message, result);
}
}

[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void TestError()
{
string s = MyStringMath.Error();
}

[TestMethod]
public void TestSub()
{
string value1 = "6";
string value2 = "2";

string result = MyStringMath.Sub(value1, value2);
Assert.AreEqual("4", result);
}

[TestMethod]
public void TestMul()
{
string value1 = "6";
string value2 = "3";

string result = MyStringMath.Mul(value1, value2);
Assert.AreEqual("18", result);
}
}
}