NUnit - testy jednostkowe w .NET Core

1

Cześć,

Mam pytanie do osób, które znają temat testów jednostkowych. Z tego co na szybko się zorientowałem, takie testy wprowadza się głównie dla prostych funkcjonalności, nie stosuje się ich jeżeli mamy do czynienia z połączeniem z BD (DbContext).

W projekcie, który prowadzę wspólnie ze znajomymi architektura aplikacji na tą chwilę wygląda tak, że w kontrolerach mamy głównie metody GET i POST. Get wyświetla nam odpowiedni widok, natomiast metoda POST wysyła zapytanie do bazy danych. Nie ma obecnie w tej chwili w tym projekcie żadnych "prostych" metod typu: jeśli warunek jest spełniony to true, a jeśli nie to false.

Proszę o informację w jaki sposób (i czy w ogóle jest to możliwe) napisać testy jednostkowe do tego typu metod. Poniżej kilka przykładowych:

1) Logowanie

        [HttpPost]
        public async Task<IActionResult> Login(LoginViewModel model)
        {
            //login functionality
            var user = await _userManager.FindByNameAsync(model.Email);

            if (user != null)
            {
                //sign in
                var signInResult = await _signInManager.PasswordSignInAsync(user, model.Password, false, false);
                if (signInResult.Succeeded)
                {
                    return RedirectToAction("Index", "Home");
                }
            }

            return RedirectToAction("Index", "Home");
        }
  1. Weryfikacja eMail

        public async Task<IActionResult> VerifyEmail(string userId, string code)
        {
            var user = await _userManager.FindByIdAsync(userId);
    
            if (user == null) return BadRequest();
    
            var result = await _userManager.ConfirmEmailAsync(user, code);
    
            if (result.Succeeded)
            {
                return View();
            }
    
            return BadRequest();
        }
  2. Wylogowanie
        public async Task<IActionResult> LogOut()
        {
            await _signInManager.SignOutAsync();
            return RedirectToAction("Index", "Home");
        }
0

Jak dla mnie to za dużo masz w tych Controllerach. Ja bym to wyniósł do osobnej klasy i testował te klasę właśnie.
Tu masz w dokumentacji przykładowe testowanie: https://docs.microsoft.com/pl[...]s/testing?view=aspnetcore-3.1

Ale chyba najlepiej było by testować integracyjnie coś w tym stylu: https://docs.microsoft.com/pl[...]ion-tests?view=aspnetcore-3.1

0

@szydlak:

Tutaj według mnie warto zaznaczyć że lepiej trzymać się z dala potworka zwanego UseInMemoryDatabase

1
Krispekowy napisał(a):

Mam pytanie do osób, które znają temat testów jednostkowych. Z tego co na szybko się zorientowałem, takie testy wprowadza się głównie dla prostych funkcjonalności, nie stosuje się ich jeżeli mamy do czynienia z połączeniem z BD (DbContext).

Gdzie się zorientowałeś, że takie testy wprowadza się do prostych funkcjonalności? Testy jednostkowe mają testować jedną funkcjonalność i jak najbardziej może ona być skomplikowana. Co do DbContextu to proponuje przeczytać o mockowaniu baz danych w testach jednostkowych np. EF Effort.

0

A więc podsumowując? Bo widzę że wśród komentujących też są rozbieżności. W takim razie testować czy nie testować? :)

Jeżeli mockowanie może powodować przekłamanie bo nie odzwierciedla relacji to jest to w naszym projekcie rozwiązanie do wyrzucenia.

Korzystamy z EF i wersji .Net core 3.1

2

Zdecydujcie jako zespół (product owner, developerzy, menadżer). Możesz używać in memory database (chwile mi zeszło z odpowiednim przygotowaniem, ale później działało spoko), możesz testować na specjalnej bazie stworzonej do testów i po każdym teście usuwać dodane rekordy (albo i nie, jeśli to nie problem), możesz robić testy tylko tam gdzie nie jest potrzebne połączenie z bazą. Możliwości jest sporo, ja jednak pisałbym testy.

1

Na pewno testować, ale zastanów się w jaki sposób chcesz to zrobić

Tutaj ładnie są zebrane wady/zalety prawdziwej bazy, sqlite i InMemory.

Testing code that uses EF Core

Jest też możliwość zrobienia tak, że np. lokalnie testy lecą przez SQLite, a ci/cd używa prawdziwej bazy

Więc masz ten szybki development i łatwość postawienia lokalnie appki oraz rzetelność, bo nadal ci/cd sprawdzi na prawdziwej bazce.

8

Testy jednostkowe mają testować jednostki, a nie jakieś "funkcjonalności" (czymkolwiek są, bo jedna aplikacja może mieć jedną funkcjonalność, więc stosowanie tu liczby mnogiej odbiera sens wypowiedzi). Celem przetestowania jednostki jest na ogół stwierdzenie, czy dla podanych danych otrzymujesz spodziewane wyniki.
No i teraz jeśli założymy, że jednostką, którą chcesz przetestować jest metoda Login, która zawsze zwraca ten sam RedirectToAction, to właściwie nie ma znaczenia jaki LoginViewModel tam podamy, więc test niczego nie sprawdzi. Podobnie jest z Logout, a z VerifyEmail jest niewiele lepiej.

Ogólnie testowanie jednostkowe kontrolerów nie ma sensu, po pierwsze dlatego, że kontrolery raczej powinny oddelegowywać wykonanie zadania gdzieś dalej niż same zawierać logikę, a po drugie dlatego, że takie testy rzadko będą w jakikolwiek sposób wartościowe i pozwolą wykryć błędy. Co z tego, że test jednostkowy kontrolera przejdzie, skoro aplikacja nie będzie działać, bo nie będzie widoku, do którego chcesz zrobić redirect? Albo żądanie nawet nie osiągnie kontrolera, bo jakiś filtr/middleware/model binder po drodze będzie zepsuty?
W takich przypadkach lepiej testować e2e i faktycznie weryfikować, czy po podaniu prawidłowych danych, użytkownik staje się zalogowany. Interakcja z GUI, to jest coś, co się testuje na postawionej gdzieś aplikacji, często ręcznie, albo przy użyciu specjalizowanych frameworków.

Używaj testów jednostkowych tam, gdzie mają sens - do przetwarzania danych, a nie interakcji użytkownika ze stroną na podstawie danych zawartych w bazie. Np. kod odpowiadający za dodanie dwóch liczb, obliczenie wartości rabatu na podstawie liczby produktów w koszyku oraz dotychczasowej wartości zamówień klienta, obliczający wartość zaliczki na podatek dochodowy na podstawie kwoty zarobków, to są miejsca, w których testy jednostkowe się sprawdzą doskonale. Masz wejście, funkcję i wyjście, sam kod, bez żadnej komunikacji po HTTP, używania baz danych i interakcji z GUI.

3

Być może lekko uzupełnię wypowiedź somekind.
Nie testuj frameworka ani cudzych bibliotek. Często ludzie wpadają w taką pułapkę, że zaczynają to robić. Np. sprawdzają, czy podając niepoprawne hasło do bazy, połączymy się z nią, czy wyskoczy błąd. To już zostało przetestowane przez twórców tych bibliotek i frameworków. Zatem...

Testuj TYLKO swój kod.
W kontrolerach masz za dużo. Kontrolery powinny być głupie i przekazywać pracę gdzie indziej. Przykładowo u Ciebie - powinieneś pozbyć się z kontrolera obiektu userManager. Za to powinieneś stworzyć sobie taki interfejs:

public interface IUserService
{
    Task<bool> Login(LoginData data);
    Task<bool> Logout();
    Task<bool> VerifyEmail(EmailData data);
}

Następnie powinieneś stworzyć sobie na tej podstawie serwis:

public class UserService: IUserService
{
 //
}

i tutaj mieć całą tą logikę, którą masz w kontrolerze teraz. Razem z obiektem userManager.
Oczywiście musisz pamiętać o tym, żeby ten serwis zarejestrować w dependency injection.

I potem w swoim kontrolerze posługujesz się tylko tym serwisem:

[HttpPost]
        public async Task<IActionResult> Login(LoginViewModel model)
        {
            //login functionality
           bool loginResult = await userService.Login(model); 
           if(loginResult)
             return RedirectToAction("Index", "Home");
           else
             return RedirectToAction("BadLogin", "Home");
        }

I analogicznie z VerifyMail.
Co Ci to daje? Wiele. Przede wszystkim masz głupie kontrolery, które przekazują zadanie dalej. Kontrolery są ściśle powiązane z frameworkiem. Po drugie możesz mockować UserService. Po trzecie - możesz testować UserService. W tym KONKRETNYM przypadku testowanie UserService nie ma jednak sensu, bo skończyłoby się to na testowaniu frameworka. Jeśli Twój UserService się rozrośnie, to wtedy tak. W tym momencie wszystko tak naprawdę delegujesz do UserManagera.
Testuj swój serwis w momencie, gdy zyska on jakąś logikę. Np. po poprawnym zalogowaniu musi coś zrobić. Albo po stworzeniu konta musi utworzyć jakieś dane, stworzyć token do weryfikacji maila i być może nawet wysłać maila aktywacyjnego. I wtedy to testujesz w taki sposób:

  • podajesz poprawne dane i patrzysz, czy serwis wysłał maila
  • podajesz niepoprawne dane i patrzysz, co się stało (serwis nie powinien wysłać maila)

Co do mockowania - mockowanie to takie trochę oszukiwanie. Popatrz na taki przykład:

public class AccountService: IAccountService
{
    IUserService us;

    public AccountService(IUserService us)
    {
       this.us = us;
    }
}

Jak widzisz mamy tutaj klasę AccountService, którą będziesz chciał testować. Jest ona zależna od UserService. I dlatego stworzyłeś interfejs IUserService, żeby nie posługiwać się konkretną implementacją. Dzięki temu możesz stworzyć nową klasę:

public class FakeUserService: IUserService
{
    public bool ShouldLogin {get; set;}

    public Task<bool> Login(LoginData data)
    {
       return Task.FromResult(ShouldLogin);
    }
}

Stworzyłeś zupełnie nową implementację interfejsu IUserService. Teraz tą klasę możesz przekazać do AccountService podczas testów. A dlaczego tak? Bo testy jednostkowe mają testować jedną konkretną rzecz. Czyli jakąś metodę w klasie AccountService. Jeśli ta metoda jest zależna od UserService, to tak naprawdę testowałbyś jednocześnie AccountService i UserService. W tym przypadku dokładnie wiesz jak się zachowa FakeUserService. I możesz przetestować klasę AccountService jeszcze lepiej. Możesz zobaczyć co się stanie, gdy logowanie się udało, a co, gdy nie. Ale jest coś lepszego. Nie musisz tworzyć klasy FakeUserService, żeby to zrobić. Poczytaj o mockowaniu (i bibliotece Moq). Ona to zrobi za Ciebie w bardzo prosty sposób, np:

[Test]
public void Blabla_UserServiceBadLogin_ShouldThrow()
{
    Mock<IUserService> mock = new Mock<IUserService>();
    mock.Setup(x => x.Login(It.IsAny<LoginData>).ReturnsAsync(false));  //*

    AccountService service = new AccountService(mock.Object);
    //i tutaj testujesz metodę w account service
}
  • nie pamiętam dokładnie konstrukcji tego mocka, więc coś może się nie zgadzać, ale mniej więcej tak się to robi.

Mam nadzieję, że wyczerpałem temat :)

0

Gdzie mogę znaleźć informację na temat właśnie tego rodzaju "dobrych praktyk"? Może coś przeoczyłem, ale na stronie Microsoftu też w controllerach zawarta jest logika: https://docs.microsoft.com/pl[...]s/testing?view=aspnetcore-3.1

EDIT:
Przejrzałem niektóre opensource'owe projekty na github w tej technologii i zauważyłem, że też w kontrolerach jest sporo logiki. Być może wszyscy popełniają te same błędy, dlatego chciałbym nabyć dobre praktyki jak najwcześniej.

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0