Dotychczas nie pisałem testów (jakoś kod testowałem ręcznie) i odkrywam jak bardzo są pożyteczne. Po pierwsze i najważniejsze, umożliwiają wykrycie błędów w implementacji klas zanim zaczniemy ich używać.
Tworząc nowy projekt testów w Visual Studio dostajemy MSTest, który nie działa pod Linuxem. W związku z tym sięgnąłem po NUnit.
Tak de facto projekt z testami to po prostu ‘Class Library’, który po zbudowaniu podaje się do aplikacji ‘Test Runner’, która ładuje klasy i wykonuje metody oznaczone odpowiednimi atrybutami.
Oznaczając klasę atrybutem [TextFixture]
określamy, że w tej klasie znajdują się testy. Możemy dodatkowo oznaczyć ją atrybutem [TestOf(Type)]
, w którym określimy klasę jaką testujemy.
Testy to metody oznaczone atrybutem [Test]
. Dodatkowo jeśli chcemy mieć metodą inicjalizującą, która zostanie wykonana przed każdym testem oznaczymy ją atrybutem [SetUp]
. A jeśli chcemy mieć metodę, wykonującą jakieś sprzątanie po każdym teście to oznaczymy ją atrybutem [TearDown]
.
Mamy dwa rodzaje testów: parametryzowane i nieparametryzowane. Te drugie nie wymagają dodatkowych atrybutów. Te pierwsze natomiast potrzebują atrybuty opisujące ich argumenty. Najprostszym atrybutem jest [Values]
, który przyjmuje w konstruktorze listę wartości do użycia. (Note: Dla typów Enum
i bool
nieużycie listy wartości spowoduje użycie wszystkich możliwości). Do tego mamy jeszcze atrybuty [Random(min, max, int count)]
i [Range(from, to, step)]
.
Jeśli chcemy aby nasze testy były wykonywane w określonej kolejności to mamy do tego atrybut [Order(int)]
. Testy są wykonywane w kolejności rosnącej względem parametru tego atrybutu. Jeśli używamy wielu wątków do jednoczesnego uruchamiania testów to testy będą uruchamiane w odpowiedniej kolejności, ale nie będą czekać aż poprzednie się zakończą.
Dodatkowo możemy nałożyć limit czasowy, po którym test będzie anulowany. Do tego służy atrybut [Timeout(milliseconds)]
. Jeśli chcemy aby test dobiegł końca, ale dostał negatywną ocenę jeśli działa za długo, to do tego służy atrybut [Maxtime(milliseconds)]
.
Przykład użycia
using NUnit.Framework;
using System.IO;
using System.Text;
[TestFixture]
public class MyTest
{
private Stream stream;
[SetUp]
public void Init()
{
stream = File.Open("test.bin", FileMode.OpenOrCreate);
}
[Test]
public void WriteTest([Values(1, 2, 3)]int num)
{
using(var bw = new BinaryWriter(stream, Encoding.UTF8, true))
{
bw.Write(num);
bw.Flush();
}
}
[TearDown]
public void Dispose()
{
stream.Dispose();
}
}
Więcej atrybutów możecie znaleźć w dokumentacji NUnit lub na moim gist: Usefull NUnit Attributes.
Mocking
Testy jednostkowe mają na celu sprawdzenie implementacji danego interfejsu niezależnie od implementacji interfejsów, z których ona korzysta. Aby to osiągnąć należy stworzyć sztuczny obiekt, w którym można dodatkowo zasymulować oczekiwane zachowanie.
Jednym z frameworków do takiej symulacji jest Moq. Udostępnia on stosunkowo prosty interfejs do konfiguracji symulowanych obiektów.
Jeszcze nie rozgryzłem bardziej zaawansowanych użyć Moqa, ale w najprostszej postaci używam go np. do czegoś takiego:
public IConfiguration GetMockConfiguration(IEnumerable<KeyValuePair<string, object>> testData)
{
var mock = new Mock<IConfiguration>();
var data = testData.ToList();
mock.Setup(cfg => cfg.GetAllProperties()).Returns(data);
return mock.Object;
}
Powyższy kawałek kodu spowoduje wygenerowanie obiektu typu IConfiguration
, któremy ustawiamy, że wywołanie metody GetAllProperties()
spowoduje zwrócenie zmiennej data
.