.NET Core ve .NET Standard ile birim testi en iyi yöntemleri

Birim testleri yazmanın birçok avantajı vardır; regresyona yardımcı olur, belge sağlar ve iyi bir tasarım sağlar. Ancak, okuması zor ve kırılgan birim testleri kod tabanınızda hasara neden olabilir. Bu makalede, .NET Core ve .NET Standard projeleriniz için birim testi tasarımıyla ilgili bazı en iyi yöntemler açıklanmaktadır.

Bu kılavuzda, testlerinizi dayanıklı ve anlaşılması kolay tutmak için birim testleri yazarken en iyi yöntemleri öğreneceksiniz.

John Reese ile Roy Osherove'a özel teşekkürler

Neden birim testi?

Birim testlerini kullanmanın çeşitli nedenleri vardır.

İşlevsel testleri gerçekleştirmek için daha az zaman

İşlevsel testler pahalıdır. Bunlar genellikle uygulamayı açmayı ve beklenen davranışı doğrulamak için sizin (veya başka birinin) izlemesi gereken bir dizi adımı gerçekleştirmeyi içerir. Bu adımlar her zaman test eden tarafından bilinmeyebilir. Testi gerçekleştirmek için bölgede daha bilgili birine ulaşmaları gerekecek. Testin kendisi önemsiz değişiklikler için saniyeler veya daha büyük değişiklikler için dakikalar sürebilir. Son olarak, bu işlemin sistemde yaptığınız her değişiklik için tekrarlanması gerekir.

Öte yandan birim testleri milisaniye alır, bir düğmeye basılarak çalıştırılabilir ve sistem hakkında büyük bir bilgi gerektirmez. Testin başarılı olup olmadığı veya başarısız olup olmadığı bireysel değil, test çalıştırıcısına aittir.

Regresyona karşı koruma

Regresyon hataları, uygulamada bir değişiklik yapıldığında ortaya çıkan hatalardır. Test edenlerin yalnızca yeni özelliklerini test etmekle kalmaz, önceden uygulanan özelliklerin hala beklendiği gibi çalıştığını doğrulamak için önceden var olan özellikleri test etmesi de yaygındır.

Birim testi sayesinde her derlemeden sonra veya kod satırını değiştirdikten sonra tüm test paketinizi yeniden çalıştırabilirsiniz. Yeni kodunuzun mevcut işlevselliği bozmadığından size güven verir.

Yürütülebilir belgeler

Belirli bir yöntemin ne yaptığı veya belirli bir girişe göre nasıl davrandığı her zaman açık olmayabilir. Kendinize şunu sorabilirsiniz: Boş bir dize geçirirsem bu yöntem nasıl davranır? Null?

İyi adlandırılmış birim testlerinden oluşan bir paketiniz olduğunda, her test belirli bir giriş için beklenen çıkışı açıkça açıklayabilmelidir. Buna ek olarak, gerçekten çalıştığını doğrulayabilmelidir.

Daha az bağlı kod

Kod sıkı bir şekilde birleştirildiğinde birim testi yapmak zor olabilir. Yazdığınız kod için birim testleri oluşturmadan, bağlama daha az görünür olabilir.

Kodunuz için test yazmak, kodunuzu doğal olarak birbirinden ayırmanıza neden olur çünkü aksi takdirde test etmek daha zor olur.

İyi bir birim testinin özellikleri

  • Hızlı: Olgun projelerin binlerce birim testi olması yaygın bir durum değildir. Birim testlerinin çalıştırılması çok az zaman almalıdır. Milisaniye.
  • Yalıtılmış: Birim testleri tek başınadır, yalıtılmış olarak çalıştırılabilir ve dosya sistemi veya veritabanı gibi dış faktörlere bağımlılıkları yoktur.
  • Yinelenebilir: Bir birim testinin çalıştırılması sonuçlarıyla tutarlı olmalıdır, yani çalıştırmalar arasında hiçbir şeyi değiştirmezseniz her zaman aynı sonucu döndürür.
  • Kendi Kendine Denetim: Test, herhangi bir insan etkileşimi olmadan başarılı olup olmadığını otomatik olarak algılayabilmelidir.
  • Zamanında: Birim testinin yazılması test edilen kodla karşılaştırıldığında orantısız bir şekilde uzun sürmemelidir. Kodun test edilmesi, kodu yazmakla karşılaştırıldığında çok uzun sürüyorsa, daha test edilebilir bir tasarım düşünün.

Kod kapsamı

Yüksek kod kapsamı yüzdesi genellikle daha yüksek bir kod kalitesiyle ilişkilendirilir. Ancak ölçümün kendisi kodun kalitesini belirleyemez . Aşırı iddialı bir kod kapsamı yüzdesi hedefi belirlemek ters üretim olabilir. Binlerce koşullu dal içeren karmaşık bir proje düşünün ve %95 kod kapsamı hedefi belirlediğinizi düşünün. Şu anda proje %90 kod kapsamına sahip. Kalan %5'lik kısmın tüm uç durumlarını hesaba katma süresi büyük bir girişim olabilir ve değer teklifi hızla azalır.

Yüksek kod kapsamı yüzdesi, başarı göstergesi değildir ve yüksek kod kalitesi anlamına gelmez. Yalnızca birim testlerinin kapsadığı kod miktarını temsil eder. Daha fazla bilgi için bkz . birim testi kod kapsamı.

Aynı dili konuşalım

Sahte ifade ne yazık ki testten bahsederken sıklıkla kötüye kullanılır. Aşağıdaki noktalar, birim testleri yazarken en yaygın sahte türleri tanımlar:

Fake - Sahte, saplama veya sahte nesne tanımlamak için kullanılabilecek genel bir terimdir. Saplama veya sahte olması, kullanıldığı bağlama bağlıdır. Yani başka bir deyişle sahte bir saplama veya sahte olabilir.

Sahte - Sahte nesne, sistemdeki birim testinin geçip geçmediğine veya başarısız olup olmadığına karar veren sahte bir nesnedir. Sahte bir şey sahte olarak başlar ta ki ona karşı iddia edilene kadar.

Saplama - Saplama, sistemdeki mevcut bir bağımlılık (veya ortak çalışan) için denetlenebilir bir değiştirmedir. Saplama kullanarak, doğrudan bağımlılıkla ilgilenmeden kodunuzu test edebilirsiniz. Varsayılan olarak, saplama sahte olarak başlar.

Aşağıdaki kod parçacığını göz önünde bulundurun:

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

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

Yukarıdaki örnek, sahte olarak adlandırılan bir saplama olabilir. Bu durumda, bu bir saplamadır. Örneği oluşturabilmek Purchase için (test altındaki sistem) bir araç olarak Sipariş'i geçiriyorsunuz. Bu ad MockOrder da yanıltıcıdır çünkü sipariş sahte değildir.

Daha iyi bir yaklaşım:

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

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

sınıfını olarak yeniden adlandırarak sınıfı FakeOrderçok daha genel hale getirdiniz. Sınıfı, test çalışması için hangisi daha iyiyse sahte veya saplama olarak kullanılabilir. Yukarıdaki örnekte, FakeOrder saplama olarak kullanılır. Onay sırasında hiçbir şekilde veya biçimde kullanmazsınız FakeOrder . FakeOrder oluşturucunun Purchase gereksinimlerini karşılamak için sınıfına geçirildi.

Sahte olarak kullanmak için aşağıdaki koda benzer bir şey yapabilirsiniz:

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

purchase.ValidateOrders();

Assert.True(mockOrder.Validated);

Bu durumda, Fake üzerinde bir özelliği denetlersiniz (buna karşı onaylarsınız), bu nedenle önceki kod parçacığında mockOrder bir Sahtedir.

Önemli

Bu terminolojiyi doğru yapmak önemlidir. Saplamalarınıza "sahte" derseniz, diğer geliştiriciler amacınız hakkında yanlış varsayımlarda bulunur.

Sahteler ve saplamalar hakkında hatırlanması gereken en önemli şey, sahtelerin aynı saplamalar gibi olmasıdır, ancak sahte nesneye karşı iddiada bulunursunuz, ancak saplama için onay vermezsiniz.

En iyi yöntemler

Birim testleri yazmak için en önemli en iyi uygulamalardan bazıları aşağıdadır.

Altyapı bağımlılıklarından kaçınma

Birim testleri yazarken altyapıya bağımlılıklar eklememeye çalışın. Bağımlılıklar, testleri yavaş ve kırılgan hale getirir ve tümleştirme testleri için ayrılmalıdır. Açık Bağımlılıklar İlkesi'ni izleyerek ve Bağımlılık Ekleme'yi kullanarak uygulamanızda bu bağımlılıklardan kaçınabilirsiniz. Birim testlerinizi tümleştirme testlerinden ayrı bir projede de tutabilirsiniz. Bu yaklaşım, birim testi projenizin altyapı paketlerine yönelik başvuruları veya bağımlılıkları olmamasını sağlar.

Testlerinizi adlandırma

Testinizin adı üç bölümden oluşmalıdır:

  • Test edilen yöntemin adı.
  • Test edildiği senaryo.
  • Senaryo çağrıldığında beklenen davranış.

Neden?

Adlandırma standartları, testin amacını açıkça ifade ettiğinden önemlidir. Testler, kodunuzun çalıştığından emin olmaktan öte belgeler de sağlar. Yalnızca birim testleri paketine bakarak kodun kendisine bakmadan kodunuzun davranışını çıkarabilmeniz gerekir. Ayrıca testler başarısız olduğunda, beklentilerinizi tam olarak hangi senaryoların karşılamadığı görebilirsiniz.

Kötü:

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

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

    Assert.Equal(0, actual);
}

Iyi:

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

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

    Assert.Equal(0, actual);
}

Testlerinizi düzenleme

Düzenleme, Eylem, Onay birim testi sırasında yaygın bir desendir. Adından da anlaşılacağı gibi üç ana eylemden oluşur:

  • Nesnelerinizi düzenleyin , oluşturun ve gerektiği şekilde ayarlayın.
  • Bir nesne üzerinde işlem yapın.
  • Bir şeyin beklendiği gibi olduğunu onaylar .

Neden?

  • Test edilenleri düzenleme ve onaylama adımlarından açıkça ayırır.
  • "Act" koduyla onayları karıştırma şansı daha azdır.

Okunabilirlik, test yazarken en önemli yönlerden biridir. Testte bu eylemlerin her birini ayırmak, kodunuzu çağırmak için gereken bağımlılıkları, kodunuzun nasıl çağrıldığını ve onaylamaya çalıştığınız şeyleri açıkça vurgular. Bazı adımları birleştirmek ve testinizin boyutunu küçültmek mümkün olsa da, birincil hedef testi mümkün olduğunca okunabilir hale getirmektir.

Kötü:

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

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

Iyi:

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

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

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

Çok az geçişli testler yazma

Birim testinde kullanılacak giriş, şu anda test etmekte olduğunuz davranışı doğrulamak için mümkün olan en basit giriş olmalıdır.

Neden?

  • Testler, kod tabanındaki gelecekteki değişikliklere karşı daha dayanıklı hale gelir.
  • Uygulama üzerinde test davranışına daha yakın.

Testi geçirmek için gerekenden daha fazla bilgi içeren testlerin teste hata ekleme şansı daha yüksektir ve testin amacını daha az net hale getirebilirsiniz. Testler yazarken davranışa odaklanmak istiyorsunuz. Modellerde ek özellikler ayarlamak veya gerekli olmadığında sıfır olmayan değerler kullanmak, yalnızca kanıtlamaya çalıştığınız şeyi geri alır.

Kötü:

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

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

    Assert.Equal(42, actual);
}

Iyi:

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

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

    Assert.Equal(0, actual);
}

Sihirli dizelerden kaçının

Birim testlerindeki değişkenleri adlandırmak, üretim kodundaki değişkenleri adlandırmaktan daha önemli değilse önemlidir. Birim testleri sihirli dizeler içermemelidir.

Neden?

  • Değeri özel kılan şeyi bulmak için test okuyucusunun üretim kodunu incelemesi gereksinimini önler.
  • Neyi başarmak yerine neyi kanıtlamaya çalıştığınızı açıkça gösterir.

Sihirli dizeler, testlerinizin okuyucusunun kafa karışıklığına neden olabilir. Bir dize olağan dışı görünüyorsa, parametre veya dönüş değeri için belirli bir değerin neden seçildiğini merak edebilir. Bu tür dize değeri, teste odaklanmak yerine uygulama ayrıntılarına daha yakından bakmalarına neden olabilir.

İpucu

Test yazarken, mümkün olduğunca çok amacı ifade etmeyi hedeflemeniz gerekir. Sihirli dizeler söz konusu olduğunda, bu değerleri sabitlere atamak iyi bir yaklaşımdır.

Kötü:

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

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

    Assert.Throws<OverflowException>(actual);
}

Iyi:

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

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

    Assert.Throws<OverflowException>(actual);
}

Testlerde mantıktan kaçınma

Birim testlerinizi yazarken, el ile dize birleştirmeden, , while, forve switchgibi ifmantıksal koşullardan ve diğer koşullardan kaçının.

Neden?

  • Testlerinizin içinde bir hataya neden olma şansı daha düşüktür.
  • Uygulama ayrıntıları yerine sonu sonuğa odaklanın.

Test paketinize mantık eklediğinizde, içine bir hata ekleme şansı önemli ölçüde artar. Hata bulmak istediğiniz son yer, test paketinizin içindedir. Testlerinizin çalışacağına dair yüksek güven düzeyine sahip olmanız gerekir, aksi takdirde bunlara güvenmezsiniz. Güvenmediğiniz testler herhangi bir değer sağlamaz. Test başarısız olduğunda, kodunuzla ilgili bir sorun olduğunu ve yoksayılamaz olduğunu algılamak istersiniz.

İpucu

Testinizde mantık kaçınılmaz görünüyorsa, testi iki veya daha fazla farklı teste bölmeyi göz önünde bulundurun.

Kötü:

[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;
    }
}

Iyi:

[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);
}

Kurulum ve yırtma için yardımcı yöntemleri tercih etme

Testleriniz için benzer bir nesneye veya duruma ihtiyacınız varsa, ve Teardown özniteliklerini kullanmak Setup yerine yardımcı bir yöntemi tercih edin.

Neden?

  • Kodun tümü her testin içinden görülebildiğinden testleri okurken daha az karışıklık olur.
  • Verilen test için çok fazla veya çok az ayarlama şansı daha azdır.
  • Testler arasında durum paylaşma olasılığı daha azdır ve bu da aralarında istenmeyen bağımlılıklar oluşturur.

Birim testi çerçevelerinde, Setup test paketinizdeki her birim testlerinden önce çağrılır. Bazıları bunu yararlı bir araç olarak görse de, genellikle şişirilmiş ve zor okunan testlere yol açar. Testin çalışır duruma getirilmesi için her testin genel olarak farklı gereksinimleri olacaktır. Ne yazık ki, Setup sizi her test için tam olarak aynı gereksinimleri kullanmaya zorlar.

Not

xUnit, 2.x sürümünden itibaren Hem SetUp hem de TearDown'ı kaldırdı

Kötü:

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);
}

Iyi:

[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();
}

Birden çok eylemden kaçının

Testlerinizi yazarken, test başına yalnızca bir eylem eklemeyi deneyin. Tek bir eylem kullanmaya yönelik yaygın yaklaşımlar şunlardır:

  • Her eylem için ayrı bir test oluşturun.
  • Parametreli testleri kullanın.

Neden?

  • Test başarısız olduğunda, hangi eylemin başarısız olduğu açıktır.
  • Testin yalnızca tek bir olaya odaklanmasını sağlar.
  • Testlerinizin neden başarısız olduğuyla ilgili tüm resmi verir.

Birden çok eylemin tek tek Onaylanması gerekir ve tüm Assert'ların yürütüleceği garanti edilmemektedir. Çoğu birim testi çerçevesinde, bir Assert birim testinde başarısız olduğunda, devam eden testlerin otomatik olarak başarısız olduğu kabul edilir. Aslında çalışan işlevler başarısız olarak gösterileceği için bu tür işlemler kafa karıştırıcı olabilir.

Kötü:

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

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

Iyi:

[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);
}

Birim testi genel yöntemlerine göre özel yöntemleri doğrulama

Çoğu durumda, özel bir yöntemi test etme gereksinimi olmamalıdır. Özel yöntemler bir uygulama ayrıntısıdır ve hiçbir zaman yalıtımlı olarak mevcut olmaz. Bir noktada, uygulamasının bir parçası olarak özel yöntemi çağıran genel kullanıma yönelik bir yöntem olacaktır. Dikkate alınması gereken şey, özel yöntemi çağıran genel yöntemin sonudur.

Aşağıdaki durumu göz önünde bulundurun:

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

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

yönteminin beklendiği gibi çalıştığından emin olmak istediğiniz için ilk tepkiniz için TrimInput bir test yazmaya başlamak olabilir. Ancak, testi beklemediğiniz bir şekilde işleyip sanitizedInput işe yaramayan bir test TrimInput oluşturmak tamamen mümkündürParseLogLine.

Gerçek test, genel kullanıma yönelik yönteme ParseLogLine karşı yapılmalıdır, çünkü sonuçta ilgilenmeniz gereken şey budur.

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

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

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

Bu bakış açısıyla, özel bir yöntem görürseniz genel yöntemi bulun ve testlerinizi bu yönteme karşı yazın. Bir özel yöntemin beklenen sonucu döndürmesi, sonunda özel yöntemi çağıran sistemin sonucu doğru kullandığı anlamına gelmez.

Saplama statik başvuruları

Birim testinin ilkelerinden biri, test altındaki sistemin tam denetimine sahip olması gerektiğidir. Bu ilke, üretim kodu statik başvurulara (örneğin, DateTime.Now) yönelik çağrılar içerdiğinde sorunlu olabilir. Aşağıdaki kodu inceleyin:

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

Bu kod nasıl birim testi yapılabilir? Aşağıdakiler gibi bir yaklaşım deneyebilirsiniz:

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);
}

Ne yazık ki, testlerinizle ilgili birkaç sorun olduğunu hızla fark edersiniz.

  • Test paketi Salı günü çalıştırılırsa, ikinci test geçer, ancak ilk test başarısız olur.
  • Test paketi başka bir gün çalıştırılırsa, ilk test geçer, ancak ikinci test başarısız olur.

Bu sorunları çözmek için üretim kodunuz için bir dikiş eklemeniz gerekir. Bir yaklaşım, bir arabirimde denetlemeniz gereken kodu sarmalayıp üretim kodunun bu arabirime bağımlı olmasını sağlamaktır.

public interface IDateTimeProvider
{
    DayOfWeek DayOfWeek();
}

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

Test paketiniz artık aşağıdaki gibi olur:

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);
}

Artık test paketi üzerinde DateTime.Now tam denetime sahiptir ve yöntemine çağrılırken herhangi bir değeri saplayabilir.