Ajánlott egységtesztelési eljárások a .NET Core és a .NET Standard használatával

Az egységtesztek írásának számos előnye van; segítenek a regresszióban, dokumentációt nyújtanak, és megkönnyítik a jó tervezést. A nehezen olvasható és törékeny egységtesztek azonban pusztítást okozhatnak a kódbázison. Ez a cikk néhány ajánlott eljárást ismertet a .NET Core- és .NET Standard-projektek egységteszt-tervezésével kapcsolatban.

Ebben az útmutatóban megismerhet néhány ajánlott eljárást az egységtesztek írása során, hogy a tesztek rugalmasak és könnyen érthetőek maradjanak.

John Reese külön köszönet Roy Osherove

Miért érdemes egységtesztet végezni?

Az egységtesztek használatának több oka is van.

Kevesebb idő a funkcionális tesztek elvégzésére

A funkcionális tesztek költségesek. Ezek általában magukban foglalják az alkalmazás megnyitását és az Ön (vagy valaki más) által követendő lépések sorozatát a várt viselkedés ellenőrzéséhez. Előfordulhat, hogy ezek a lépések nem mindig ismertek a tesztelő számára. A teszt elvégzéséhez kapcsolatba kell lépniük valakivel, aki jártosabb a területen. Maga a tesztelés másodpercekig is eltarthat a triviális változások esetén, vagy a nagyobb módosítások esetén percekig is eltarthat. Végül ezt a folyamatot meg kell ismételni a rendszerben végzett minden módosításnál.

Az egységtesztek viszont ezredmásodpercig is eltarthatnak, egy gombnyomással futtathatók, és nem feltétlenül igényelnek a rendszer széles körű ismereteit. Az, hogy a teszt sikeres-e vagy sem, az a tesztfuttatón múlik, nem az egyénen.

Regresszió elleni védelem

A regressziós hibák olyan hibák, amelyeket az alkalmazás módosításakor vezetnek be. Gyakori, hogy a tesztelők nem csak az új funkciójukat tesztelik, hanem a korábban meglévő funkciókat is tesztelik annak ellenőrzéséhez, hogy a korábban implementált funkciók továbbra is a várt módon működnek-e.

Az egységtesztelés lehetővé teszi a teljes tesztcsomag újrafuttatását minden build után, vagy akár egy kódsor módosítása után is. Ezzel biztos lehet abban, hogy az új kód nem szakítja meg a meglévő funkciókat.

Végrehajtható dokumentáció

Lehet, hogy nem mindig nyilvánvaló, hogy egy adott módszer mit tesz, vagy hogyan viselkedik adott bemenettel. Felteheti magának a kérdést: Hogyan viselkedik ez a módszer, ha egy üres sztringet adok át? Null?

Ha jól elnevezett egységtesztekkel rendelkezik, minden tesztnek egyértelműen meg kell tudnia magyarázni egy adott bemenet várható kimenetét. Emellett képesnek kell lennie annak ellenőrzésére, hogy valóban működik-e.

Kevésbé kapcsolt kód

Ha a kód szorosan össze van állítva, nehéz lehet egyesíteni a tesztelést. Ha nem hoz létre egységteszteket az éppen írt kódhoz, az összekapcsolás kevésbé nyilvánvaló.

A kód tesztjeinek írása természetesen leválasztja a kódot, mert máskülönben nehezebb lenne tesztelni.

A jó egységteszt jellemzői

  • Gyors: Nem ritka, hogy az érett projektek több ezer egységteszttel rendelkeznek. Az egységtesztek futtatása kevés időt vesz igénybe. Ezredmásodperc.
  • Izolált: Az egységtesztek önállóak, elkülönítve futtathatók, és nem függenek semmilyen külső tényezőtől, például fájlrendszertől vagy adatbázistól.
  • Megismételhető: Az egységteszt futtatásának összhangban kell lennie az eredményeivel, vagyis mindig ugyanazt az eredményt adja vissza, ha nem módosít semmit a futtatások között.
  • Önellenőrzés: A tesztnek képesnek kell lennie arra, hogy automatikusan észlelje, hogy az emberi beavatkozás nélkül sikeres vagy sikertelen volt-e.
  • Idő: Az egységtesztek írása nem vehet aránytalanul hosszú időt a tesztelt kódhoz képest. Ha úgy találja, hogy a kód tesztelése nagy időt vesz igénybe a kód írásához képest, fontolja meg a tesztelhetőbb kialakítást.

Kódlefedettség

A magas kódlefedettségi arány gyakran magasabb szintű kódhoz van társítva. Maga a mérés azonban nem tudja meghatározni a kód minőségét. A túlságosan ambiciózus kódlefedettségi százalékcél beállítása kontraproduktív lehet. Képzeljen el egy összetett projektet több ezer feltételes ággal, és képzelje el, hogy 95%-os kódlefedettségi célt tűz ki. A projekt jelenleg 90%-os kódlefedettségi szinten áll. A fennmaradó 5%-ban az összes peremes ügy esetében figyelembe vehető idő nagy vállalkozás lehet, és az értékajánlat gyorsan csökken.

A magas kódlefedettségi arány nem a sikeresség jelzője, és nem is jelent magas kódminőséget. Csak az egységtesztek által lefedett kód mennyiségét jelöli. További információ: egységtesztelési kódlefedettség.

Beszéljük ugyanazt a nyelvet

A "mock" kifejezést sajnos gyakran visszaélik, amikor a tesztelésről beszél. Az egységtesztek írásakor az alábbi pontok határozzák meg a leggyakoribb hamis típusokat:

Hamis – A hamis egy általános kifejezés, amely egy csonk vagy egy szimulált objektum leírására használható. Attól függ, hogy csonkról vagy zokniról van-e szó, attól függ, hogy milyen környezetben használják. Tehát más szóval, egy hamis lehet egy csonk vagy egy zokni.

Mock – A modellobjektum egy hamis objektum a rendszerben, amely eldönti, hogy egy egységteszt sikeres vagy sikertelen volt-e. A zokni hamisként indul, amíg nem érvényesítik.

Csonk – A csonk a rendszer meglévő függőségeinek (vagy közreműködőinek) szabályozható helyettesítése. Egy csonk használatával anélkül tesztelheti a kódot, hogy közvetlenül kezelned kell a függőséget. Alapértelmezés szerint egy csonk hamisként indul el.

Vegye figyelembe a következő kódrészletet:

var mockOrder = new MockOrder();
var purchase = new Purchase(mockOrder);

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

Az előző példában egy csonkot nevezünk makettnek. Ebben az esetben ez egy csonk. Csak úgy adja át a sorrendet, hogy képes legyen példányosítani Purchase (a tesztelt rendszer). A név MockOrder azért is félrevezető, mert a sorrend nem egy makett.

A jobb megközelítés a következő lenne:

var stubOrder = new FakeOrder();
var purchase = new Purchase(stubOrder);

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

Az osztály FakeOrderátnevezésével sokkal általánosabbá tette az osztályt. Az osztály használható makettként vagy csonkként, attól függően, hogy melyik a jobb a tesztesethez. Az előző példában FakeOrder csonkként használatos. Az állítás során semmilyen alakzatot vagy formát nem használ FakeOrder . FakeOrder a konstruktor követelményeinek kielégítése érdekében át lett adva Purchase az osztályba.

Ha mockként szeretné használni, az alábbi kódhoz hasonlót tehet:

var mockOrder = new FakeOrder();
var purchase = new Purchase(mockOrder);

purchase.ValidateOrders();

Assert.True(mockOrder.Validated);

Ebben az esetben egy tulajdonságot ellenőriz a Hamis (érvényesítve vele szemben) tulajdonságon, így az előző kódrészletben ez mockOrder egy Mock.

Fontos

Fontos, hogy a terminológia helyes legyen. Ha a csonkokat "mocks"-nak hívja, más fejlesztők hamis feltételezéseket fognak tenni a szándékáról.

A zoknikkal és a csonkokkal kapcsolatban az a legfontosabb, hogy a zoknik olyanok, mint a csonkok, de a modell objektummal szemben érvényesíted, míg a csonkok ellen nem.

Ajánlott eljárások

Az alábbiakban bemutatjuk az egységtesztek írásának legfontosabb ajánlott eljárásait.

Az infrastruktúra-függőségek elkerülése

Az egységtesztek írásakor próbálja meg nem bevezetni az infrastruktúrától való függőségeket. A függőségek lassúvá és törékenysé teszik a teszteket, és az integrációs tesztek számára fenntartottnak kell lenniük. Ezeket a függőségeket elkerülheti az alkalmazásban az Explicit függőségek elvének követésével és a függőséginjektálás használatával. Az egységteszteket az integrációs tesztektől eltérő projektben is megtarthatja. Ez a megközelítés biztosítja, hogy az egységtesztelési projekt nem hivatkozik az infrastruktúra-csomagokra vagy függőségekre.

A tesztek elnevezése

A teszt nevének három részből kell állnia:

  • A tesztelt metódus neve.
  • A tesztelési forgatókönyv.
  • A forgatókönyv meghívásának várt viselkedése.

Miért?

Az elnevezési szabványok azért fontosak, mert kifejezetten kifejezik a teszt szándékát. A tesztek nem csupán a kód működésének biztosítását teszik lehetővé, de dokumentációt is nyújtanak. Csak az egységtesztek csomagjának megtekintésével képesnek kell lennie arra, hogy a kód viselkedését anélkül tudja kikövetkeztetni, hogy még magát a kódot sem nézi. Emellett ha a tesztek sikertelenek, pontosan láthatja, hogy mely forgatókönyvek nem felelnek meg az elvárásainak.

Rossz:

[Fact]
public void Test_Single()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

Jobb:

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

A tesztek rendezése

Az elrendezés, a cselekvés, az igényesség gyakori minta az egységtesztelés során. Ahogy a név is mutatja, három fő műveletből áll:

  • Rendezze el az objektumokat, hozza létre és állítsa be őket szükség szerint.
  • Művelet egy objektumon.
  • Állítsd be , hogy valami a vártnak megfelelően van.

Miért?

  • Egyértelműen elkülöníti a tesztelt elemet az elrendezési és érvényesítési lépésektől.
  • Kevesebb eséllyel keverheti össze az állításokat az "Act" kóddal.

A teszt írásakor az olvashatóság az egyik legfontosabb szempont. Az egyes műveleteknek a teszten belüli elkülönítése egyértelműen kiemeli a kód meghívásához szükséges függőségeket, a kód meghívásának módját és az érvényesíteni kívánt műveletet. Bár előfordulhat, hogy egyesíthet néhány lépést, és csökkentheti a teszt méretét, az elsődleges cél az, hogy a teszt a lehető legolvasottabb legyen.

Rossz:

[Fact]
public void Add_EmptyString_ReturnsZero()
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Assert
    Assert.Equal(0, stringCalculator.Add(""));
}

Jobb:

[Fact]
public void Add_EmptyString_ReturnsZero()
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Act
    var actual = stringCalculator.Add("");

    // Assert
    Assert.Equal(0, actual);
}

Minimálisan átmenő tesztek írása

Az egységtesztben használandó bemenetnek a lehető legegyszerűbbnek kell lennie a jelenleg tesztelt viselkedés ellenőrzéséhez.

Miért?

  • A tesztek ellenállóbbá válnak a kódbázis jövőbeli változásaival szemben.
  • Közelebb a tesztelési viselkedéshez a megvalósítás során.

A teszt elvégzéséhez szükségesnél több információt tartalmazó tesztek nagyobb eséllyel vezetnek be hibákat a tesztbe, és kevésbé egyértelművé tehetik a teszt szándékát. Tesztek írásakor a viselkedésre szeretne összpontosítani. Ha további tulajdonságokat állít be a modelleken, vagy nem nulla értéket használ, ha nincs szükség rá, csak a bizonyítani kívánt értékeket vonja vissza.

Rossz:

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("42");

    Assert.Equal(42, actual);
}

Jobb:

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

A bűvös sztringek elkerülése

A változók elnevezése az egységtesztekben fontos, ha nem fontosabb, mint az éles kód változóinak elnevezése. Az egységteszteknek nem szabad mágikus sztringeket tartalmazniuk.

Miért?

  • Megakadályozza, hogy a teszt olvasójának meg kell vizsgálnia az éles kódot annak érdekében, hogy kiderítse, mi teszi különlegessé az értéket.
  • Kifejezetten megmutatja, hogy mit próbál bizonyítani ahelyett, hogy megpróbálná elérni.

A mágikus sztringek zavart okozhatnak a tesztek olvasója számára. Ha egy sztring a szokásostól eltérően néz ki, felmerülhet a kérdés, hogy miért választottak ki egy adott értéket egy paraméterhez vagy visszatérési értékhez. Az ilyen típusú sztringértékek ahelyett, hogy a tesztre összpontosítanának, közelebbről megvizsgálhatják a megvalósítás részleteit.

Tipp.

Tesztek írásakor a lehető legtöbb szándék kifejezésére kell törekednie. A mágikus sztringek esetében jó módszer ezeknek az értékeknek az állandókhoz való hozzárendelése.

Rossz:

[Fact]
public void Add_BigNumber_ThrowsException()
{
    var stringCalculator = new StringCalculator();

    Action actual = () => stringCalculator.Add("1001");

    Assert.Throws<OverflowException>(actual);
}

Jobb:

[Fact]
void Add_MaximumSumResult_ThrowsOverflowException()
{
    var stringCalculator = new StringCalculator();
    const string MAXIMUM_RESULT = "1001";

    Action actual = () => stringCalculator.Add(MAXIMUM_RESULT);

    Assert.Throws<OverflowException>(actual);
}

A logika elkerülése a tesztekben

Az egységtesztek írásakor kerülje a manuális sztringösszefűzést, a logikai feltételeket, például ifa , while, forés switchaz egyéb feltételeket.

Miért?

  • Kevesebb esély van arra, hogy hibát vezessen be a teszteken belül.
  • A végeredményre összpontosítson a megvalósítás részletei helyett.

Amikor logikát vezet be a tesztcsomagba, jelentősen megnő annak az esélye, hogy egy hibát bevezsel. Az utolsó hely, ahol hibát szeretne találni, a tesztcsomagban található. Magas szintű megbízhatóságot kell adnia a tesztek működésében, ellenkező esetben nem fog megbízni bennük. A nem megbízható tesztek nem adnak értéket. Ha egy teszt meghiúsul, azt szeretné, hogy legyen olyan érzése, hogy valami nincs rendben a kóddal, és hogy nem hagyható figyelmen kívül.

Tipp.

Ha a teszt logikája elkerülhetetlennek tűnik, fontolja meg a teszt két vagy több különböző tesztre való felosztását.

Rossz:

[Fact]
public void Add_MultipleNumbers_ReturnsCorrectResults()
{
    var stringCalculator = new StringCalculator();
    var expected = 0;
    var testCases = new[]
    {
        "0,0,0",
        "0,1,2",
        "1,2,3"
    };

    foreach (var test in testCases)
    {
        Assert.Equal(expected, stringCalculator.Add(test));
        expected += 3;
    }
}

Jobb:

[Theory]
[InlineData("0,0,0", 0)]
[InlineData("0,1,2", 3)]
[InlineData("1,2,3", 6)]
public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected)
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add(input);

    Assert.Equal(expected, actual);
}

Segítségkérési módszerek használata a beállításhoz és a lebontáshoz

Ha hasonló objektumot vagy állapotot igényel a tesztekhez, inkább egy segédmetódust válasszon, mint a használatot Setup és Teardown az attribútumokat, ha léteznek.

Miért?

  • A tesztek olvasásakor kisebb a zavar, mivel az összes kód látható az egyes teszteken belül.
  • Kevesebb esély van arra, hogy túl sokat vagy túl keveset állítson be az adott teszthez.
  • Kevesebb esély van az állapot tesztek közötti megosztására, ami nem kívánt függőségeket hoz létre közöttük.

Az egységtesztelési keretrendszerekben Setup a program minden egyes egységteszt előtt meghívja a tesztcsomagot. Bár egyesek hasznos eszköznek tekinthetik, általában felfúvódó és nehezen olvasható tesztekhez vezet. Az egyes tesztek általában eltérő követelményekkel rendelkeznek a teszt indításához és futtatásához. Sajnos, kényszeríti Setup , hogy pontosan ugyanazokat a követelményeket minden teszt.

Feljegyzés

Az xUnit a 2.x verziótól a SetUp és a TearDownt is eltávolította

Rossz:

private readonly StringCalculator stringCalculator;
public StringCalculatorTests()
{
    stringCalculator = new StringCalculator();
}
// more tests...
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var result = stringCalculator.Add("0,1");

    Assert.Equal(1, result);
}

Jobb:

[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var stringCalculator = CreateDefaultStringCalculator();

    var actual = stringCalculator.Add("0,1");

    Assert.Equal(1, actual);
}
// more tests...
private StringCalculator CreateDefaultStringCalculator()
{
    return new StringCalculator();
}

Több felvonás elkerülése

A tesztek írásakor próbáljon tesztenként csak egy műveletet belefoglalni. Egyetlen jogi aktus használatának gyakori megközelítései a következők:

  • Hozzon létre egy külön tesztet minden egyes felvonáshoz.
  • Használjon paraméteres teszteket.

Miért?

  • Ha a teszt sikertelen, egyértelmű, hogy melyik cselekmény meghiúsul.
  • Biztosítja, hogy a teszt csak egyetlen esetre koncentráljon.
  • A tesztek sikertelenségével kapcsolatos teljes képet ad.

Több cselekményt kell egyenként érvényesíteni, és nem garantált, hogy az összes érvényesítés végrehajtásra kerül. A legtöbb egységtesztelési keretrendszerben a rendszer automatikusan sikertelennek tekinti az eljárásteszteket, ha egy egységteszt során egy helyességi feltétel meghiúsul. Ez a folyamat zavarba ejtő lehet, mivel a ténylegesen működő funkciók meghiúsulnak.

Rossz:

[Fact]
public void Add_EmptyEntries_ShouldBeTreatedAsZero()
{
    // Act
    var actual1 = stringCalculator.Add("");
    var actual2 = stringCalculator.Add(",");

    // Assert
    Assert.Equal(0, actual1);
    Assert.Equal(0, actual2);
}

Jobb:

[Theory]
[InlineData("", 0)]
[InlineData(",", 0)]
public void Add_EmptyEntries_ShouldBeTreatedAsZero(string input, int expected)
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Act
    var actual = stringCalculator.Add(input);

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

Privát metódusok ellenőrzése nyilvános metódusok egységtesztelésével

A legtöbb esetben nem szükséges privát metódust tesztelni. A privát metódusok implementálási részletek, és soha nem léteznek elszigetelten. Egy bizonyos ponton lesz egy nyilvánosan elérhető metódus, amely a privát metódust a implementáció részeként hívja meg. Ami fontos, az a privát metódusba behívott nyilvános metódus végeredménye.

Fontolja meg a következő esetet:

public string ParseLogLine(string input)
{
    var sanitizedInput = TrimInput(input);
    return sanitizedInput;
}

private string TrimInput(string input)
{
    return input.Trim();
}

Az első reakció lehet, hogy elkezd írni egy tesztet TrimInput , mert meg szeretné győződni arról, hogy a módszer a várt módon működik. Azonban teljesen lehetséges, hogy ParseLogLine manipulálja sanitizedInput oly módon, hogy nem számít, így a teszt ellen TrimInput használhatatlan.

Az igazi tesztet a nyilvánosan elérhető módszeren ParseLogLine kell elvégezni, mert végső soron ez az, amellyel foglalkoznia kell.

public void ParseLogLine_StartsAndEndsWithSpace_ReturnsTrimmedResult()
{
    var parser = new Parser();

    var result = parser.ParseLogLine(" a ");

    Assert.Equals("a", result);
}

Ezzel a nézőponttal, ha privát metódust lát, keresse meg a nyilvános metódust, és írja a teszteket erre a metódusra. Csak azért, mert egy privát metódus a várt eredményt adja vissza, nem jelenti azt, hogy a privát metódust végül meghívó rendszer helyesen használja az eredményt.

Csonk statikus hivatkozásai

Az egységtesztek egyik alapelve, hogy teljes mértékben ellenőriznie kell a vizsgált rendszert. Ez az elv akkor lehet problémás, ha az éles kód statikus hivatkozásokra (például DateTime.Now) irányuló hívásokat tartalmaz. Tekintse meg az alábbi kódot:

public int GetDiscountedPrice(int price)
{
    if (DateTime.Now.DayOfWeek == DayOfWeek.Tuesday)
    {
        return price / 2;
    }
    else
    {
        return price;
    }
}

Hogyan lehet ezt a kódot egységtesztelni? Kipróbálhat egy olyan módszert, mint például:

public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
    var priceCalculator = new PriceCalculator();

    var actual = priceCalculator.GetDiscountedPrice(2);

    Assert.Equals(2, actual)
}

public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
    var priceCalculator = new PriceCalculator();

    var actual = priceCalculator.GetDiscountedPrice(2);

    Assert.Equals(1, actual);
}

Sajnos gyorsan rájön, hogy néhány probléma van a tesztekkel.

  • Ha a tesztcsomag kedden fut, a második teszt sikeres lesz, de az első teszt sikertelen lesz.
  • Ha a tesztcsomag bármelyik másik napon fut, az első teszt sikeres lesz, de a második teszt sikertelen lesz.

A problémák megoldásához be kell vezetnie egy varratot az éles kódba. Az egyik módszer az, hogy becsomagolja azt a kódot, amelyet egy felületen kell vezérelnie, és az éles kód az adott interfésztől függ.

public interface IDateTimeProvider
{
    DayOfWeek DayOfWeek();
}

public int GetDiscountedPrice(int price, IDateTimeProvider dateTimeProvider)
{
    if (dateTimeProvider.DayOfWeek() == DayOfWeek.Tuesday)
    {
        return price / 2;
    }
    else
    {
        return price;
    }
}

A tesztcsomag a következő lesz:

public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
    var priceCalculator = new PriceCalculator();
    var dateTimeProviderStub = new Mock<IDateTimeProvider>();
    dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Monday);

    var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);

    Assert.Equals(2, actual);
}

public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
    var priceCalculator = new PriceCalculator();
    var dateTimeProviderStub = new Mock<IDateTimeProvider>();
    dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Tuesday);

    var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);

    Assert.Equals(1, actual);
}

Most a tesztcsomag teljes körű vezérléssel DateTime.Now rendelkezik, és bármilyen értéket képes megcsúszni a metódusba való behíváskor.