Testowanie w C# i .NET Core

17.04.2020

Artykuł

O jak wielu rodzajach testów słyszałeś? Testy jednostkowe, integracyjne, end-to-end, smoke testy, testy manualne – taką listę można ciągnąć jeszcze długo. W tym artykule skoncentrujemy się tylko na ich małym wycinku, jakim jest testowanie w C# i .NET Core. Omówimy bardzo popularne mechanizmy jak testy jednostkowe i BDD. Oczywiście, to nie wszystko! Zajmiemy się również bardzo ciekawym podejściem, niestety niezbyt rozpowszechnionym, czyli testami mutacyjnymi.

Używane biblioteki

Oczywiście, będziemy pisać kod! W tym celu skorzystamy z C# i .Net Core’a w wersji 3.1 Dlaczego .Net Core? Ponieważ w tej chwili to jeden z najdynamiczniej rozwijających się frameworków. Na dodatek, zgodnie z raportem StackOverflow, jest również bardzo lubiany wśród developerów. Do testów jednostkowych użyjemy xUnit i Fluent Assertions, a przy BDD dodatkowo wspomoże nas ChillBDD. Testy mutacyjne poznamy z wykorzystaniem Stryker .Net.

Reguły biznesowe

Żadne testy nie mają sensu, jeżeli nie weryfikują jakiegoś systemu. Naszym kodem produkcyjnym będzie maszyna z przekąskami, czyli klasyczna TDD Kata.

Jak działa nasz automat? Dzisiaj zajmiemy się tylko częścią jego funkcji. Będziemy weryfikować dwie rzeczy:

  • wrzucamy do niego pieniądze i możemy poprosić o ich zwrot
  • kupujemy produkt i otrzymujemy resztę

Rozszerzoną wersję, wraz ze 100% pokryciem testami, znajdziesz na githubie.

Testy, testy i jeszcze więcej testów

Testy jednostkowe

Znając wymagania, możemy zająć się testami. Zaczniemy od najmniej skomplikowanego podejścia, czyli testów jednostkowych.
W tej części będziemy weryfikować zachowanie pojedynczych metod klasy VendingMachine.

Naszym pierwszym testem będzie sprawdzenie, czy możemy otrzymać z powrotem wrzuconą kwotę:

[Theory]
[InlineData("1, 1, 1", "1, 1, 1")]
[InlineData("0.25, 0.25, 0.25, 0.25", "0.25, 0.25, 0.25, 0.25")]
public void ReturnsAllInsertedCoins(string insertedCoins, string expected)
{
    var vendingMachine = new VendingMachine();
    vendingMachine.InsertMoney(insertedCoins);

    var returnedCoins = vendingMachine.Return();

    returnedCoins.Should().Be(expected);
}

Na co warto zwrócić uwagę w tym fragmencie kodu?
Na pewno na atrybut [InlineData], który pozwala przekazać nam parametry do testu. Korzystając z niego, definiujemy wartość monet wrzuconych do maszyny (parametr insertedCoins) oraz oczekiwany rezultat (parametr expected).
Następnie tworzymy instancję klasy, którą testujemy i wywołujemy na niej odpowiednie metody.
Na koniec, korzystając z FluentAssertions, weryfikujemy rezultat.
W tym stylu będą utrzymane pozostałe testy jednostkowe, które możesz zobaczyć TUTAJ.

Testy BDD

Przyszła pora na testy BDD, czyli Behavior Driven Development. Czym różnią się one od testów jednostkowych? Przede wszystkim, zmieniamy podejście. W tym przypadku, spróbujemy od razu zdefiniować końcowy scenariusz. Takie scenariusze będą „mówić w języku biznesu”. To znaczy, że zamiast weryfikować konkretne metody, spróbujemy zapisać nasz test w języku zbliżonym do naturalnego. Jak wygląda to w “żywym” kodzie?
Na przykład, w ten sposób:

namespace BDDTests
{
    namespace ForBuyingProductsWithChange
    {
        public class WhenInsertedMoreMoneyThanPriceOfProduct 
               : GivenSubject<VendingMachine, string>
        {
            public WhenInsertedMoreMoneyThanPriceOfProduct()
            {
                Given(() => Subject.InsertMoney("1, 0.50"));

                When(() => Subject.GetCola());
            }

            [Fact]
            public void ThenGetColaWithChange()
            {
                Result.Should().ContainAll("Cola", "0.50");
            }
        }
    }
}

Jak widać, dzieje się tutaj dużo więcej, niż we wcześniejszym przykładzie. Sprawę komplikuje również to, że korzystamy z biblioteki ChillBDD.
Zacznijmy od dwóch namespace’ów, które pozwalają nam dokładniej opisać, co dzieje się w samym teście.
Zwróć uwagę na dziedziczenie po generycznej klasie GivenSubject<vendingmachine, string=””></vendingmachine,>. Dzięki temu korzystamy z właściwości Subject, która jest instancją klasy VendingMachine oraz z właściwości Result, która jest typu string i oczywiście, zawiera rezultat. Ok, wszystko pięknie, ale rezultat czego? I tutaj znowu musimy odwołać się do Chill. Zapewne zauważyłaś lub zauważyłeś dwie lambdy w konstruktorze – Given oraz When.
To właśnie w tym miejscu przygotowujemy dane i wywołujemy metodę, której rezultat weryfikujemy. When(() => Subject.GetCola()); przypisuje do właściwości Result wynik metody GetCola().
Dzięki temu, możemy opisać nasz test, korzystając ze schematu Given – When – Then, gdzie Then jest po prostu weryfikacją wyniku.

Szukasz szkolenia dla siebie lub swojego teamu?
Sprawdź nasze zdalne szkolenia dla firm z trenerem NA ŻYWO

Testy mutacyjne

Na koniec zupełnie zmienimy nasz punkt widzenia. Co ciekawe, tym razem nie napiszemy ani linijki kodu! Skorzystamy natomiast z testów mutacyjnych i biblioteki Stryker .Net.

Czym charakteryzuje się ten rodzaj testów? Przede wszystkim, w ten sposób będziemy weryfikować jakość do tej pory napisanych testów.

Cały proces składa się z trzech części:

1. Stryker .Net wprowadzi do naszego kodu mutanty. Czym są mutanty? To nic innego jak zmiany w naszym kodzie. Mogą to być zarówno zmiany operatora z > na <, zanegowanie wartości typu bool, jak i zamiana stringa. Po listę wszystkich dostępnych mutacji zapraszam do dokumentacji.

2. W następnym kroku, dla każdego wprowadzonego mutanta, Stryker uruchomi nasze testy.

3. Na koniec procesu otrzymamy raport w formacie html, który powie nam, które mutanty przeżyły, a które zostały wychwycone przez nasze testy. Mutant, który przeżył to taka zmiana, która nie zepsuła żadnego z naszych testów.

Jakie wnioski możemy wyciągnąć z tej procedury? Oczywiście, szukamy mutantów, które przeżyły. Jeżeli zmiana w kodzie nie zepsuła chociaż jednego testu, to powinna zapalić się nam czerwona lampka. Skoro możemy dowolnie zmieniać kod, a nasze testy nie są w stanie tego wychwycić, to jakość naszych testów nie jest najwyższa.

Sprawdźmy, jak zachowają się nasze testy w starciu z mutantami.

W odróżnieniu od poprzednich bibliotek, które możemy zainstalować przez Nuget, Stryker .Net instalujemy ręcznie.
Jeżeli masz zainstalowane .Net Core SDK w wersji wyższej niż 2.1 to z linii poleceń wystarczy wywołać instrukcję:

dotnet tool install -g dotnet-stryker

Następnie, z katalogu z testami, uruchamiamy bibliotekę:

dotnet stryker --reporters "['html', 'progress']"

Po około minucie powinniśmy zobaczyć rezultat oraz ścieżkę do pliku z raportem.
Jego fragment wygląda w ten sposób:

screen

Jest to część metody GetProductWithChange, z zaznaczonymi mutantami. Zwróć uwagę na numer 20 i zmianę, którą wprowadził Stryker. Po wprowadzeniu mutanta, który zwiększał ilość produktów po zakupie, nasze testy dalej przechodziły. Taka zmiana bardzo szybko doprowadzi do problemów ponieważ będziemy próbowali sprzedać produkt, którego nie mamy w maszynie.

Jak powinien wyglądać test, który wychwyci taką zmianę? Oczywiście, powinien on sprawdzać czy ilość produktów została zmniejszona po jego zakupie. Spróbuj naprawić ten problem w ramach treningu.

Kiedy stosować dany typ testów

Wiedząc już, jak wygląda nasz mały wycinek świata testów, zastanówmy się, kiedy stosować konkretne rodzaje testów.

Testy jednostkowe zazwyczaj przynoszą najwięcej korzyści, kiedy weryfikujemy jakiś mały, niezależny wycinek systemu. Czym jest mały i niezależny wycinek? To pytanie jest tematem niekończących się sporów i internetowych wojen. Najprostszym założeniem jest testowanie pojedynczych klas i ich metod. I przy tej definicji zostaniemy.

Takie testy jest stosunkowo łatwo napisać, a uruchamiają się bardzo szybko. W związku z tym możemy z nich korzystać bardzo często i na bieżąco sprawdzać, czy niczego nie zepsuliśmy.

BDD sprawdza się na przykład przy tworzeniu kryteriów akceptacyjnych. Konkretne wymagania możemy zapisać w formie kodu i zadanie uznać za skończone, kiedy nasz test przechodzi.

Warto pamiętać o tym, że takie testy będą częścią kodu źródłowego naszej aplikacji. W związku z tym przedyskutuj wcześniej z zespołem, czy planujecie takie testy tworzyć oraz jakich bibliotek do tego użyjecie.

Testy mutacyjne przydatne są w każdym momencie. Z racji tego, że bibliotekę do takich testów możemy zainstalować lokalnie i nie musimy w tej sprawie konsultować się ze swoim zespołem. Jedynym wymogiem jest po prostu to, że piszecie testy. Jeżeli nie macie żadnych testów w projekcie (co, niestety się zdarza), to taka metoda weryfikacji nie będzie miała żadnego sensu.

Podsumowanie

Jak widać, świat testów jest niesamowicie bogaty, a zacząć jest bardzo łatwo. Wystarczy podstawowa znajomość języka programowania i kompatybilnej z nim biblioteki. Zazwyczaj, w komercyjnych projektach, będzie już do tego dostępna infrastruktura więc nie trzeba się będzie troszczyć o wybór bibliotek oraz ich konfigurację.

Jeżeli szukasz czegoś bardziej skomplikowanego, możesz zapoznać się tematem Property-Based testing dla tej samej maszyny z przekąskami.

Autor:
Jakub Ciechowski

Jakub Ciechowski - autor wpisu

Programista C# w Aspire Systems Poland. Miłośnik DDD i ścisłej współpracy z biznesem. Wielki fan kawy, który nie wyobraża sobie dnia, bez dzbanka naparu z drippera.

Przeczytaj także:
Klient HTTP i obsługa API