Testy jednostkowe z NUnit i Moq

19 Maja 2016

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.