Co to jest mockowanie?
Mockowanie, czyli naśladowanie czegoś, jakiegoś zachowania. W polskim tłumaczeniu można się spotkać z różnymi tłumaczeniami słowa mock, między innymi makieta, ja jednak będę używał po prostu mock. Jeżeli metoda, ma w sobie logikę oraz korzysta z zewnętrznych zasobów (takich jak bazadanych, plik, webserwisy itp.), to w naszych testach jednostkowych nie dałoby się takiej metody testować. Jeżeli natomiast podstawimy w miejsce zewnętrznego zasobu, jakiś sztuczny obiekt, który nie ma żadnej logiki, to wtedy jak najbardziej uda nam się taką metodę testować. Żeby taki zabieg nam się udał, nasz kod musi stosować się do pewnych zasad. Między innymi musi mieć luźne powiązania i operować na interfejsach. Jeżeli stosujemy się do tych zasad, to dzięki temu możemy podmienić implementacje na potrzeby testów. Dlatego, jeżeli chcemy dodać testy do jakichś aplikacji, które nie były pisane z myślą o testach, to tutaj może pojawić się problem. Musimy taki kod zrefaktoryzować, co bez wcześniejszych testów może być dużym wyzwaniem :)
Frameworki do mockowania w C#
Mamy do wyboru dużo różnych frameworków do mockowania, między innymi Moq, NSubstitute, FakeItEasy, Rhino Mocks i wiele innych. Myślę, że warto zainteresować się frameworkiem Moq, którego ja używam na co dzień i Tobie również go polecam. Ma wszystkie funkcjonalności, które potrzebuję do mockowania obiektów, jest łatwy w użyciu.
Kod z zewnętrznymi zależnościami w C#
Najłatwiej będzie nauczyć się mockowania już na konkretnym przykładzie. Załóżmy, że chcemy przetestować metodę logowania do aplikacji. Dla uproszczenia przykładu powiedzmy, że metoda dla złych danych zwraca komunikat, a dla dobrych pustego stringa. Nasza klasa może wyglądać tak:
public interface IUsersRepository
{
bool Login(string user, string password);
}
public class Authentication
{
private readonly IUsersRepository _usersRepository;
public Authentication(IUsersRepository usersRepository)
{
_usersRepository = usersRepository;
}
public string Login(string user, string password)
{
var isAuthenticated = _usersRepository.Login(user, password);//data from database
if (!isAuthenticated)
return "User or password is incorrect.";
return string.Empty;
}
}
Mamy klasę Authentication, która w konstruktorze przyjmuję obiekt klasy implementującej interfejs IUserRepository, klasa ta jest odpowiedzialny za logowanie. Klasa Authentication ma jedną metodę Login, która na podstawie tego, czy uda nam się zalogować zwraca odpowiedni komunikat, jeżeli dane do logowania będą poprawne, zostanie zwrócony pusty string, w przeciwnym przypadku komunikat, że dane są nieprawidłowe.
Przykład testu jednostkowego, z zewnętrznymi zależnościami w C#
Jeżeli chcielibyśmy sprawdzić w teście jednostkowym, czy faktycznie dostaniemy odpowiedni komunikat, to musielibyśmy skorzystać z zewnętrznego zasobu (bazy danych) i wtedy nasze testy nie byłyby już jednostkowymi. Dobrze, że powyższy kod pisał dobry programista :) który wiedział, że należy operować na interfejsach, tak żeby klasa była testowalna. Możemy podmienić implementację IUsersRepository i użyć w miejscu interfejsu - mocka. Nasz kod testowy może wyglądać tak:
public class AuthenticationTests
{
[Test]
public void Login_WhenIncorrectData_ShouldReturnCorrectMessage()
{
//Arrange
var mockUserRepository = new Mock<IUsersRepository>();
mockUserRepository.Setup(x => x.Login("user", "password")).Returns(false);
var authentication = new Authentication(mockUserRepository.Object);
//Act
var result = authentication.Login("user", "password");
//Assert
Assert.That(result, Does.Contain("User or password is incorrect"));
}
}
Na początek musimy zainstalować framework do testowania. Musisz wyszukać poprzez Manage NuGet Package - Moq, następnie go zainstalować.
Jak widzisz w pierwszej linii arrange, za pomocą frameworka Moq jest inicjalizacja mock obiektu. Następnie definiujemy, jak ma działać metoda Login naszego obiektu. To znaczy, mówimy, że w po wywołaniu metody Login dla konkretnych parametrów, w naszym przypadku login - user, oraz hasła - password, zwróć false. W kolejnej linii przekazujemy obiekt mocka jako parametr klasy Authentication. Wywołujemy metodę login i sprawdzamy, czy wynik zawiera podaną wiadomość. Nasz test jednostkowy przechodzi poprawnie, nie odwołujemy się do żadnych zewnętrznych zależności, a udało nam się przetestować logikę zawartą w tym teście.
Zauważ jak ważne, w tym przypadku jest operowanie na interfejsie. Jeżeli klasa Authentication zamiast działania na interfejsie IUserRepository, pracowała by na zwykłej klasie, nie dało by się zastosować mocków, a co za tym idzie testów jednostkowych.
Zobaczmy jeszcze, jak może wyglądać przykład, gdy użytkownik poda poprawne hasło i login, wtedy powinien zostać zwrócony pusty string.
[Test]
public void Login_WhenCorrectData_ShouldReturnEmptyString()
{
//Arrange
var mockUserRepository = new Mock<IUsersRepository>();
mockUserRepository.Setup(x => x.Login("user", "password")).Returns(true);
var authentication = new Authentication(mockUserRepository.Object);
//Act
var result = authentication.Login("user", "password");
//Assert
Assert.That(result, Is.Empty);
}
W tym przykładzie w konfiguracji metody Login mówimy, że chcemy, aby nasza metoda, jeżeli zostanie wywołana z parametrami user i password zwróciła true. Dzięki temu możemy przetestować ścieżkę, która nas interesuje.
Zauważ, że jeżeli w act wywołamy metodę Login z innymi parametrami, to nasz test będzie czerwony. W arrange skonfigurowaliśmy metodę Login, aby tylko dla parametrów user, password zwróciła true, jeżeli parametry będą inne, wtedy zostanie zwrócona wartość domyślna, czyli false. Zobacz to na przykładzie:
[Test]
public void Login_WhenCorrectData_ShouldReturnEmptyString()
{
//Arrange
var mockUserRepository = new Mock<IUsersRepository>();
mockUserRepository.Setup(x => x.Login("user", "password")).Returns(true);
var authentication = new Authentication(mockUserRepository.Object);
//Act
var result = authentication.Login("1", "2");
//Assert
Assert.That(result, Is.Empty);
}
Taki kod nie przechodzi. Otrzymujemy komunikat: "Expected:
[Test]
public void Login_WhenCorrectData_ShouldReturnEmptyString()
{
//Arrange
var mockUserRepository = new Mock<IUsersRepository>();
mockUserRepository.Setup(x => x.Login(It.IsAny<string>(), It.IsAny<string>())).Returns(true);
var authentication = new Authentication(mockUserRepository.Object);
//Act
var result = authentication.Login("1", "2");
//Assert
Assert.That(result, Is.Empty);
}
Wynik testy jest zielony. Dzięki takiej konfiguracji nieważne z jakimi parametrami w act zostanie wywołana zamockowana metoda, dla każdego parametru typu string zostanie zwrócona wartość true.
Oczywiście metodę login, którą w tym przypadku mockujemy również musimy przetestować, ale zrobimy to już w testach integracyjnych, gdzie sprawdzimy logowanie na prawdziwej bazie danych. W tym przykładzie chcemy przetestować pozostały kod, to znaczy logikę w klasie Authentication, a do tego musimy się pozbyć zewnętrznych zależności.
Weryfikowanie wywołania mocków
Dzięki mockom, możemy również zweryfikować, ile razu została wywołana zamockowana metoda, lub czy w ogóle została wywołana. Aby to przedstawić, zmienię trochę klasę testowaną.
public interface IUsersRepository
{
bool Login(string user, string password);
void UpdateLastLoginDate(string user);
}
public class Authentication
{
private readonly IUsersRepository _usersRepository;
public Authentication(IUsersRepository usersRepository)
{
_usersRepository = usersRepository;
}
public string Login(string user, string password)
{
var isAuthenticated = _usersRepository.Login(user, password);//data from database
if (!isAuthenticated)
return "User or password is incorrect.";
_usersRepository.UpdateLastLoginDate(user);
return string.Empty;
}
}
Do interfejsu IUserRepository została dodana metoda UpdateLastLoginDate(string user), która aktualizuje datę ostatniego logowania dla konkretnego użytkownika. W metodzie Login, metoda UpdateLastLoginDate powinna zostać wywołana tylko wtedy, gdy użytkownik się zaloguje. Sprawdźmy, czy tak faktycznie się dzieje:
[Test]
public void Login_WhenCorrectData_ShouldUpdateLastLoginDate()
{
//Arrange
var mockUserRepository = new Mock<IUsersRepository>();
mockUserRepository.Setup(x => x.Login("user", "password")).Returns(true);
mockUserRepository.Setup(x => x.UpdateLastLoginDate("user"));
var authentication = new Authentication(mockUserRepository.Object);
//Act
var result = authentication.Login("user", "password");
//Assert
mockUserRepository.Verify(x => x.UpdateLastLoginDate("user"), Times.Once);
}
Dzięki metodzie Verify, możemy sprawdzić, ile razy została wywołana dana metoda. W powyższym przypadku interesowało nas akurat, aby metoda Login została wywołana dokładnie jeden raz i tak się rzeczywiście stało. Równie dobrze, możesz sprawdzić, że dana metoda nie została wywołana ani raz:
[Test]
public void Login_WhenIncorrectData_ShouldNotUpdateLastLoginDate()
{
//Arrange
var mockUserRepository = new Mock<IUsersRepository>();
mockUserRepository.Setup(x => x.Login("user", "password")).Returns(false);
mockUserRepository.Setup(x => x.UpdateLastLoginDate("user"));
var authentication = new Authentication(mockUserRepository.Object);
//Act
var result = authentication.Login("user", "password");
//Assert
mockUserRepository.Verify(x => x.UpdateLastLoginDate("user"), Times.Never);
}
Weryfikujemy, że jeżeli podane zostaną nieprawidłowe dane, to wtedy metoda UpdateLastLoginDate nie zostanie wywołana.
PODSUMOWANIE
Mockowanie obiektów w świecie testów jednostkowym jest bardzo ważnym tematem. Często musimy zastąpić zewnętrzne zależności jakimiś sztucznymi zachowaniami. W dzisiejszym artykule zademonstrowałem Ci na prostych przykładach, jak powinno się to robić w .NET, przy użyciu Moq. Ja używam frameworka Moq, ale jeżeli chcesz, to oczywiście możesz używać innego frameworka. Polecam potestować i zacząć używać takiego, który Ci najbardziej odpowiada. Wszystkie mają takie same cele, czyli mockowanie obiektów, różnią się głównie składnią. Jeżeli masz jakieś pytanie co do mockowania lub brakuje Ci więcej informacji, przykładów to proszę, napisz mi o tym w komentarzu. W kolejnym artykule pokaże Ci jak pisać testy integracyjne w .NET. Przetestujemy kod, który będzie dodawał dane do bazy danych za pomocą entity framework.
Poprzedni artykuł - Testy Jednostkowe 100% Tego, Co Musisz O Nich Wiedzieć.
Następny artykuł - Testujemy Operacje na Bazie Danych - Wprowadzenie do Testów Integracyjnych w .NET.