xUnit Theory w praktyce: Praca z InlineData, MemberData i ClassData

xUnit Theory w praktyce: Praca z InlineData, MemberData i ClassData
19 października 2024 Brak komentarzy Testowanie Tajko

xUnit to jeden z najpopularniejszych frameworków do testowania jednostkowego w .NET. Oferuje on różne możliwości definiowania danych testowych, co jest kluczowe przy pracy z tzw. „parametryzowanymi testami” (theory). Jednym z najczęściej wykorzystywanych podejść jest użycie atrybutów takich jak InlineData, MemberData i ClassData, które pozwalają na przekazywanie danych do testów w sposób elastyczny i wygodny. W tym wpisie omówię każde z tych podejść i pokażę ich zastosowania w praktyce.

Czym jest Theory w xUnit?

W frameworku testowym xUnit, który jest popularnym narzędziem do testów jednostkowych w .NET, istnieją dwa kluczowe atrybuty do oznaczania metod testowych: Fact i Theory.

  • Fact reprezentuje test, który zawsze powinien się wykonywać z tą samą logiką i tymi samymi danymi. Jest to prosty test jednostkowy, który nie wymaga żadnych parametrów. Gdy masz pojedynczy przypadek testowy, używasz właśnie Fact.
  • Theory natomiast służy do bardziej elastycznego testowania, gdzie ta sama logika testu jest sprawdzana przy użyciu różnych zestawów danych. Theory pozwala na parametryzację testów za pomocą atrybutu InlineData lub innych źródeł danych, co umożliwia przeprowadzenie tego samego testu na różnych danych wejściowych. Jest to szczególnie przydatne, gdy chcemy sprawdzić różne scenariusze dla tej samej funkcjonalności w jednej metodzie testowej.

Przykładem użycia Theory może być testowanie metody, która dodaje elementy do listy. Chcemy przetestować tę samą metodę dla różnych zestawów danych (np. pustej listy, listy z kilkoma elementami), a Theory pozwala nam na przetestowanie tych przypadków bez konieczności pisania osobnych testów dla każdego scenariusza.

InlineData

InlineData to najprostszy sposób przekazywania danych do testów w xUnit. Dane są podawane bezpośrednio w atrybucie i przypisywane jako parametry do testu.

Przykład 1: Testowanie dodawania liczb

// Test sprawdza, czy suma dwóch liczb zwraca oczekiwany wynik.
[Theory]
[InlineData(1, 2, 3)]    // 1 + 2 = 3
[InlineData(5, 5, 10)]   // 5 + 5 = 10
[InlineData(-1, -2, -3)] // -1 + -2 = -3
public void Add_TwoNumbers_ReturnsCorrectSum(int a, int b, int expected)
{
    // Sprawdzamy, czy suma dwóch liczb jest równa oczekiwanemu wynikowi.
    Assert.Equal(expected, a + b);
}

Przykład 2: Testowanie długości stringa

// Test sprawdza, czy długość stringa zwraca oczekiwaną wartość.
[Theory]
[InlineData("test", 4)]   // "test" ma długość 4
[InlineData("xUnit", 5)]  // "xUnit" ma długość 5
[InlineData("", 0)]       // pusty string ma długość 0
public void StringLength_ReturnsCorrectLength(string input, int expectedLength)
{
    // Sprawdzamy, czy długość stringa jest równa oczekiwanej wartości.
    Assert.Equal(expectedLength, input.Length);
}
MemberData

MemberData pozwala na pobieranie danych z metod, właściwości lub pól w klasie. Jest bardziej elastyczne niż InlineData, ponieważ umożliwia zwracanie bardziej złożonych struktur danych.

Przykład 1: Testowanie dodawania liczb z MemberData

// Zdefiniowanie danych testowych jako właściwości.
public static IEnumerable<object[]> AddTestData =>
[
    [1, 2, 3],     // 1 + 2 = 3
    [10, 20, 30],  // 10 + 20 = 30
    [-5, -10, -15] // -5 + -10 = -15
];

// Test korzystający z MemberData, pobierający dane z właściwości AddTestData.
[Theory]
[MemberData(nameof(AddTestData))]
public void Add_TwoNumbers_ReturnsCorrectSum_MemberData(int a, int b, int expected)
{
    // Sprawdzamy, czy suma dwóch liczb jest równa oczekiwanemu wynikowi.
    Assert.Equal(expected, a + b);
}

Przykład 2: Testowanie czy liczba jest parzysta

// Zdefiniowanie danych testowych w metodzie zwracającej IEnumerable<object[]>.
public static IEnumerable<object[]> EvenNumberTestData()
{
    yield return new object[] { 2, true };   // 2 jest parzyste
    yield return new object[] { 3, false };  // 3 jest nieparzyste
    yield return new object[] { 10, true };  // 10 jest parzyste
}

// Test sprawdzający, czy liczba jest parzysta, korzystający z MemberData.
[Theory]
[MemberData(nameof(EvenNumberTestData))]
public void IsEvenNumber_ReturnsCorrectResult_MemberData(int number, bool expected)
{
    // Sprawdzamy, czy liczba jest parzysta (czy reszta z dzielenia przez 2 wynosi 0).
    Assert.Equal(expected, number % 2 == 0);
}
ClassData

ClassData jest jeszcze bardziej zaawansowanym podejściem, które pozwala na wykorzystanie pełnych klas do dostarczania danych testowych. Klasa dostarczająca dane musi implementować interfejs IEnumerable<object[]>.

Przykład 1: Klasa danych do testów liczb parzystych

// Klasa implementująca IEnumerable<object[]> dostarczająca dane do testów.
public class EvenNumberClassData : IEnumerable<object[]>
{
    public IEnumerator<object[]> GetEnumerator()
    {
        // Zwracamy dane testowe: liczba oraz czy jest parzysta.
        yield return new object[] { 4, true };  // 4 jest parzyste
        yield return new object[] { 7, false }; // 7 jest nieparzyste
        yield return new object[] { 16, true }; // 16 jest parzyste
    }

    // Implementacja IEnumerable.
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

// Test korzystający z ClassData, pobierający dane z klasy EvenNumberClassData.
[Theory]
[ClassData(typeof(EvenNumberClassData))]
public void IsEvenNumber_ReturnsCorrectResult_ClassData(int number, bool expected)
{
    // Sprawdzamy, czy liczba jest parzysta.
    Assert.Equal(expected, number % 2 == 0);
}

Przykład 2: Klasa danych do testowania operacji na stringach

// Klasa dostarczająca dane do testowania operacji na stringach.
public class StringOperationsClassData : IEnumerable<object[]>
{
    public IEnumerator<object[]> GetEnumerator()
    {
        // Zwracamy dane: oryginalny string oraz oczekiwany wynik po zamianie na wielkie litery.
        yield return new object[] { "hello", "HELLO" }; // "hello" -> "HELLO"
        yield return new object[] { "world", "WORLD" }; // "world" -> "WORLD"
    }

    // Implementacja IEnumerable.
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

// Test sprawdzający, czy metoda ToUpper zwraca string w wielkich literach.
[Theory]
[ClassData(typeof(StringOperationsClassData))]
public void ToUpper_ReturnsUppercaseString(string input, string expected)
{
    // Sprawdzamy, czy zamiana na wielkie litery działa poprawnie.
    Assert.Equal(expected, input.ToUpper());
}
Zaawansowane przykłady

Teraz przyjrzyjmy się bardziej złożonym przypadkom użycia MemberData i ClassData.

Zaawansowany Przykład 1: Testowanie złożonych obiektów za pomocą MemberData
Czasami musimy przetestować bardziej złożone dane, jak obiekty, które mają więcej właściwości.

// Klasa reprezentująca osobę.
public class Person
{
    public string Name { get; set; } // Imię
    public int Age { get; set; }     // Wiek
}

// Zdefiniowanie danych testowych jako właściwości.
public static IEnumerable<object[]> PersonTestData =>
[
    [new Person { Name = "Rias", Age = 22 }, true],    // Rias jest pełnoletnia
    [new Person { Name = "Hinata", Age = 17 }, false]  // Hinata nie jest pełnoletnia
];

// Test sprawdzający, czy osoba jest pełnoletnia, na podstawie wieku.
[Theory]
[MemberData(nameof(PersonTestData))]
public void IsAdult_ReturnsCorrectResult(Person person, bool expected)
{
    // Sprawdzamy, czy osoba jest dorosła (wiek >= 18).
    bool isAdult = person.Age >= 18;
    Assert.Equal(expected, isAdult);
}

Zaawansowany Przykład 2: Dynamiczne generowanie danych w ClassData

 // Klasa dynamicznie generująca dane testowe na podstawie parametru.
 public class DynamicTestData : IEnumerable<object[]>
 {
     private readonly int _max;

     // Domyślny konstruktor z ustaloną wartością maksymalną.
     public DynamicTestData()
     {
         _max = 10; // Ustalamy liczbę jako 10
     }

     // Generowanie danych testowych.
     public IEnumerator<object[]> GetEnumerator()
     {
         for (int i = 1; i <= _max; i++)
         {
             // Zwracamy liczby od 1 do max oraz informację, czy są parzyste.
             yield return new object[] { i, i % 2 == 0 };
         }
     }

     // Implementacja IEnumerable.
     IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
 }

 // Test sprawdzający, czy liczba jest parzysta, z dynamicznie generowanymi danymi.
 [Theory]
 [ClassData(typeof(DynamicTestData))] // Testujemy liczby od 1 do 10
 public void IsEvenNumber_GeneratesDataDynamically(int number, bool expected)
 {
     // Sprawdzamy, czy liczba jest parzysta.
     Assert.Equal(expected, number % 2 == 0);
 }
Podsumowanie

Korzystanie z atrybutów takich jak InlineData, MemberData i ClassData w xUnit pozwala na wygodne i elastyczne parametryzowanie testów jednostkowych. InlineData jest świetne dla prostych danych, MemberData pozwala na bardziej elastyczne definiowanie zestawów testowych, a ClassData umożliwia dynamiczne generowanie danych testowych w bardziej złożonych scenariuszach. Wybór zależy od złożoności testów i potrzeb projektu.

Dzięki dodanym komentarzom, kod staje się bardziej czytelny i łatwiejszy do zrozumienia, co jest kluczowe, zwłaszcza w przypadku pracy zespołowej.

Tagi
O Autorze
Tajko
Tajko Tajko z tej strony:) Obecnie pracuję we Wrocławiu przy projektach desktopowych. Skoro sam się czegoś nauczyłem to i inni mogliby nauczyć się tego co ja w łopatologiczny sposób:)

ZOSTAW ODPOWIEDŹ