Unit Tests – SUT (System Under Test) Factory

W Unit Testach (testach jednostkowych) powinno się testować tylko jedna klasę (jakiś tam serwis). Gdyby testować kilka na raz to już nie jest Unit Test tylko Integration Test, ale dziś nie o tym.

Ta jedna klasa, bohater kilku testów powinna być stworzona tylko raz (coś jak unikanie używania operatora new w zwykłym kodzie) – służy do tego prywatna funkcja, która u mnie nazywa się CreateService(). Dzięki temu, że będzie tworzona tylko w jednym miejscu, zaoszczędzimy na przyszłych refactoringach, gdy nasz HeroService będzie się zmieniać, rozrastać.

Usprawnienia krok po kroku

Punkt startowy – tworzenie testowanego obiektu w każdym teście. Jest to przykład „sztywny”, który nie zapewnia nam tego, co chcemy (minimum zmian w teście gdy zmienia się nasza klasa HeroService). Krok po kroku będziemy więc ten kod „usprawniać”.

W przykładach korzystam z biblioteki do mockowania moq.

    public void Given_when_then_etc()
    {
        var userService = new Mock<IUserService>();
        var configurationProvider = new Mock<IConfigurationProvider>();

        var service = new HeroService(
            userService.Object,
            configurationProvider.Object);

        var result = service.Calculate(5);
        // asserts
    }

Następny etap to utworzenie prywatnej metody tworzącej testowany obiekt:

HeroService CreateService(
    Mock<IUserService> userService, 
    Mock<IConfigurationProvider> configurationProvider)
{
    return new HeroService(
        userService.Object, 
        configurationProvider.Object);
}

Jeśli nasze testy nie korzystają z ConfigurationProvidera to można go pominąć, czyli wrzucić zawsze pustego mocka:

HeroService CreateService(
    Mock<IUserService> userService)
{
    return new HeroService(
        userService.Object, 
        new Mock<IConfigurationProvider>().Object);
}

Jeśli jedne testy mockuja IUserService a inne nie to możemy skorzystać z opcjonalnych parametrów:

    HeroService CreateService(
        Mock<IUserService> userService = null) // Optional parameter
    {
        // Create default mock if no mock specified
        userService = userService ?? new Mock<IUserService>();

        return new HeroService(
            userService.Object,
            new Mock<IConfigurationProvider>().Object);
    }

    public void Test_with_mocked_user_service()
    {
        var userService = new Mock<IUserService>();
        userService.Setup(x => x.GetUser())
            .Returns(new User());

        var service = CreateService(userService);

        var result = service.Calculate(5);
        // asserts
    }

    public void Simpler_test_without_specific_user_service()
    {
        var service = CreateService();

        var result = service.Calculate(5);
        // asserts
    }

Klasa może mieć wiele zależności, więc wszystkie ustawiamy jako opcjonalne

    HeroService CreateService(
        Mock<IUserService> userService = null,
        Mock<IAuthenticationService> authenticationService = null,
        Mock<ISecurityService> securityService = null)
    {
        userService = userService ?? new Mock<IUserService>();
        authenticationService = authenticationService ?? new Mock<IAuthenticationService>();
        securityService = securityService ?? new Mock<ISecurityService>();
    
        return new HeroService(
            userService.Object,
            authenticationService.Object,
            securityService.Object,
            new Mock<IUnitOfWorkFactory>().Object, // always not important so created here
            new Mock<IConfigurationProvider>().Object);
    }

A następnie poprzez „Named Parameters” ustawiamy, dzięki czemu reorganizacja kolejności będzie bezpieczna oraz nie trzeba będzie wpisywać nulli podczas wywoływania CreateService().

    public void Test_only_with_mocked_authenticatedService()
    {
        var authenticationService = new Mock<IAuthenticationService>();
        authenticationService.Setup(x => x.IsAuthenticated())
            .Returns(true);

        var service = CreateService(authenticationService: authenticationService);
        // instead of:
        // var service = CreateService(null, authenticationService);
    }

Na początku myślałem, że dobrze od razu stworzyć przeładowania dla wszystkich, teraz podchodzę do tego lazy i CreateService() dostaje nowy opcjonalny parametr tylko gdy jest to już konieczne – wykorzystywane w testach.

BTW jest to jeden z nielicznych przykładów, gdzie podoba mi się korzystanie z opcjonalnych parametrów.

Zbyt dużo zależności

Czy powinno być aż tyle serwisów? – to już inne pytanie, na które dziś nie będę odpowiadał. Gdy zrobimy sobie jakieś założenia odnośnie maksymalnej ilości zależności jakie może mieć klasa to ciągle jest problem refactorowania, aby zamienić jedne zależności na inne (np. agregacja kilku serwisów do jednego).

Chodziło mi o pokazanie, że nawet gdyby nie wiadomo ile tych zależności nie było to i tak można sobie poradzić poprzez stosowanie fabryki (prostsze refactoringi).

Fakt, że w jednych testach w ten samej klasie potrzebujemy innych zależności niż w innych testach jest pewnym code smell, na którego są inne sposoby. Na pewno jednak SUT Factory jest rozwiązaniem przy przechodzeniu do idealnego stanu, gdzie klasy przyjmują mniej zależności, wszystkie one są właściwe, a klasy wykonuje tylko jedną rzecz (Single Responsibility Principle). Czego sobie i Wam życzę 😉

Dodane

Inspirowane SUT Factory (by Mark Seemann) z tym, że nie tworzę osobnej klasy fabryki, ale to już mała różnica.

Reklamy
Ten wpis został opublikowany w kategorii Programowanie i oznaczony tagami . Dodaj zakładkę do bezpośredniego odnośnika.