← Wszystkie wpisy

Testy jednostkowe C#: pierwszy test i czerwony wynik

Pierwszy test w xUnit nie sprawdza, czy kod działa, tylko zmusza do opisania, co to znaczy. Prawdziwe wyjście dotnet test, zielone i czerwone.

Artur Kot 8 min czytania

Testy jednostkowe C# sprowadzają się do jednego polecenia, dotnet test, i jednej decyzji o tym, co znaczy, że kod działa. Na końcu będziesz miał projekt z dwoma testami xUnit i będziesz wiedział, co zrobić, kiedy wynik jest czerwony. Pierwszy z nich najpierw zawiedzie, i o tym jest ten wpis.

Krok 0: szablon jest już zielony

Zanim napiszesz cokolwiek, załóż rozwiązanie z biblioteką i projektem testowym:

dotnet new sln -n Oceny
dotnet new classlib -n Grades
dotnet new xunit -n Tests
dotnet sln add Grades Tests
dotnet add Tests reference Grades
dotnet test Tests

Szablon xUnit zawiera jeden test o nazwie Test1, w którym nie ma ani jednej linii. Mimo to dotnet test kończy się tak:

Passed!  - Failed:     0, Passed:     1, Skipped:     0, Total:     1, Duration: 23 ms - Tests.dll (net10.0)

Zielony wynik oznacza tylko tyle, że w teście nic nie rzuciło wyjątku. Metoda bez asercji nie może zawieść, więc nic nie mówi o twoim kodzie. Pierwsze uruchomienie, razem z kompilacją obu projektów, trwało u mnie 10,8 sekundy.

Pierwszy test: najpierw zdecyduj, co znaczy „działa”

Biblioteka ma jedną metodę, która liczy średnią ocen:

namespace Grades;

public static class Average
{
    public static double Of(int[] grades)
    {
        return grades.Sum() / grades.Length;
    }
}

Żeby ją przetestować, musisz najpierw odpowiedzieć na pytanie, którego kod nie zadaje: ile ma wynosić średnia z ocen 3 i 4? Trzy i pół, trzy, a może cztery po zaokrągleniu? Zapisujesz tę odpowiedź w teście, zanim sprawdzisz, czy kod ją znał:

using Grades;

namespace Tests;

public class AverageTests
{
    [Fact]
    public void Of_ThreeAndFour_ReturnsThreePointFive()
    {
        var result = Average.Of(new[] { 3, 4 });

        Assert.Equal(3.5, result);
    }
}

Nazwa metody mówi, co jest wejściem i czego oczekujesz, a ciało ma trzy części: przygotowanie danych, wywołanie, asercja. Jedna asercja na test to nie dogmat, tylko wygoda. Kiedy zawiedzie, wiesz, której obietnicy kod nie dotrzymał.

Nazwa składa się z trzech części rozdzielonych podkreśleniem: metoda, wejście, oczekiwany wynik. To konwencja, a nie wymóg kompilatora. Dzięki niej po samej nazwie wiesz, czego dotyczy test, a lista czerwonych testów po zmianie kodu czyta się jak spis niedotrzymanych obietnic.

Jak czytać czerwony wynik

Ten test nie przejdzie. Metoda dzieli dwie liczby całkowite, więc wynik jest obcinany do liczby całkowitej, zanim zamieni się w double. Z polskim interfejsem dotnet dostajesz to:

[xUnit.net 00:00:00.27]     Tests.AverageTests.Of_ThreeAndFour_ReturnsThreePointFive [FAIL]
  Niepowodzenie Tests.AverageTests.Of_ThreeAndFour_ReturnsThreePointFive [36 ms]
  Komunikat o błędzie:
   Assert.Equal() Failure: Values differ
Expected: 3.5
Actual:   3

Ślad stosu pominąłem, bo pod pierwszą linią, tą z UnitTest1.cs, są już tylko ramki refleksji. Zwróć uwagę, które części są po polsku. Tłumaczone są komunikaty dotnet, a komunikat asercji pochodzi z xUnit i zostaje po angielsku. Czyta się go prosto: Expected to wartość, którą zapisałeś w teście, a Actual to to, co zwrócił kod. Czerwony wynik mówi więc, że kod zwraca 3, a ty chciałeś 3.5, a nazwa testu wskazuje metodę, w której trzeba szukać.

Łatwo jednak zamienić argumenty miejscami. Pierwszy argument Assert.Equal to wartość oczekiwana, a drugi to wynik. Po zapisie Assert.Equal(result, 3.5) ten sam błąd wygląda tak:

Expected: 3
Actual:   3.5

Etykiety są odwrócone, więc komunikat twierdzi, że kod miał zwrócić 3, a zwrócił 3.5, czyli na odwrót. Z takim wynikiem poprawisz kod w złą stronę. Dlatego przy czerwonym teście najpierw sprawdź, czy sam test jest napisany poprawnie.

Poprawka kodu to rzutowanie przed dzieleniem:

return (double)grades.Sum() / grades.Length;

Po niej dotnet test kończy się wynikiem Passed! z jednym testem. Taki wynik ma teraz wartość, bo wiesz, że test potrafi zawieść. Test, którego nigdy nie widziałeś na czerwono, mógł być zielony od początku z niewłaściwego powodu.

Drugi test: pusta lista ocen

Pierwszy test zadał pytanie o zaokrąglanie. Kolejne nasuwa się samo: ile wynosi średnia z zera ocen? Zapiszmy najprostszą odpowiedź, zero:

[Fact]
public void Of_NoGrades_ReturnsZero()
{
    var result = Average.Of(Array.Empty<int>());

    Assert.Equal(0, result);
}

Wynik, tym razem w angielskiej wersji dotnet, razem z podsumowaniem pod spodem:

Failed Tests.AverageTests.Of_NoGrades_ReturnsZero [2 ms]
  Error Message:
   Assert.Equal() Failure: Values differ
Expected: 0
Actual:   NaN

Failed!  - Failed:     1, Passed:     1, Skipped:     0, Total:     2, Duration: 28 ms - Tests.dll (net10.0)

Pierwszy test nadal przechodzi, drugi nie, a podsumowanie liczy oba, więc czerwony wynik jednego nie unieważnia reszty.

Po poprawce z rzutowaniem dzielenie zera przez zero daje NaN, czyli wartość, która nie jest liczbą, i metoda zwraca ją bez żadnego ostrzeżenia. Dalej w programie takie NaN przechodzi przez kolejne obliczenia i psuje każdą liczbę, z którą się zetknie. Tym razem zła była odpowiedź, a nie kod. Średnia z pustej listy nie istnieje, więc lepiej rzucić wyjątek, niż zwracać cokolwiek:

if (grades.Length == 0)
{
    throw new ArgumentException(
        "Średnia z pustej listy ocen nie istnieje.", nameof(grades));
}

Kod testu zmienia się razem z decyzją:

[Fact]
public void Of_NoGrades_Throws()
{
    Assert.Throws<ArgumentException>(() => Average.Of(Array.Empty<int>()));
}

Teraz dotnet test pokazuje dwa zielone testy. Oba zapisują decyzje, które wcześniej istniały tylko w twojej głowie.

Dwa błędy z pierwszego projektu

Najpierw odwołanie do biblioteki. Jeśli pominiesz dotnet add Tests reference Grades, projekt testowy nie zobaczy klasy Average i kompilacja kończy się, zanim cokolwiek zostanie uruchomione:

UnitTest1.cs(1,7): error CS0246: Nie można znaleźć nazwy typu lub przestrzeni nazw „Grades” (brak dyrektywy using lub odwołania do zestawu?)

Skróciłem ścieżkę przed nazwą pliku. To nie jest czerwony test, tylko błąd kompilacji, i komunikat jest po polsku, bo tłumaczy go kompilator, a nie xUnit. Dotyczy struktury projektu, nie twojego kodu.

Drugi jest groźniejszy, bo wynik pozostaje zielony. Usuń atrybut [Fact] sprzed pierwszej metody testowej i dotnet test nie zgłosi porażki. Zgłosi tylko ostrzeżenie kompilatora i podsumowanie z mniejszą liczbą testów:

UnitTest1.cs(7,17): warning xUnit1013: Public method 'Of_ThreeAndFour_ReturnsThreePointFive' on test class 'AverageTests' should be marked as a Fact. ...
Passed!  - Failed:     0, Passed:     1, Skipped:     0, Total:     1, Duration: 15 ms - Tests.dll (net10.0)

Metoda bez atrybutu jest zwykłą metodą, którą runner pomija. Przy dwóch napisanych testach zielone Total: 1 to jedyny ślad. Patrz więc na liczbę testów, a nie tylko na kolor.

Żeby sprawdzić, które testy runner w ogóle widzi, uruchom dotnet test Tests --list-tests:

The following Tests are available:
    Tests.AverageTests.Of_ThreeAndFour_ReturnsThreePointFive
    Tests.AverageTests.Of_NoGrades_Throws

Test, którego na tej liście brakuje, nie został uruchomiony, bez względu na to, jak wygląda jego kod. Gdy projekt urośnie, możesz uruchomić jeden test przez filtr: dotnet test Tests --filter "FullyQualifiedName~NoGrades". Przy rozszerzonym wyjściu (--logger "console;verbosity=normal") dotnet test wypisze jedną linię Passed z nazwą testu zamiast całego zestawu.

Co testować najpierw, a czego wcale

W testach jednostkowych C# zacznij od przypadku, w którym znasz odpowiedź bez liczenia, a potem przejdź do granic: pustej listy, jednego elementu, zera. Błędy kryją się przy krawędziach częściej niż w środku zakresu. Dla pojedynczej metody dwa lub trzy takie testy wystarczą na pierwszy dzień.

Są trzy rzeczy, których nie warto testować na początku. Pierwsza to kod biblioteki standardowej. Asercja sprawdzająca, że Sum() dodaje liczby, nie mówi nic o twoim programie. Druga to oczekiwana wartość policzona tym samym wzorem co kod. Gdybyś w teście napisał dzielenie dwóch liczb całkowitych tak samo jak w metodzie, zielony wynik potwierdziłby błąd zamiast go wykryć. Oczekiwaną wartość wpisuj ręcznie, z kartki. Trzecia to trywialne właściwości bez logiki, które nie mają się jak zepsuć.

Najlepszy moment na kolejny test przychodzi wtedy, gdy znajdziesz błąd w działającym kodzie. Zanim go poprawisz, napisz test, który go odtwarza, i zobacz, jak jest czerwony. Poprawka zamienia go w zielony. Testy jednostkowe C# działają wtedy jak zapis tego, czego program już raz nie potrafił, a ten test pilnuje, żeby błąd nie wrócił przy następnej zmianie.

Kiedy czerwony test pokazuje, że kod zwraca coś innego, niż chcesz, ale nie widać dlaczego, przydaje się debugger zamiast kolejnych printów. Jeśli z kolei wynik to wyjątek, a nie różnica wartości, NullReferenceException wskazuje inną linię, niż podejrzewasz.

Program kursu zawiera rozdział „Testy jednostkowe z xUnit”, a wbudowane środowisko uruchamia dotnet test bez wychodzenia z kursu.

Tematy: TestowanieC#Narzędzia

Pobieranie ruszyło

DevJourney_2.1.1_x64-setup.exe · 2.1.1

Jeśli system ostrzeże i jak sprawdzić plik

Jeśli Windows ostrzeże

Komunikat „System Windows ochronił Twój komputer” nie oznacza wykrycia wirusa. SmartScreen ufa certyfikatowi, którym podpisany jest instalator, a ten jeszcze zbiera reputację poprzez licznik pobrań.

W oknie ostrzeżenia: Więcej informacji → Uruchom mimo to.

Sprawdź to sam

Zobacz wynik skanowania w VirusTotal ↗

Albo w PowerShellu, w katalogu z pobranym plikiem:

Get-FileHash .\DevJourney_2.1.1_x64-setup.exe

Powinno wypisać:

2e441de05e02ae32caad96bdca86f9ca0ff59e01cbc8b289a1f6f783e4f7bc46