MoqCzęść NR.2 Wcześniej pokazałem Ci jak przebiega praca programisty z TDD. Dziś będziemy rozbijać klasy na pojedyncze cegiełki, bo inaczej nie można tego testować, gdy jest wiele klas. Na razie wszystko wygląda w porządku, bo mamy jedną klasę. Sprawy jednak się skomplikują, gdy będziesz miał więcej klas i zależności pomiędzy nimi. Zobaczymy jak to możemy ugryźć

W poprzednim wpisie stworzyliśmy klasę GameBuyingRequestProcessor, która zajmuje się przyjęciem zakupionej gry. Spełniliśmy już do niej następujące wymagania .

  • Ma ona zwracać te same dane, które otrzymaliśmy zapytaniu
  • Ma wyrzucić wyjątek, gdy zapytanie będzie NULL
  • Ma zwrócić status zapytania poprawny, gdy wysłaliśmy poprawne dane i proces przebiegł poprawnie

W następnych wpisach zrealizujemy kolejne założenia:

  • Zapis zakupu gry do bazy danych
    • Tylko zaraz jeszcze nie mamy bazy danych. Nauczymy się dziś jak obejść ten wymóg i napisać już do niego test. Musimy tylko oddzielić logikę łączenia się z bazą danych. Jak to zrobimy? Zaraz się przekonasz.
  • Sprawdzenie czy dana gra może być kupiona
    • Powinniśmy móc kupić grę tylko, gdy istnieje taka możliwość i mamy ją w bazie danych
  • Sprawdzenie czy jest powiązanie zakupu ID z daną grą 
  • Ulepszymy też status działania zakupu gry. Czyli wywalimy jeden z testów by napisać lepszy, ponieważ wymógł się zmienił. Chcemy otrzymać status sukcesu, gdy zakup przebiegł poprawnie. Chcemy też otrzymać status GameDoesntExist, gdy dana gra nie istnieje oraz status GameNotAvailable, gdy nie możemy jej kupić, ale gra istnieje.
  • Powinniśmy też zwrócić ID zakupu po dokonaniu operacji.

Jak widzisz nasza aplikacja nie jest już piaskownicą zabaw. Sprawa zaczyna wyglądać poważnie i profesjonalnie.

Zaczynamy od zrealizowania wymagania zapisu zakupu gry, gdyż od tego wymagania zależne są inne wymagania.

Jak odseparować zależności

Według tego wymagania możemy już sobie wyobrazić, że w trakcie działania metody BuyGame, która przyjmuje obiekt GameBuyingRequest będziemy musieli stworzyć obiekt kupionej gry  "BoughtGame" i zapisać ją do bazy.

Diagram GameBuyingRequestProcessor i problem z bazą danych

Jednak nie mamy jeszcze bazy danych. Jak możesz więc przetestować te wymaganie, jeśli nie masz bazy? Słyszałeś na pewno o zasadzie "Single Responsibility Principle" czyli o zasadzie pojedynczej odpowiedzialności.

Mówi ona, że każdy komponent, czyli klasa,metoda powinna mieć jedną odpowiedzialność.

Jak jednak widzisz na tym obrazku metoda BuyGame ma już przynajmniej dwie odpowiedzialności

  • Przetworzenie GameBuyingRequest
  • Oraz zapis obiektu BoughtGame

Wiemy, że przetworzenie GameBuyingRequest jest już naszym zrealizowanym wymaganiem więc tutaj jest wszystko w porządku. 

Czy jednak on powinien być odpowiedzialny za zapis do źródła danych? Oczywiście,że nie.

Zgodnie z zasadą pojedynczej odpowiedzialności każda klasa lub metoda powinna mieć tylko jeden powód do zmiany. Na chwilę obecną, jeśli chcemy zmienić przetworzenie obiektu GameBuyingRequest to wiesz gdzie ten kod trzeba zmienić.

Gdybyś jednak chciał zmienić zapis zakupu to też byś musiał w tej metodzie grzebać. Widać wyraźnie, że mamy tutaj dwie odpowiedzialności. 

Nasz procesor nie powinien wiedzieć "JAK" zapisać zakupioną grę do źródła danych.

Przedstaw więc kolejną klasę "GameBuyingRepository", która będzie miała metodę Save. To ona będzie odpowiedzialna za zapis dokonanego zakupu.

Diagram rozbijamy na dwie klasy

Nasz procesor wciąż musi zapisywać zakup. Jednak nie stoi nic na przeszkodzie aby on nawiązał kontakt z nową klasą i wykonał w niej metodę Save.

Jak idzie komunikacja pomiędzy obiektami

Teraz widzisz, że nasz procesor nie jest zależny od źródła danych i też nie musi wiedzieć JAK obiekt zakupu jest zapisywany.

Teraz jednak mamy kolejny problem. Nasza klasa GameBuyingRequestProcessor jest zależna od klasy GameBuyingRepository i jej metody save. Ta struktura nie będzie działać dla testów jednostkowych.

Pamiętaj my chcemy przetestować jeden przypadek w jednym kroku. Nie chcemy testować zbioru wszystkich zasad i wymagań. Co więcej, nasz test nie powinien być zależny od bazy danych, która może żyć gdzieś na serwerze i mieć zmienną naturę.

Na ratunek przychodzi kolejna zasada projektowa. Dependency Inversion Principle, czyli zasada odwrócenia zależności.

Mówi ona, że każdy komponent musi być zależny od abstrakcji danego elementu, a nie od jego implementacji. Czy jest ta abstrakcja? Jest to np. interfejs bądź klasa abstrakcyjna. 

public interface IGameBuyingRepository
{
    void Save();
}

Rozbijamy więc tą zależność do GameBuyingRepository i tworzymy do niego interfejs.

Nazwałem go IGameBuyingRepository 

Pojawia się interfejs w diagramie

Teraz ten Interfejs będziemy przekazywać do konstruktora  GameBuingRequestProcessor.

A później metoda BuyGame wywoła metodę Save.

Procesor gada z interfejsem

Repozytorium używane na produkcji implementuje ten interfejs i zapiszę zakup.

interfejsem implementowany jest na produkcji

Teraz widzisz, że procesor jest zależny od interfejsu, czyli od abstrakcji, a nie od implementacji, która zagląda do bazy.

Czyli teraz możemy testować nawet nie mając implementacji repozytorium.

Oczywiście teraz zadajesz sobie bardzo dobre pytanie. Przecież na interfejsie nie możemy pracować. Przypadkiem nie mamy tutaj kolejnego problemu. Jak możemy testować kod nie mając kodu, który mówi "JAK" coś mamy zrobić?

Aby więc napisać pierwsze wymaganie musimy przedstawić ten interfejs i w teście jednostkowym zrobić MOCK, który ten interfejs będzie implementować.

Pojawia MOCK

Ten obiekt MOCK nie zagląda do bazy. To fałszywy obiekt potrzebny do naszego testu. Zweryfikujemy w ten sposób czy rzeczywiście wywołaliśmy metodę Save nie wnikając w to, co ona dokładnie robi.

Piszemy test

Na chwilę obecną mój test wygląda tak. Przeniosłem Request do konstruktora, ponieważ wiem, że ten kod będzie się powtarzał przy kolejnych testach.

public class GameBuyingRequestProcessorTests
{
    private GameBuyingRequestProcessor _processor;
    private readonly GameBuyingRequest _request;

    public GameBuyingRequestProcessorTests()
    {
        _processor = new GameBuyingRequestProcessor();

        // Arrange
        _request = new GameBuyingRequest()
        {
            FirstName = "Cezary",
            LastName = "Walenciuk",
            Email = "walenciukc@gmail.com",
            Date = DateTime.Now
        };
    }

    [Fact]
    public void ShouldReturnBuyingGameResultWhitRequestValues()
    {
        //Act
        GameBuyingResult result = _processor.BuyGame(_request);

        //Assert
        Assert.NotNull(result);
        Assert.Equal(_request.FirstName, result.FirstName);
        Assert.Equal(_request.LastName, result.LastName);
        Assert.Equal(_request.Email, result.Email);
        Assert.Equal(_request.Date, result.Date);
    }

    [Fact]
    public void ShouldThrowExecptionIfRequestIsNull()
    {
        var exception = Assert.Throws<ArgumentNullException>(

             () => _processor.BuyGame(null)

        );

        Assert.Equal("request", exception.ParamName);
    }

    [Fact]
    public void ShouldReturnStatusTrueWhenSendedCorrectValues()
    {
        // Arrange
        var request = new GameBuyingRequest()
        {
            FirstName = "Cezary",
            LastName = "Walenciuk",
            Email = "walenciukc@gmail.com",
            Date = DateTime.Now
        };

        //Act
        GameBuyingResult result = _processor.BuyGame(request);

        //Assert
        Assert.Equal(true, result.IsStatusOk);
        Assert.Equal(0, result.Errors.Count);
    }
}

Piszemy więc kolejny test do sprawdzenia.

[Fact]
public void ShouldSaveBoughtGame()
{

}

Do projektu z kodem tworzę folder, a w nim utworzę potrzebny interfejs.

Stworzenie folder interfejs

Oto kod interfejsu. Nie ma jednak jeszcze obiektu zakupionej gry.

public interface IGameBuyingRepository
{
    void Save(GameBought gameBought);
}

Ponownie korzystam z Visual Studio by taką klasę wygenerować

Stworzenie klasy w Visual Studio

Na razie klasie GameBought mam dokładnie te same pola co w request.

using System;

namespace GameShop.Core
{
    public class GameBought
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public DateTime Date { get; set; }
    }
}

Teraz wypadałoby zmodyfikować kod tak aby można było napisać ten test. W końcu nasz interfejs jest jeszcze nigdzie nieużywany.

[Fact]
public void ShouldSaveBoughtGame()
{
    _processor.BuyGame(_request);
}

Jeszcze taki test w sumie niczego nie sprawdza. Jak widzisz w konstruktorze naszego procesora jeszcze nie korzystamy z naszego repozytorium.

public GameBuyingRequestProcessorTests()
{
    _processor = new GameBuyingRequestProcessor();

    // Arrange
    _request = new GameBuyingRequest()
    {
        FirstName = "Cezary",
        LastName = "Walenciuk",
        Email = "walenciukc@gmail.com",
        Date = DateTime.Now
    };
}

Najpierw jednak stwórzmy pole prywatne w teście z naszym interfejsem, które będzie zaimplementowane przez MOCK.

public class GameBuyingRequestProcessorTests
{
    private Mock<IGameBuyingRepository> _repositoryMock;

Teraz musimy zainstalować paczkę Moq by robić fałszywe implementacje obiektów.

Piszą kod MOCK pola może zainstalować paczkę Moq w taki sposób.

Instalacja paczki Moq w Visual Studio przez kod

Możesz też bezpośrednio wejść do opcji paczek NuGet w projekcie klikając na Dependencies i zainstalować paczkę Moq.

otworzenie NuGetPackages

Wpisujesz Moq i znajdujesz odpowiednią paczkę i instalujesz.

Zainstalowanie Moq

W konstruktorze uzupełniamy definicję tego pola.

public GameBuyingRequestProcessorTests()
{
    _repositoryMock = new Mock<IGameBuyingRepository>();

Dodaje nowy parametr do konstruktora naszego procesora. Zauważ, że w MOCK korzystam z właściwości object. To pozwala mi na odwołanie się do fałszywego obiektu.

public GameBuyingRequestProcessorTests()
{
    _repositoryMock = new Mock<IGameBuyingRepository>();
    _processor = new GameBuyingRequestProcessor(_repositoryMock.Object);

Teraz jednym kliknięciem mogę utworzyć nowy konstruktor do naszego procesora. 

Wstrzykiwanie zależności

Mój procesor na chwilę obecną wygląda tak:

public class GameBuyingRequestProcessor
{
    private IGameBuyingRepository @object;

    public GameBuyingRequestProcessor()
    {
    }

    public GameBuyingRequestProcessor(IGameBuyingRepository @object)
    {
        this.@object = @object;
    }

    public GameBuyingResult BuyGame(GameBuyingRequest request)
    {
        if (request == null)
            throw new ArgumentNullException(nameof(request));

        var result = new GameBuyingResult();

        result.FirstName = request.FirstName;
        result.LastName = request.LastName;
        result.Date = request.Date;
        result.Email = request.Email;
        result.IsStatusOk = true;
        result.Errors = new System.Collections.Generic.List<string>();

        return result;
    }
}

Pora na napisanie testu, który zakończy się czerwoną lampką.

Najpierw tworze obiekt zakupu gry, który ma być zapisany w bazie. Jest on NULL, ponieważ nie odgrywa on tutaj dużej roli na razie. Będziemy go uzupełniać później.

[Fact]
public void ShouldSaveBoughtGame()
{
    GameBought savedgameBought = null;

    _repositoryMock.Setup(x => x.Save(It.IsAny<GameBought>()))

        .Callback<GameBought>(game =>
       {
           savedgameBought = game;
       }

    );

    _processor.BuyGame(_request);
}

Później używam MOCK i muszę go odpowiednio ustawić korzystając z metody Setup.

Mówie mu, że chce przy wywołaniu metody SAVE dla jakiekolwiek obiektu GameBought.

Korzystając z metody CallBack złapie wysłany do metody obiekt GameBought i przypiszę go do zmiennej savedgameBought;

Pora na weryfikację

Najpierw w MOCK mogę sprawdzić czy metoda Save w ogóle została uruchomiona. Mogę też sprawdzić czy została wywołana tylko raz.

_repositoryMock.Verify(x => x.Save(It.IsAny<GameBought>()), Times.Once);

Później sprawdzam czy obiekt GameBought ma te same właściwości co Request.

Assert.NotNull(savedgameBought);
Assert.Equal(_request.FirstName, savedgameBought.FirstName);
Assert.Equal(_request.LastName, savedgameBought.LastName);
Assert.Equal(_request.Email, savedgameBought.Email);
Assert.Equal(_request.Date, savedgameBought.Date);

Wygląda to dobrze. Nasz test jest gotowy:

[Fact]
public void ShouldSaveBoughtGame()
{
    GameBought savedgameBought = null;

    _repositoryMock.Setup(x => x.Save(It.IsAny<GameBought>()))

        .Callback<GameBought>(game =>
       {
           savedgameBought = game;
       }

    );

    _processor.BuyGame(_request);

    _repositoryMock.Verify(x => x.Save(It.IsAny<GameBought>()), Times.Once);

    Assert.NotNull(savedgameBought);
    Assert.Equal(_request.FirstName, savedgameBought.FirstName);
    Assert.Equal(_request.LastName, savedgameBought.LastName);
    Assert.Equal(_request.Email, savedgameBought.Email);
    Assert.Equal(_request.Date, savedgameBought.Date);
}

Oczywiście jak uruchomimy nasz nowy test w Test Explorer to on nie przejdzie.

Nowy test się pojawia

Jesteśmy w fazie RED.

Test przechodzi na czerowno

Test nie przechodzi na razie przez wszystkie warunki. Pierwszy jednak dotyczy tego, ze w ogóle nie uruchamiamy metody Save.

Faza Green

Analogicznie do poprzednich przykładów teraz musimy zmienić kod tak aby test przeszedł.

Nasza metoda BuyGame musi wywołać metodę Save i utworzyć poprawnie obiekt GameBought

    public class GameBuyingRequestProcessor
    {
        private IGameBuyingRepository _repository;

        public GameBuyingRequestProcessor(IGameBuyingRepository repository)
        {
            _repository = repository;
        }

        public GameBuyingResult BuyGame(GameBuyingRequest request)
        {
            if (request == null)
                throw new ArgumentNullException(nameof(request));


            GameBought gameBought = new GameBought();
            gameBought.FirstName = request.FirstName;
            gameBought.LastName = request.LastName;
            gameBought.Date = request.Date;
            gameBought.Email = request.Email;

            _repository.Save(gameBought);

            var result = new GameBuyingResult();

            result.FirstName = request.FirstName;
            result.LastName = request.LastName;
            result.Date = request.Date;
            result.Email = request.Email;
            result.IsStatusOk = true;
            result.Errors = new System.Collections.Generic.List<string>();

            return result;
        }
    }

Wywaliłem też bez parametrowy konstruktor i zmieniłem nazwę pola prywatnego @object na _repository.

Teraz test powinien przejść na zielono i nie tylko ten test w końcu niczego nie popsuliśmy.

Wszystkie testy na zielono

Refactoring

Moja pierwsza zmiana polega na przeniesieniu procesora do innego folderu

Stworzenie nowego folderu

Jak zauważyłeś aż 3 klasy mają w sobie te same właściwości. Możemy z 3 klasy zrobić jedna klasę, ale nigdy nie wiesz jakie parametry będą potrzebne do Request, a jakie do Response , a jakie do zapisu obiektu w bazie więc to zostawiamy.

Stwórzmy więc klasę bazową

public class GameBuyingBase
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public DateTime Date { get; set; }
}

I dodajmy ją do każdej z klas 

public class GameBuyingRequest : GameBuyingBase
{
    public GameBuyingRequest()
    {
    }
}
public class GameBuyingResult : GameBuyingBase
{
    public bool IsStatusOk { get; set; }
    public List<string> Errors { get; set; }
}
public class GameBought : GameBuyingBase
{

}

Teraz gdy będziemy potrzebować więcej pól w tych samych klasach na raz to wystarczy, że zmienimy klasę bazową.

W przyszłości też możemy się zastanowić nad zmianą nazw tych klas. Request i Response jest moim zdaniem czytelny, ale czas przeszły definiujący zakupioną grę może nie jest tutaj na miejscu.

W samym procesorze dla ułatwienia kodu można stworzyć metodę generyczną, która zwróci odpowiednią klasę z skopiowanymi polami.

private static T Create<T>(GameBuyingRequest request) where T : GameBuyingBase, new()
{
    return new T()
    {
        Date = request.Date,
        Email = request.Email,
        FirstName = request.FirstName,
        LastName = request.LastName
    };
}

Teraz metoda Buy będzie dużo krótsza.

public GameBuyingResult BuyGame(GameBuyingRequest request)
{
    if (request == null)
        throw new ArgumentNullException(nameof(request));


    GameBought gameBought = Create<GameBought>(request);

    _repository.Save(gameBought);

    var result = Create<GameBuyingResult>(request);

    result.IsStatusOk = true;
    result.Errors = new System.Collections.Generic.List<string>();

    return result;
}

To by było na tyle w następnym wpisie zrobimy kolejne przypadki.