Testing with properties using FsCheck

Example-based testing vs Property-based testing. Testovanie na základe špecifikácií nám môže pomôcť testovať scenáre na ktoré nemáme príklady.

Property-based testing - FsCheck

Predpokladám, že väčšina z nás pri písaní unit testov využíva techniku, ktorá sa nazýva example-based testing. Táto technika je založená na tom, že napíšeme testy, ktoré overujú správanie našich funkcií pre konkrétne vstupy.

Je to prirodzený spôsob, pretože naše vnímanie funguje na základe príkladov.

Example based testing pre funkciu sčítania by mohol vyzerať nasledovne:

[Theory]
[InlineData(1, 2, 3)]
[InlineData(2, 2, 4)]
[InlineData(3, 2, 5)]
public void Add_ShouldReturnSumOfTwoNumbers(int a, int b, int expected)
{
    var result = Calculator.Add(a, b);
    result.Should().Be(expected);
}

Je to jednoduché, veď čo sa už môže pokaziť na funkcii sčítania. Pár príkladov nám stačí a máme pokrytie 100%. Alebo nie?

Čo keď ale chceme implementovať vlastnú "super rýchlu" funkciu sčítania založenú na bitových operáciách? Napríkald niečo nasledovné:

public static int Add(int a, int b)
{
    while (b != 0)
    {
        int carry = a & b;
        a = a ^ b;
        b = carry << 1;
    }
    return a;
}

Ako overíme, že táto funkcia funguje správne? Čo keď je algoritmus komplikovaný a pár príkladov nám nestačí?
Tu nám môže pomôcť property-based testing. Jedná sa o testovanie na základe definovaných vlastností, alebo možno lepšie špecifikácií.

Kto si pamätá z matematiky, tak sčítanie má niekoľko vlastností, ktoré musia byť splnené:

  1. Komutatívna vlastnosť: a + b = b + a
  2. Asociatívna vlastnosť: (a + b) + c = a + (b + c)
  3. Existencia neutrálného prvku: a + 0 = a

Tieto vlastnosti môžeme použiť na overenie správnosti našej funkcie sčítania. Aby sme to nemuseli všetko robiť, ručne tak nám môže pomôcť knižnica FsCheck.
Jedná sa o knižnicu primárne určenú pre jazyk F#, ale jej API je použiteľné aj v C#.

Nainštalujeme si knižnicu cez NuGet:

dotnet add package FsCheck.Xunit
// 👆 based on your testing framework

A potom môžeme napísať testy na základe vlastností:

[Property] // 👈 attribute for property-based testing
public Property Add_Should_Be_Commutative(int a, int b)
{
    return (Add(a, b) == Add(b, a))
        .ToProperty(); // 👈 convert boolean to Property
}

[Property]
public Property Add_Should_Be_Associative(int a, int b, int c)
{
    return (Add(Add(a, b), c) == Add(a, Add(b, c))).ToProperty();
}

[Property]
public Property Add_Should_Be_Identity(int a)
{
    return (Add(a, 0) == a).ToProperty();
}

A to je všetko. Teraz máme testy, ktoré overujú správnosť našej funkcie sčítania na základe matematických vlastností.
A to všetko bez toho, aby sme museli písať konkrétne príklady.

FsCheck bude pre nás generovať náhodné vstupy a overovať našu funkciu na základe definovaných vlastností. Má veľa generátorov pre rôzne typy dát a metodiky ako generovať náhodné vstupy (aký rozsah, ...).

V prípade, že test odhalí chybu, tak metódou shrinking nám nájde najmenší vstup, ktorý spôsobuje chybu.

Napríklad niečo takéto:

FsCheck.Xunit.PropertyFailedException : 
Falsifiable, after 3 tests (11 shrinks) (StdGen (422220575,297303727)):
Original:
(-137, 122)
Shrunk:
(1, 101)

Áno, nie každý deň píšeme funkciu na sčítanie, alebo podobné matematické funkcie. Ale property-based testing nám môže pomôcť aj pri testovaní "bežných" algoritmov alebo funkcií. Napríklad pri testovaní parserov, serializátorov, validátorov, ...

Je to vhodné v prípade keď:

  • existuje invertná funkcia (serializácia / deserializácia, zápis / čítanie, crypt / decrypt, ...)
  • vieme definovať požadované vlastnosti (komutatívna, asociatívna, distributívna, ...)
  • robíme refaktoring (overenie, že nová implementácia je ekvivalentná starej)
  • robíme Fuzz testing (chceme zistiť, kde sú hranice nášho algoritmu)
  • ...

🔗 Zdroje