TDDCzęść NR.1 Czym jest TDD? Czyli Test Driven Development. Jest to styl tworzenie programowania, który polega na tym, że zanim napiszesz kod najpierwsz piszesz do niego testy jednostkowe.

Zanim zaczniesz pisać kodu czy to z TDD, czy bez masz na pewno już pewne stwierdzone wymagania do napisania kodu. 

Z TDD bierzesz te pojedyncze wymagania i zanim napiszesz swój pierwszy kod piszesz TEST, który sprawdza dane wymaganie.

Wymaganiem może być funkcja, która zwróci określony obiekt z określonymi polami. W teście byś sprawdził czy te pola są zwracane dla poprawnego zapytania.

Piszesz test jako pierwszy i wiadomo, że bez poprawnego kodu ten test będzie dawał wynik czerwony. Tak ma być, ponieważ logikę napiszesz potem. Test ma pokazać i pilnować Cię i innych programistów o poprawne działanie danej funkcji i jego danego wymagania.

Faza, etap RED w TDD

Mamy więc test z lampką czerwoną. Co potem? Potem piszesz najprostszy kod, który sprawi, że ten test wykona się poprawnie. 

Faza, etap Green w TDD

Na końcu robisz poprawki kodu (Refactoring) tak, aby test wykonywał się na zielono.

Faza, etap Refactor w TDD

Przechodzisz do następnego wymagania danego projektu i piszesz do niego kolejny test, który się nie wykona, czyli będzie miał czerwoną lampkę.

Tak wracam do początku tego cyklu procesu. Piszemy znowu kod by test wykonał sie na zielono, a potem robimy poprawki. 

Przechodzimy więc do kolejnego wymagania i robimy kolejny test, który początkowo ma czerwoną lampkę. 

Powrót do fazy RED W TDD

Z Test Driven Development masz więc taki cykl iteracyjny (RED-GREEN-REFACTOR) tworzenia swojej aplikacji. Teraz też powinieneś zrozumieć samą nazwę TDD.

W końcu twoje TESTY kierują (driven) twoją implementacją kodu. Sercem TDD jest właśnie ten proces RED-GREEN-REFACTOR

Cykl TDD

Jest on także nazywany cyklem TDD. Jakby więc ktoś Ciebie zapytałby czym jest te TDD to odpowieź jest prosta: Jest to tworzenie oprogramowania według cyklu budowania testów (RED-GREEN-REFACTOR).

Jakie są zalety?

Po pierwsze TDD wymusza na tobie dokładne przemyślenie potrzebnych klas, metod i właściwości. W końcu, jeśli tego nie wiesz to nie napiszesz testu. Dzięki temu istnieje duża szansa, że stworzone przez Ciebie klasy i metody będą klarowne i ograniczone do pojedynczej odpowiedzialności.

Gdy już to ustaliłeś musisz pomyśleć CO KOD powinnien robić. TDD pozwala tobie pomyśleć o tym CO KOD ma robić i na chwilę nie wnikać JAK TO ZROBI.

Pisząc test z czerwoną lampką i nie musisz spędzać czas nad myśleniem JAK coś ma być zrobione. Robisz to w następnym kroku. 

Gdy już masz kod i test to dostajesz szybką informację czy twój kod nadal działa poprawnie. Jest to kolejna zaleta.  Zamiast uruchamiać aplikację by sprawdzić czy wszystko jest OKEJ możesz uruchomić swój test. Jest to dużo szybsze sprawdzenie kodu niż uruchomienie całej aplikacji.  Co więcej, z testami możesz sprawdzać kod nie mając kompletnej aplikacji z wartswą UI. Może chciałbyś przetestować logikę biznesową, ale nie masz kodu obsługi tej logiki w stronie internetowej. 

Z testem jednostkowym nie ma takiego problemu. Z TDD w sumie możesz zacząć budować aplikację od warstwy biznesowej/logicznej, a nie od warstwy UI

Kolejna zaleta TDD polega na automatycznym tworzeniu modularność kodu. Czyli twój kod jest  rozbijany na mniejsze cegiełki. Aby przetestować klasę musisz ją odseparować od innych klas. Użyć wstrzykiwania zależności i kontenerów. 

TDD wymusza na Tobie takie działanie od samego początku. Skupiasz się wtedy na działaniu pojedynczych cegiełek swojej aplikacji i na wymaganiu danej cegiełki . Nie testujesz aplikacji w całości .

Co, jeśli twoja baza danych jest jeszcze nie gotowa? Nie ma problemu odłączasz bezpośrednią relację danych klas z bazą tak byś mógł działać na fałszywych statycznych kolekcjach lub na fałszywej bazie danych w twojej pamięci RAM.

Co, jeśli dane WEB API nie jest gotowe? Nie ma problemu odłączasz bezpośrednio relację z WEB API i twój test będzie działał na fałszywej usłudze.

To wszystko pozwala na pisanie kodu, który łatwiej zarządza. Jeśli dodajesz nowy kodu i chciałbyś sprawdzić czy czegoś nie rozwaliłeś...PROSTE. Uruchamiasz test i sprawdzasz wszystkie poprzednie przypadki działania kodu. 

Same testy są dokumentacją kodu. Testy w końcu mówią co dana klasa powinna robić. Patrząc na cudzy kod możesz od razu nie wiedzieć co on robi. Zadawać sobie pytania, jak "po co jest tam ten IF".

Z testem jednostkowym jest inaczej. Widzisz wszystkie przypadki, z którym działa dany kod.

Jak widzisz TDD ma dużo zalet, ale jakie są wady?

Ciężko jest zacząć pisać aplikację z filozofią TDD? Ktoś może nawet powiedzieć, że jest to przerost formy nad treścią i nie odczuwa on dużych zalet z działania z TDD.

Trzeba też pilnować innych pracowników by takie testy pisali. Tłumaczyć im, że najpierw piszemy testy, a nie, że najpierw kod, a potem test. Kto wie może jak odejdziesz z firmy to testy zostaną zakurzonymi projektami z których nikt nie korzysta.

Bo trzeba szybko coś poprawić więc nie ma mowy o tworzeniu nowego przypadku testowego. Takie są realia. 

Czy TDD jest umiejętnością, którą każdy programista powinien mieć? TAK, jednakże nie zawsze z metodyki TDD będziesz korzystał w swojej pracy. To wymaga czasem pracy wielu ludzi nie tylko programistów aby TDD mogło działać przy tworzeniu oprogramowania. 

Scenariusz do napisania aplikacji w stylu TDD

Ten kurs musimy zacząć od konkretnego przykładu. W końcu co innego jest pisać o TDD, a co innego jest zobaczyć jak proces pisania aplikacji w TDD wygląda. 

GameShop projekt solucja : 3 warstwy aplikacji

Stworzymy przy użyciu TDD prostą aplikację symulującą sklep z grami. Będzie ona miała 3 warstwy. Warstwę logiki biznesowej w GameShop.Core, Warstwę dostępu do danych w GameShop.DataAccess i warstwę prezentacji w GameShop.Web.

W projektach tutaj użyjemy ASP.NET CORE i Entity Frameworka

Po co to wszystko? Po to by Ci uświadomić, że każda warstwa ma swoje testy jednostkowe i nie powinniśmy ich mieszać ze sobą.

Na obecną chwilę tyle musisz wiedzieć. Z TDD możemy skoczyć prosto do pisania logiki biznesowej i od razu przejść do pisania testów bez martwienia się co potem zrobię z tym kodem w innych warstwach.

Zamiast myśleć "BIG PICTURE", czyli całościowo to koncentrujemy się teraz na pierwszych cegiełkach naszej aplikacji.

Jakie mamy pierwsze wymaganie 

Zanim napiszemy pierwszy test musimy zadać sobie pierwsze poważne pytanie. Jakie mamy pierwsze wymaganie. Bez niego nie możemy napisać testu.

Wiemy, że nasza aplikacja internetowa będzie chciała kupić grę, aby to zrobić będziemy musieli wysłać takie zapytanie o zakup do naszej warstwy logiki biznesowej.

W logice biznesowej będziemy musieli mieć do tego jakąś klasę np. GameBuyingRequestProcessor

Ta klasa musi mieć metodę do wysłania tego polecenia zakupu np. BuyGame. To jest nasze API.

W metodzie tej musimy przyjąć obiekt zapytania zakupu gry będzie to klasa : BuyGameRequest

W zapytaniu tym będziemy mieli dane użytkownika: jego imię, nazwisko, e-mail oraz datę wysłania zapytania. Spakujmy te dane do jednej klasy, aby nie dodawać dużo parametrów. 

W metodzie tej też będziemy zwracać status ukończenia tego zakupu oraz wszystkie dane użytkownika, które otrzymaliśmy wcześniej.

Mamy więc nasze pierwsze wymaganie i możemy teraz napisać aplikację w stylu TDD.

Tworzymy projekt w stylu TDD

Zaczynamy od tworzenia nowej solucji w Visual Studio.

Tworzenie nowego projektu w Visual Studio

Wybieramy pustą solucję.

Blank Solution w Visual Studio

Nazywamy ją GameShop

test_16.PNG

Dodajemy do niej odpowiednie foldery określające jej warstwy aplikacji.

Visual Studio Foldery Warstw

i do foldera Core dodamy nasz pierwszy test w końcu mamy od niego zacząć. Jak widzisz w Visual Studio mamy dużo szablonów do tworzenia testów w .NET :

  • MStest
  • NUnit
  • XUnit

Filozofia tych frameworków jest taka sama jednakże różnią się składnią kodu.

testy jednostkowe szablony w Visual Studio

Ja na potrzeby tego wpisu wybrałem test xUnit. W innym wpisie opiszę różnicę pomiędzy tymi frameworkami.

XUnit Wybieram w Visual Studio

Projekt testowy nazwiemy GameShop.Core.Test i od razu usuwamy z niego domyślną test jednostkowy.

Kasowanie domyślnego testu

Zaglądamy do właściwości projektu testowego...

Właściwości projektu w Visual Studio

...i zmieniamy domyślną przestrzeń nazw na GameShop.Core. Dlaczego? 

Jest to użyteczne, gdyż będziemy generować klasy w projekcie testowym, a potem je przenosić do właściwego projektu. Zrobimy to za chwilę.

Dzięki temu klasy, które stworzymy będą pod właściwą przestrzenią nazwy i nie będziemy musieli nic zmieniać.

Domyślna przestrzeń nazw w Visual Studio

Faza RED

Teraz tworzymy nową klasę i zadbajmy od nazewnictwo tej klasy. Chcemy przetestować działania klasy : GameBuyingRequestProcessor. Może tej klasy jeszcze nie mamy, ale już ją zaplanowaliśmy w notatkach.

Nasza klasa do testów nazywa się więc : GameBuyingRequestProcessorTest

public class GameBuyingRequestProcessorTest
{
    public void ShouldReturnGameBuyingResultWithRequestValues()
    {

    }
}

Dodajemy do tej klas nasze pierwsze wymaganie. Powinniśmy otrzymać obiekt zwrotny z tymi samymi wartościami, które otrzymaliśmy w zapytaniu. Sam obiekt zwrotny nie powinien być pusty. Zauważ, że nasza metoda nazywa się tak samo, jak nasze wymaganie. Teraz rozumiesz jak test mogą być dokumentacją kodu

Na górze metody oznaczamy ją atrybutem [Fact] jest to sygnał xUnit, żeby traktował tą metodę jako test, który ma uruchomić.

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

        //Act
        var processor = new GameBuyingRequestProcessor();
        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);
    }
}

Każdy test składa się z trzech etapów.

  • Z przygotować parametrów ("Arrange")
  • z działaniem ("Act")
  • z sprawdzaniem ("Assert")

Korzystając z klasy Assert będziemy sprawdzać czy dane pod dane wymaganie zostało spełnione. Jeśli tak nie będzie to test zakończy się czerwoną lampką.

Mamy więc napisany test, ale nie mamy do niego żadnych klas. Nie ma problemu możemy je utworzyć poprzez kreatora Visual Studio. Najeżdżasz myszką na brakującą klasę i wybierasz opcję "Generate class X in new file"

Tworzenie testu bez kodu

Najpierw tworzymy GameBuyingRequest. Jak widzisz Visual Studio wie jakie właściwości dopisać.

Generowanie klasy przez Visual Studio

Potem tworzymy metodę.

Generowanie metody przez Visual Studio

Na koniec zostaną ci właściwości.

Generowanie właściwości przez Visual Studio

Tutaj akurat musisz trochę kod poprawić. Najwidoczniej dla Visual Studio typ string może być kolekcją IEnumerable<char>.

internal class GameBuyingResult
{
    public IEnumerable<char> FirstName { get; internal set; }
    public IEnumerable<char> LastName { get; internal set; }
    public IEnumerable<char> Email { get; internal set; }
    public IEnumerable<object> Date { get; internal set; }
}

Robimy więc poprawkę.

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

Tak zakończyliśmy pierwszą fazę RED naszego cyklu TDD. Uruchamiasz test poprzez kliknięcie prawym przyciskiem myszki na projekt testowy i wybierasz "Run Tests".

uruchomienie testu w Visual Studio

Oczywiście test kończy się niepowodzeniem, ponieważ nasze klasy i metody nie mają żadnej logiki.

Testy na RED czerwono

Faza GREEN

Mamy więc test. Teraz pora na kod, który sprawi, że ten test zakończymy na zielono.

internal class GameBuyingRequestProcessor
{
    public GameBuyingRequestProcessor()
    {
    }

    internal GameBuyingResult BuyGame(GameBuyingRequest request)
    {
        throw new NotImplementedException();
    }
}

Wymaganie polega na zwróceniu tych samych wartości, które otrzymaliśmy w Request więc je zwracamy w obiekcie Response.

internal class GameBuyingRequestProcessor
{
    public GameBuyingRequestProcessor()
    {
    }

    internal GameBuyingResult BuyGame(GameBuyingRequest request)
    {
        var result = new GameBuyingResult();

        result.FirstName = request.FirstName;
        result.LastName = request.LastName;
        result.Date = request.Date;
        result.Email = request.Email;

        return result;

    }
}

Uruchamiamy ponownie test i mamy lampkę zieloną. Ten wymóg spełniliśmy. 

Testy na GREEN zielono

Faza Refactor

To mamy kod, który działa teraz czas na refactoring. Tworzymy nowy projekt do folderu Core.

Nowy projekt biblioteki .NET

Tworzymy projekt biblioteki .NET pod .NET Standardem.

Class Library (.NET Standard)

Nazywamy go GameShop.Core.

Nazwa nowego projektu biblioteki .NET

Zaznaczamy wszystkie plik z projektu testowego oprócz samego klasy testowej i...CTRL+C, CTRL+V i mamy te same pliki w projekcie GameShop.Core

Kopiowanie plików

Kasujemy pliki w folderze GameShop.Test. Nie potrzebujemy duplikatów.

Kasowanie plików

Teraz do projektu testowego dodajemy referencję do nowego projektu.

Dodanie referencji do projektu

Zaznaczamy nasz projekt.

Dodanie referencji do projektu zaznaczenie pola

Na koniec pozostało na zamienić wszystkie słowa kluczowe internal na public. Przez to klasy,metody nie są widoczne między projektami.

internal class GameBuyingRequest
internal class GameBuyingRequestProcessor
internal class GameBuyingResult

Na tym zakończyliśmy fazę refactoring. Pora na kolejne wymagania, albo może przypadki.

Kolejne wymagania

W przypadku wysłania pustego obiektu zapytania, czyli NULL do naszego API chcemy wyrzucić wyjątek. Oto nowe wymaganie, a nowe wymaganie wymaga nowego testu więc go piszemy.

[Fact]
public void ShouldThrowExecptionIfRequestIsNull()
{
    var processor = new GameBuyingRequestProcessor();


    var exception = Assert.Throws<ArgumentNullException>(

         () => processor.BuyGame(null)

    );

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

Oczywiście na początku test nie kończy się powodzeniem.

Test na czerwono znowu

Zmieniamy więc kod.

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;

    return result;

}

Teraz nasz test zakończy się na zielono.

W fazie refactoring pora na odseparowanie logiki tworzenia naszego procesora, gdyż już ona pojawia się dwa razy. Tworzymy więc instancje tego obiektu w konstruktorze testu.

public class GameBuyingRequestProcessorTests
{
    private GameBuyingRequestProcessor _processor;
    public GameBuyingRequestProcessorTests()
    {
        _processor = new GameBuyingRequestProcessor();
    }

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

        //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);
    }

}

Przejdźmy do następnego wymagania. Chcielibyśmy w zapytaniu mieć jeszcze status wykonania naszego zapytania. Naturalnie powinien mieć on wartość true, jeśli spodziewamy się, że nasze zapytanie wykonało się poprawnie. Lista błędów też powinna być pusta dla poprawnego zapytania.

Oto test takie przypadku.

[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);
}

Do klasy dodajemy nowe pola.

public class GameBuyingResult
{
    public string FirstName { get; internal set; }
    public string LastName { get; internal set; }
    public string Email { get; internal set; }
    public DateTime Date { get; internal set; }
    public bool IsStatusOk { get; set; }
    public List<string> Errors { get; set; }
}

Nowy test oczywiście kończy się na czerwono.

Nowy test na czerwono RED

Zmieniamy więc kod by test zakończył się na zielono

public class GameBuyingRequestProcessor
{
    public GameBuyingRequestProcessor()
    {
    }

    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;
    }
}

Wszystkie test są znowu na zielono.

Wszystkie testy na zielono

To by było na tyle, jeżeli chodzi o ten wpis. W następnym wpisie omówimy bardziej skomplikowane warunki oraz skorzystamy z wstrzykiwania zależności, oraz kontenerów. W końcu chcemy spełniać zasadę projektową pojedynczej odpowiedzialności.