Koń czy tam testy jednostkowe - jakie są, każdy widzi. Nie? No dobra, nie do końca tak to działa, chodź sama idea testów jest dość prosta. Acz by dokładnie zagłębić się w tematykę skorzystamy ze źródła wiedzy, które pomogło uzyskać kilka milionów licencjatów i magistrów - wikipedii. Zgodnie z definicją powinniśmy je wykorzystywać do testowania najmniejszej części wytworzonego przez nas oprogramowania. Jednak postępowanie zgodnie z zasadami nie zawsze jest proste. Obecnie aplikację webowe składają się z wielu warstw, gdzie każda z nich posiada olbrzymią ilość klas, modułów, zależności - to właśnie one najbardziej przeszkadzają nam w testowaniu możliwie najmniejszych części programu w izolowanym środowisku. Jak temu zaradzić? Po pierwsze - trzeba tworzyć mniejsze klasy :P. Jak jednak pierwsza część zawodzi, a kakałko robi się zimne, przychodzi punkt drugi - używanie biblioteki odpowiedzialnej za Mockowanie. Dlatego dzisiaj chciałbym wam przedstawić jak w prosty sposób wykorzystać bibliotekę Moq do usprawnienia życia podczas pisania testów jednostkowych.
Przygotowanie środowiska pracy
Zacznijmy od szybkiego stworzenia środowiska pracy wykorzystując dotnet CLI (nie proponuje robić wolno, bo w sumie po co? Serial czeka, kakałko się chłodzi, żarty o basistach czekają)
$ mkdir netcore-moq && cd netcore-moq
$ mkdir src tests
$ dotnet new sln
$ dotnet new consolelib -n Application -o src/
$ dotnet new xunit -n Application.Tests -o tests/
$ dotnet sln add \*\*/\*.csproj
$ dotnet add reference ../src/Application.csproj
Tak tworzymy solucję, w skład której wchodzą dwa projekty. Pierwszy z nich to nasza biblioteka, która znajduje się w katalogu src. Natomiast w "tests" znajduje się nasz projekt testów jednostkowych. W przypadku braku w wiedzy dotyczącej frameworka do testów xUnit zapraszam do zajrzenia na mój poprzedni artykuł: Net Core - Testy jednostkowe z wykorzystanie xUnit.
Dodanie paczki Moq do projektu testów
No dobra, skoro cały projekt mamy gotowy, czas dodać paczkę nuget-a. Wystarczy w konsoli wpisać
$ dotnet add package Moq
Proste, nie? :)
Przygotowanie klas do mockowania
Żeby móc mockować, to najpierw trzeba mieć co :). Aby przekazać wam wiedzę skuteczniej ten jeden raz złamie święte zasady TDD i napiszę kawałek kodu jeszcze przed testami (tak was uwielbiam!). Nasz projekt będzie się składał z dwóch klas i jednego interfejsu, odpowiednio Car, Engine i IEngine. Podstawowa implementacja będzie wyglądać następująco
public class Car
{
private IEngine _engine;
public Car(IEngine engine)
{
_engine = engine;
}
}
public interface IEngine
{
}
public class Engine : IEngine
{
}
Jak widać wiele się nie dzieje. Więc co możemy przetestować? Pierwszym testem, jaki możemy wykonać, jest spróbowanie utworzenia instancji klasy wraz z mockowanym obiektem implementującym interfejs.
Wygląda to następująco:
using Xunit;
using Moq;
namespace Application.Tests
{
public class CarTest
{
[Fact]
public void Car_Should_Have_Engine()
{
var engineMock = new Mock<IEngine>();
var car = new Car(engineMock.Object);
}
}
}
Jak widać wraz z biblioteką Moq, otrzymaliśmy typ generyczny Mock<T>
, gdzie jako typ możemy podać interfejs lub klasę, którą chcemy za mockować. Oczywiście preferowany jest tutaj interfejs, ponieważ mockowanie klasy ma swoje ograniczenia (np. będziemy mogli zamockować tylko abstrakcyjne i wirtualne metody).
Dla porównania możemy zaimplementować wersję bez wykorzystania biblioteki Moq. Wygląda to mniej więcej tak:
using Xunit;
namespace Application.Tests
{
public class CarTest
{
[Fact]
public void Car_Should_Have_Engine()
{
var engineMock = new Engine();
var car = new Car(engineMock);
}
}
}
Pomimo, że otrzymaliśmy jedną linijkę mniej (usunięcie using-a) powinniśmy zadać sobie pytanie - czy to jest lepsze rozwiązanie?
Nie!
Pamiętacie to stwierdzenie z początku - w testach jednostkowych powinniśmy testować najmniejsze części naszego oprogramowania w izolacji? Czy właśnie tego nie łamiemy? Przecież chcemy przetestować samą klasę Car, to po co nam tworzyć inny obiekt naszej domeny?
I jeszcze jeden problem: co w przypadku stworzenia innej klasy, która mogłaby zastąpić zwykły Engine, np. HybridEngine? Musielibyśmy zmieniać wystąpienia klasy Engine we wszystkich testach? Podwajać testy aby mogły one testować obie implementacje interfejsu IEngine? No pewnie, że...nie. Jedna ze świętych zasad programowania powiada:
Programuj do interfejsu, nie do implementacji.
Mockowanie metod i propertisów
Możliwości biblioteki Moq nie kończą się na samym tworzeniu obiektów. Jest też opcja mokowania metod i propertisów. Dzięki temu możemy testować, czy klasa Car odpowiednio przetwarza dane dostarczone przez klasę Engine. Ewentualnie, czy klasa Car wywołuje jakąś metodę klasy Engine odpowiednią ilość razy.
Tak więc koniec zabawy, implementujemy jakąś logikę w naszej klasie Car. Powiedzmy, że chcielibyśmy posiadać jakiś propertis, który informowałby nas czy dioda oleju jest zapalona. Jako.., że nie chce wrzucać tutaj event-ów, to sprawdzenie tego stanu zrobimy tylko w konstruktorze klasy Car.
public class Car
{
private IEngine _engine;
public bool IsOilDiodeActive { get; private set; }
public Car(IEngine engine)
{
_engine = engine;
if (!_engine.IsOilOk())
IsOilDiodeActive = true;
}
}
Teraz, aby przetestować coś takiego w testach jednostkowych możemy użyć dwie metody udostępnione nam przez instancję obiektu Mock<IEngine>
: Setup i Returns. Do metody Setup przekażemy funkcję lambda, w ciele której możemy wybrać jaką metodę lub propertis chcemy zamockować. W ramach pracy domowej wywnioskujcie samodzielnie co przekazujemy do metody Returns.
\[Fact\]
public void Car_Should_Activate_Oile_Diode_When_Engine_Has_Low_Oil()
{
var engineMock = new Mock<IEngine>();
engineMock.Setup(e => e.IsOilOk()).Returns(false);
var car = new Car(engineMock.Object);
Assert.True(car.IsOilDiodeActive);
}
W taki sam sposób możemy mockować propertisy obiektu. Dla przykładu, żeby to zobrazować, dodajmy do klasy Car możliwość zwracania ilości oleju w silniku. W skrócie - dodajemy metodę GetOilStatus
public double GetOilStatus()
{
return _engine.OilStatus;
}
Aby przetestować coś takiego możemy zrobić podobnie jak wcześniej
[Fact]
public void Car_Should_Have_Information_About_Oil_Status()
{
var oilStatus = 2.0;
var engineMock = new Mock<IEngine>();
engineMock.Setup(e => e.OilStatus).Returns(oilStatus);
var car = new Car(engineMock.Object);
Assert.Equal(oilStatus, car.GetOilStatus());
}
Oczywiście to nie jest koniec możliwości tej biblioteki. Sam jednak wole krótsze artykuły, które mogę sobie przeczytać do porannej herbaty ewentualnie energetyka w pracy (czy tam kto co rana robi).
Podsumowanie
W dzisiejszym poście przedstawiłem wam trochę podstaw na temat wykorzystania biblioteki Moq do mockowania zależności klas w testach jednostkowych. Pozwala nam to na izolację wywołania testów. Dodatkowo wymusza na nas programowanie do interfejsu.
Jak zwykle mam nadzieje że artykuł wam się podobał :)
Ja tymczasem powoli szykuje się na PROGRAMISTOK!!!!, na który ruszam w piątek. Prawdopodownie, w przyszłym tygodniu, na moim blogu, pojawi się relacja z tej konferencji.
Do Następnego!
Cześć :)