Ćwiczenia czynią mistrza, ale jak zacząć? Wczuj się w sytuacje, mamy końcówkę Czerwca, masz wiele lat komercyjnego doświadczenia, szef mianował Cię na mentorkę /mentora grupy praktykantów/stażystów. Przychodzisz do Pracy w pierwszy dzień roboczy Lipca, na końcu korytarza widzisz nowe twarze, czeka już na Ciebie grupa ambitnych praktykantów/praktykantek kierunku Informatyka, lub pokrewnego. Po wstępnej rozmowie z grupą studentów/studentek 2-3 roku studiów dowiadujesz się od nich, że o testach jednostkowych coś tam słyszeli od prowadzącego w ostatniej minucie wykładu lub od kolegów/koleżanek na korytarzu, ale nigdy na oczy nie widzieli w praktyce. Firmy coraz częściej prowadzą zajęcia na uczelniach, ale nadal dochodzi do sytuacji, że kwestia jakości oprogramowania spychana jest na margines. Najważniejsze na studiach jest by program działał tak jak oczekuje prowadzący i było zaliczenie w systemie/indeksie. Skoro program wyląduje w koszu, jaki sens ma nauka dobrych praktyk i pisanie testów jednostkowych. Jedni tutaj przytakną, a drudzy powiedzą nie masz racji. Pisząc oprogramowanie na zaliczenie i dbając o jakość wytworzonego kodu, możesz później ten projekt wrzucić na hosting GitHub, Bitbucket, lub GitLab. Jeszcze lepiej jak od samego początku będziesz wysyłał efekty swej pracy na zewnętrzne repozytorium, praktyka czyni mistrza. Publikując swoje projekty zyskasz przewagę nad konkurencją na rynku pracy, będziesz mogła/mógł opisać swoje projekty i pochwalić się linkiem np. do GitHuba w swoim CV. Zawsze to lepsze niż puste pole w rubryce „Doświadczenie” w CV.  Idąc dalej możecie zautomatyzować swoją pracę stosując Continuous Integration, zainteresowane osoby zachęcam do poczytania moich wcześniejszych wpisów z powyższej tematyki:

Po zakończeniu rozmowy decydujesz się, ze nauczysz Ich implementacji testów jednostkowych od postaw. Zastanawiasz się jak do tego podejść, podesłanie linku do dokumentacji i zadanie by pisali testy do klasy z kodu produkcyjnego nie oszukujmy się nie jest na starcie najbardziej optymalnym podejściem. Druga myśl prezentacja wprowadzająca na rzutniku z powyższej tematyki, a później z każdym praktykantem przećwiczenie wiedzy programując w parze. W tym przypadku trochę ponudzi się praktykant będący na końcu kolejki do programowania w parze. Trzecia opcja, którą chcę przestawić to podejście do nauki w stylu Coding Dojo.

Coding Dojo

Ćwiczenia, ćwiczenia, ćwiczenia …. tym jest Coding Dojo. Wyróżniamy kilka sztuk kodowania:

  • Kata – w tym podejściu  nie chodzi o wymyślenie rozwiązania, ale o naukę skrótów klawiaturowych, narzędzi i metodologii rozwiązania problemu. Jedna osoba wykonuje ćwiczenie, a pozostałe osoby po niej powtarzają implementacje.
  • Wasa – czyli dwuosobowe kata, w praktyce jedna osoba przez implementacje rozwiązuje prosty problem, a druga po niej powtarza. W innym przypadku pierwsza osoba pisze test jednostkowy wymuszając implementacje, a druga osoba implementuje rozwiązanie w celu zaliczenia testu. Po zakończeniu ćwiczenia można zamienić się rolami.
  • Randori – w ćwiczeniu bierze udział dowolna ilość osób. Pierwsza osoba pisze test, kolejna osoba podchodzi do klawiatury implementuje rozwiązanie i pisze kolejny test. I tak po kolei każdy w kółku, aż do rozwiązania wszystkich wymagań w ćwiczeniu. Do realizacji ćwiczenia przyda się rzutnik, efekt pracy w IDE powinien być widoczny dla każdego.

Tak wygląda teoria, a w praktyce warto pokombinować dodając/zmieniając wymagania w trakcie implementacji, wprowadzając ograniczenia, co do narzędzi lub wykorzystywanej składni.

FizzBuzz – Ćwicz Jasiu

Znasz już sztuki Coding Dojo, ale którą zastosować do nauki testów jednostkowych. Każda będzie dobra, osobiście proponuję Randori z Mentorem. Proces wymiany wiedzy i doskonalenia umiejętności zaczniemy od wyboru kata, dziś będzie to FizzBuzz. W ramach ćwiczenia zespół posiądzie wiedze i umiejętności na temat:

  • implementacji testów jednostkowych w języku C# z wykorzystaniem frameworka NUnit
  • metodyki TDD
  • wzorca AAA
  • konwencji nazewnictwa testów
  • obsługi VS
  • NuGet
  • Git

Czas zdradzić naszym praktykantom treść ćwiczenia FizzBuzz. Wyobraźmy, że w pokoju po okręgu siedzi grupa przyjaciół. W ramach zabawy mówią liczby od 1 do 100. Pierwsza osoba mówi jeden, druga dwa i tak dalej zgodnie z ruchem zegara. Jeśli liczba jest wielokrotnością 3 uczestnik mówi „Fizz”. Jeśli  liczba jest wielokrotnością 5 uczestnik zabawy mówi „Buzz”. W przypadku, gdy liczba jest podzielna przez 3 i 5 osoba mówi „FizzBuzz”.

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
...

Zaczynamy kodowanie, do klawiatury podchodzi tytułowy Jasiu. Jego zadaniem jest przygotowania środowiska pracy i napisanie pierwszego testu. W ramach zadania wykonuje następujące czynności:

  • inicjalizuje repozytorium Git,
  • tworzy solucje w VS  i dodaje dwa projekty (Class Library o nazwie FizzBuzzKata i Unit Test Project o nazwie FizzBuzzKata.Tests)
  • w projekcie z testami usuwa zależność do MSTest i z NuGet dodaje NUnit
  • po dodaniu projektów i zależności wypycha zmiany na repozytorium

Zadaniem Jasia jest napisanie pierwszego testu jednostkowego, który sprawdzi czy dla liczby 1 zostanie zwrócona wartość 1 w formie stringa. Jako mentor/mentorka instruujesz jak pisać testy jednostkowe zgodnie z konwencją wzorca AAA. W fazie Arrange definiowane są dane wejściowe. W sekcji Act wywoływana jest akcja (metoda, komenda), którą chcemy przetestować. W bloku Assert weryfikowana jest zgodność założeń z rzeczywistymi wynikami. Stosowanie powyższych konwencji powoduje, że testy są łatwiejsze w czytaniu. W ramach utrwalenia w kodzie każdy etap jest komentowany. Nie mamy na ten moment żadnej implementacji, tym samym Jasiu swoim testem jednostkowym wymusi implementacje metody przez następną koleżankę/kolegę.

using NUnit.Framework;

namespace FizzBuzzKata.Tests
{
    [TestFixture]
    public class UTestFizzBuzz
    {
        [Test]
        public void GetFizzBuzzResult_WhenOne_ShouldReturnOne()
        {
            //Arrange
            var fizzBuzz = new FizzBuzz();
            string expected = "1";
            //Act
            string actual = fizzBuzz.GetFizzBuzzResult(1);
            //Assert
            Assert.AreEqual(expected,actual);
        }
    }
}

Test został napisany, ale nie kompiluje się, ponieważ nie została zaimplementowana klasa FizzBuzz. Zgodnie z metodyką TDD, w pierwszym kroku programista pisze nieprzechodzący test. W drugim kroku dopisuje minimum implementacji potrzebnej do zaliczenia testu. W ostatnim kroku następuje refaktoryzacja kodu logiki biznesowej i testu. Cykl TDD zakończony, następnie pisany jest kolejny test i ponawiamy cały proces. Jasiu odchodzi od klawiatury i zmienia go koleżanka Ania. Ania dopisuje implementacje FizzBuzz w celu zaliczenia testu.

namespace FizzBuzzKata
{
    public class FizzBuzz
    {
        public string GetFizzBuzzResult(int number)
        {
            return number.ToString();
        }
    }
}

Test przechodzi i Ania wykonuje commit na repozytorium. Praca jej nie skończyła się, teraz musi napisać kolejny test weryfikujący czy dla liczby będącej wielokrotnością 3 zostanie zwrócona wartość Fizz.

[Test]
public void GetFizzBuzzResult_WhenThree_ShouldReturnFizz()
{
    //Arrange
    var fizzBuzz = new FizzBuzz();
    string expected = "Fizz";

    //Act
    string actual = fizzBuzz.GetFizzBuzzResult(3);

    //Assert
    Assert.AreEqual(expected, actual);
}

Ania odchodzi i siada do klawiatury kolejny praktykant. Implementowane są kolejne wymagania zgodnie z metodyką TDD i wzorcem AAA. Możliwy efekt końcowy Ich pracy poniżej.

namespace FizzBuzzKata
{
    public interface IFizzBuzz
    {
        string GetFizzBuzzResult(int number);
    }
}
namespace FizzBuzzKata
{
    public class FizzBuzz : IFizzBuzz
    {
        public string GetFizzBuzzResult(int number)
        {
            if (IsFizzBuzz(number)) return NameConstants.FizzBuzzName;
            if (IsFizz(number)) return NameConstants.FizzName;
            if (IsBuzz(number)) return NameConstants.BuzzName;

            return number.ToString();
        }

        private static bool IsBuzz(int number) => number % 5 == 0;

        private static bool IsFizz(int number) => number % 3 == 0;

        private static bool IsFizzBuzz(int number) => (number % 3 == 0) && (number % 5 == 0);
    }
}
namespace FizzBuzzKata
{
    public static class NameConstants
    {
        public static string FizzBuzzName = "FizzBuzz";
        public static string FizzName = "Fizz";
        public static string BuzzName = "Buzz";
    }
}
using NUnit.Framework;

namespace FizzBuzzKata.Tests
{
    [TestFixture]
    public class UTestFizzBuzz
    {
        private IFizzBuzz GetSut() => new FizzBuzz();
       
        [TestCase(1, "1")]
        [TestCase(2, "2")]
        [TestCase(4, "4")]
        [TestCase(16, "16")]
        public void GetFizzBuzzResult_WhenNumberIsNotMultipleOf3or5_ShouldReturnIntigerValue(int number, string expected)
        {
            //Arrange
            IFizzBuzz sut = GetSut();

            //Act
            string actual = sut.GetFizzBuzzResult(number);

            //Assert
            Assert.AreEqual(expected,actual);
        }

        [Test]
        public void GetFizzBuzzResult_WhenThree_ShouldReturnFizz()
        {
            //Arrange
            IFizzBuzz sut = GetSut();
            string expected = NameConstants.FizzName;

            //Act
            string actual = sut.GetFizzBuzzResult(3);

            //Assert
            Assert.AreEqual(expected, actual);
        }

        [Test]
        public void GetFizzBuzzResult_WhenFive_ShouldReturnBuzz()
        {
            //Arrange
            IFizzBuzz sut = GetSut();
            string expected = NameConstants.BuzzName;

            //Act
            string actual = sut.GetFizzBuzzResult(5);

            //Assert
            Assert.AreEqual(expected, actual);
        }

        [Test]
        public void GetFizzBuzzResult_WhenFifteen_ShouldReturnFizzBuzz()
        {
            //Arrange
            IFizzBuzz sut = GetSut();
            string expected = NameConstants.FizzBuzzName;

            //Act
            string actual = sut.GetFizzBuzzResult(15);

            //Assert
            Assert.AreEqual(expected, actual);
        }
    }
}

FizzBuzzKata TestsFizzBuzz – Nowe wymagania

Jak to w życiu wymagania biznesowe zmieniają, w naszym ćwiczeniu też to nastąpiło. Doszły nam dwa poniższe wymagania:

  • Liczba jest Fizz, jeśli jest wielokrotnością 3 lub jeśli w nazwie ma 3
  • Liczba jest Buzz, jeśli jest wielokrotnością 5 lub jeśli w nazwie ma 5

Kolejne testy, implementacje, poprawki i gotowe. Przyjrzyjmy się jak teraz wygląda kod. Interfejs IFizzBuzz i klasa NameConstants nie uległy zmianie.

namespace FizzBuzzKata
{
    public class FizzBuzz : IFizzBuzz
    {
        public string GetFizzBuzzResult(int number)
        {
            if (IsFizzBuzz(number)) return NameConstants.FizzBuzzName;
            if (IsFizz(number)) return NameConstants.FizzName;
            if (IsBuzz(number)) return NameConstants.BuzzName;

            return number.ToString();
        }

        private static bool IsBuzz(int number) => number % 5 == 0 || number.ToString().Contains("5");

        private static bool IsFizz(int number) => number % 3 == 0 || number.ToString().Contains("3");

        private static bool IsFizzBuzz(int number) => (number % 3 == 0) && (number % 5 == 0);
    }
}

Praktykanci dopisują dwa testy, weryfikujące nowe wymagania dla FizzBuzz.

[TestCase(13)]
[TestCase(31)]
[TestCase(37)]
public void GetFizzBuzzResult_WhenNumberContains3_ShouldReturnFizz(int number)
{
    //Arrange
    IFizzBuzz sut = GetSut();
    string expected = NameConstants.FizzName;

    //Act
    string actual = sut.GetFizzBuzzResult(number);

    //Assert
    Assert.AreEqual(expected, actual);
}

[TestCase(50)]
[TestCase(52)]
[TestCase(58)]
public void GetFizzBuzzResult_WhenNumberContains5_ShouldReturnBuzz(int number)
{
    //Arrange
    IFizzBuzz sut = GetSut();
    string expected = NameConstants.BuzzName;

    //Act
    string actual = sut.GetFizzBuzzResult(number);

    //Assert
    Assert.AreEqual(expected, actual);
}

Jeśli nie wierzycie testom jednostkowym, to możecie napisać prosty programik, wyrzucający wynik dla każdej liczby od 1 do 100 na console.

Podsumowanie

Dzięki powyższemu ćwiczeniu FizzBuzz grupa zaznajomiła się z tematyką testów jednostkowych i przećwiczyła je w codziennym środowisku pracy (IDE, git). Teraz gotowi są na nowe wyzwania, które czekają ich w trakcie praktyk. Co myślisz o takim podejściu do nauki testów jednostkowych w trakcie praktyk? A może znasz lepszą metodykę? Czekam na opinie i komentarze pod wpisem 🙂 Powyższe ćwiczenie można zaimplementować na wiele sposób, a także w innych językach programowania nie tylko w C#, zachęcam do ćwiczeń. Osoby chcące pogłębić temat testów jednostkowych zapraszam także do lektury dwóch wpisów: