Blog Dla Młodszych Programistów C#/.NET

wtorek, 10 marca 2020
Dzisiaj poruszę bardzo ważny temat, bez którego znajomości, nie nauczysz się pisać dobrych testów jednostkowych. Jak przeczytałeś już w poprzednich artykułach, testy jednostkowe nie powinny dotykać zewnętrznych zasobów. Jak w takim razie testować logikę w metodach, które mają odwołanie do zasobów zewnętrznych? Trzeba użyć sztucznych obiektów, tak zwanych mocków.

Jak Pisać Testy Jednostkowe? Przykład Testu Jednostkowego w .NET Dla Początkujących


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: But was: User or password is incorrect". Możemy również skonfigurować wywołanie tej metody dla różnych parametrów, dzięki It.IsAny<string>() metoda ta może zwracać ten sam wynik dla wszystkich stringów. Tak wygląda cała implementacja:

[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.
Autor artykułu:
Kazimierz Szpin
Kazimierz Szpin
Programista C#/.NET. Specjalizuje się w ASP.NET Core, ASP.NET MVC, ASP.NET Web API, Blazor, WPF oraz Windows Forms.
Autor bloga ModestProgrammer.pl
Komentarze (3)
Dariusz Gruca
DARIUSZ GRUCA, wtorek, 10 marca 2020 20:26
Świetny artykuł :) Dopełniając wiedzę, mocki to po polsku atrapy. Przekazanie do konstruktora interfejsu nazywamy wstrzykiwaniem zależności inaczej dependency injection :) alternatywą do Moq jest Nsubstitute. Powiedz mi proszę zawszę mam problem z nazewnictwem, na Moq mówimy pakiet, biblioteka czy może framework ? A może mogę używać tych określeń zamiennie?
Kazimierz Szpin
KAZIMIERZ SZPIN, wtorek, 10 marca 2020 20:59
Moq to framework, nie powinno się używać zamiennie tych określeń :) Jak instalujesz Moq przez NuGeta, to jest tam nawet opis: "Moq is the most popular and friendly mocking framework for .NET" :)
Dariusz Gruca
DARIUSZ GRUCA, wtorek, 10 marca 2020 21:56
Dzięki wielkie. Tyle już lat w branży a człowiek musi się dalej uczuć :) taki zawód :)
Dodaj komentarz

Wyszukiwarka

© Copyright 2024 modestprogrammer.pl. Wszelkie prawa zastrzeżone. Regulamin. Polityka prywatności. Design by Kazimierz Szpin