.NET Core ve .NET Standard ile birim testi en iyi .NET Standard

Birim testi yazmanın birçok faydası vardır; regresyona yardımcı olur, belgeler sağlar ve iyi tasarımı kolaylaştırır. Bununla birlikte, birim testlerini okumak ve köprükök etmek, kod tabanınıza zarar ve zarara neden olabilir. Bu makalede .NET Core ve projelerinizi temel alan projelerde birim testi tasarımıyla ilgili en iyi .NET Standard açıklanmıştır.

Bu kılavuzda, testlerinizi daha iyi ve kolay anlaşılır bir şekilde tutmak için birim testleri yazarken en iyi yöntemlerden bazıları hakkında bilgi edineceksiniz.

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

Birim testi neden?

İşlevsel testler gerçekleştirmenin daha az zamanı

İşlevsel testler pahalıdır. Bunlar genellikle uygulamayı açmanızı ve sizin (veya başka birinin) beklenen davranışı doğrulamak için izlemesi gereken bir dizi adımı gerçekleştirmeyi içerir. Bu adımlar testci tarafından her zaman bilinmeyebilir, yani testi gerçekleştirecek olan alanda daha bilgili birine ulaşmaları gerekir. Test etmek, önemsiz değişiklikler için saniyeler veya daha büyük değişiklikler için dakikalar sürebilir. Son olarak, bu işlem sistemde yapılan her değişiklik için tekrarlanır.

Öte yandan birim testleri milisaniye alır, bir düğmeye basıldığında çalıştırabilirsiniz ve sistemin büyük ölçüde bilgi sahibi olması gerekmez. Testin başarılı olup olmadığı veya başarısız olup olmadığı, tek tek test çalıştırıcısına değil, test çalıştırıcısına göredir.

Regresyona karşı koruma

Regresyon hataları, uygulamada değişiklik olduğunda ortaya çıkmış olan hatalardır. Testçilerin yeni özelliklerini test etmekle birlikte daha önce uygulanan özelliklerin beklendiği gibi çalıştığını doğrulamak için önceden var olan özellikleri de test etmek yaygındır.

Birim testi ile, her derlemeden sonra veya bir kod satırı değiştirdikten sonra bile tüm test paketinizi yeniden çalıştırabilirsiniz. Yeni kodunuzun mevcut işlevleri bozmayarak çalışmamanızı sağlar.

Yürütülebilir belgeler

Belirli bir yöntemin ne yaptığı veya belirli bir girişe göre nasıl davran yaptığı her zaman açıkça belli olmayacaktır. Kendinize şu soruyu sorabilirsiniz: Boş bir dize iletirse bu yöntem nasıl davranır? Null?

İyi adlandırılmış bir birim testi paketiniz olduğunda, her test, verilen giriş için beklenen çıkışı net bir şekilde açık bir şekilde açıklayabilir. Ayrıca, gerçekten çalıştığını doğrulayabilecek şekilde de olması gerekir.

Daha az kodla

Kod sıkı bir şekilde bağlı olduğunda, birim testi zor olabilir. Yazmakta olduğunu kod için birim testleri oluşturmadan, eşleştirme daha az belirgin olabilir.

Kodunuz için test yazmak kodunuzu doğal olarak çözecek çünkü aksi takdirde test etmek daha zor olurdu.

İyi bir birim testinin özellikleri

  • Hızlı . Olgun projelerin binlerce birim testine sahip olduğu nadir bir durum değildir. Birim testlerinin çalışması çok az zaman alır. Milisaniye.
  • Yalıtılmış. Birim testleri tek başınadır, yalıtımlı olarak çalışır ve dosya sistemi veya veritabanı gibi dış faktörlere bağımlılıkları yoktur.
  • Yinelenebilir. Birim testi çalıştırmanın sonuçlarıyla tutarlı olması, yani çalıştırmalar arasında herhangi bir değişiklik yapmasanız her zaman aynı sonucu döndüren bir sonuç olması gerekir.
  • Kendi Kendine Denetleme. Test, insan etkileşimi olmadan başarılı veya başarısız olduğunu otomatik olarak algılayana kadar devam eder.
  • Zamanında. Birim testinin, test edilen kodla karşılaştırıldığında yazması orantısız bir şekilde uzun zaman almamalı. Kodun test sınanma süresi, kodun yazılabilirlik süresiyle karşılaştırıldığında çok fazla zaman alı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şkilendirilr. Ancak ölçümün kendisi kodun kalitesini belirleyelememektedir. Aşırı tutkulu bir kod kapsamı yüzdesi hedefinin ayarlayıcı olması etkisiz olabilir. Imagine koşullu dallara sahip karmaşık bir projedir ve %95 kod kapsamı hedefi ayarlayabilirsiniz. Şu anda proje %90 kod kapsamına sahip. Kalan %5'te tüm uç durumlarını hesaba katacak süre çok 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 kalitesine de neden olmaz. Yalnızca birim testlerinin kapsamındaki kod miktarını temsil eder. Daha fazla bilgi için bkz. Birim testi kod kapsamı.

Şimdi aynı dili konuşmaya bakalım

Sahte terimi ne yazık ki testten söz konusu olduğunda sıklıkla kötüye kötüye ifadeleri kullanılmıştır. Aşağıdaki noktalar, birim testleri yazarken en yaygın sahte türleri tanımlar:

Sahte - Sahte, bir saplama veya sahte nesne tanımlamak için kullanılan genel bir terimdir. İster saplama ister sahte olsun, kullanılan bağlama bağlıdır. Başka bir deyişle sahte bir saplama veya sahte olabilir.

Sahte - Sahte nesne, sistemde bir birim testinin geçip geçilme olduğuna veya başarısız olup olmadığını karara varan sahte bir nesnedir. Sahte, onay yapılana kadar Sahte olarak başlar.

Saplama - Saplama, sistemde var olan bir bağımlılığın (veya ortak çalışanın) yerini alan, kontrol edilebilir bir değişikliktir. Saplama kullanarak, doğrudan bağımlılıkla ilgilenmeden kodunuzu testebilirsiniz. Varsayılan olarak, bir saplama sahte olarak başlar.

Aşağıdaki kod parçacığını düşünün:

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

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

Bu, sahte olarak adlandırılan bir saplama örneği olabilir. Bu durumda, bir saplamadır. Yalnızca örneği (test altındaki sistem) için Sipariş'i bir araç Purchase olarak geçirmeniz gerekir. Ad MockOrder da yanıltıcıdır çünkü yine sipariş sahte değildir.

Daha iyi bir yaklaşım şu şekilde olacaktır:

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

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

sınıfını olarak yeniden anarak, sınıfı çok daha genel hale yapmış olur, sınıfı sahte veya FakeOrder saplama olarak kullanılabilir. Test için hangisi daha iyidir? Yukarıdaki örnekte FakeOrder saplama olarak kullanılmıştır. Onay sırasında herhangi bir FakeOrder şekilde veya formda kullanmazsanız. FakeOrder , oluşturucu Purchase gereksinimlerini karşılamak için sınıfına geçirildi.

Bunu Sahte olarak kullanmak için aşağıdakine benzer bir şey de kullanabilirsiniz

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

purchase.ValidateOrders();

Assert.True(mockOrder.Validated);

Bu durumda, Fake üzerindeki bir özelliği (onaylar) kontrol edersiniz, bu nedenle yukarıdaki kod parçacığında mockOrder bir Sahtedir.

Önemli

Bu terminolojiyi doğru yapmak önemlidir. Saplamalarınızı "sahte" olarak çağırsanız, diğer geliştiriciler amacınız hakkında yanlış varsayımlar yapacaktır.

Sahteler ve saplamalar hakkında anımsanacak temel şey, sahtelerin saplamalara benzer, ancak sahte nesneye karşı onaylarken, bir saplama için onaylamazsanız.

En iyi uygulamalar

Birim testleri yazarken altyapıya bağımlılıklar tanıtmamaya çalışma. Bunlar testleri yavaşlatır ve yavaşlatır ve tümleştirme testleri için ayrılmış olması gerekir. Açık Bağımlılıklar İlkesini takip edin ve Bağımlılık Ekleme kullanarak uygulamanıza bu bağımlılıklardan kaçınabilirsiniz. Birim testlerinizi tümleştirme testlerinden ayrı bir projede de tutarak. Bu, birim testi projenizin altyapı paketlerine başvuru veya bağımlılıklara sahip olmadığını sağlar.

Testlerinizi adlandırma

Testin adı üç bölümden oluşur:

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

Neden?

  • Adlandırma standartları, testin amacını açıkça ifade etmelerinden dolayı önemlidir.

Testler, kodunuzun çalışır olduğundan emin olmaktan fazlasıdır ve belgeler de sağlar. Birim testleri paketine bakarak kodun kendisine bakmadan kodunuzun davranışını çıkarabilirsiniz. Ayrıca, testler başarısız olduğunda tam olarak hangi senaryoların beklentilerinizi karşılamaz olduğunu da görüyorsunuz.

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, Harekete Geç, Onay birim testi sırasında yaygın kullanılan bir desendir. Addan da anlaşılacağı gibi üç ana eylemden oluşur:

  • Nesnelerinizi düzen düzenerek, gerektiğinde bunları oluşturur ve ayarlar.
  • Bir nesne üzerinde hareket eder.
  • Bir şeyin beklendiği gibi olduğunu onaylar.

Neden?

  • Test edilenleri düzenleme ve onaylama adımlarından açıkça birbirinden ayırıyor.
  • "Eylem" koduyla onayları birbirine katma şansı daha azdır.

Okunabilirlik, test yazarken en önemli yönlerden birisidir. Testte bu eylemlerin her biri birbirinden ayırmak, kodunuzu çağırmanız için gereken bağımlılıkları, kodunuzun nasıl çağrıldısını ve neyi onaylamaya çalıştığınızı net bir şekilde vurgular. Bazı adımları birleştirmek ve test boyutunu azaltmak mümkün olabilir, ancak asıl amaç testi mümkün olduğunca okunabilir yapmaktır.

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

En az başarılı testler yazma

Birim testinde kullanılacak giriş, şu anda testte olduğunu doğrulamak için mümkün olan en basit giriştir.

Neden?

  • Testler, kod tabanındaki gelecekteki değişikliklere karşı daha güçlü hale geldi.
  • Uygulama üzerinde test davranışına daha yakın.

Testin başarılı olması için gerekenden daha fazla bilgi içeren testlerin teste hata oluşturma şansı daha yüksektir ve testin amacını daha az net bir şekilde ifade eder. Test yazarken davranışa odaklanmak istersiniz. Modellerde ek özellikler ayarlama veya gerekli olmayan değerler kullanma, yalnızca kanıtlamaya çalıştıkları değerin gerisini verir.

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 testlerinde değişkenleri adlandırmak, üretim kodundaki değişkenleri adlandırmak kadar önemlidir. Birim testleri sihirli dizeler içermeli.

Neden?

  • Değerin özel olduğunu an etmek için test okuyucuslarının üretim kodunu incelemesini önler.
  • gerçekleştirmeye değil kanıtlamaya çalıştığın şeyi açıkça gösterir.

Sihirli dizeler testlerinizi okuyucunun kafa karışıklığına neden olabilir. Bir dize normalin dışında görünüyorsa, bir parametre veya dönüş değeri için belirli bir değerin neden seçiliyor olduğunu merak ediyor olabilir. Bu, 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 etme amacını benimsersiniz. Sihirli dizeler söz dizisinde 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çının

Birim testlerinizi yazarken, el ile dize birleşebilir ve , , , vb. gibi if mantıksal koşulları while for switch önlersiniz.

Neden?

  • Testlerinizi içinde hataya neden olma şansı daha azdır.
  • Uygulama ayrıntılarına değil, son sonuça odaklanın.

Test paketinize mantık ekleyebilirsiniz, buna bir hata tanıtma şansı önemli ölçüde artar. Hata bulmak istediğiniz son yer test paketinizin içindedir. Testlerinizi çalışır durumda olacak şekilde yüksek bir güven düzeyine sahip olur, aksi takdirde onlara güvenmezsiniz. Güvenmezsiniz, hiçbir değer sağlamaz. Bir test başarısız olduğunda, kodunda gerçekten bir sorun olduğunu ve yoksayılamayacak bir şey olduğunu anlamanız gerekir.

İpucu

Testte mantık kaçınılmaz görünüyorsa, testi iki veya daha fazla farklı teste bölmeyi düşünün.

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 yok etmek için yardımcı yöntemleri tercih edin

Testleriniz için benzer bir nesne veya durum gerekirse, varsa ve özniteliklerden yararlanarak daha yardımcı Setup Teardown bir yöntem tercih edin.

Neden?

  • Tüm kod her testten görünür olduğu için testleri okurken daha az karışıklık.
  • Verilen test için çok fazla veya çok az ayarlama ihtimali düşük.
  • Testler arasında durum paylaşma ihtimali daha azdır ve bu da aralarında istenmeyen bağımlılıklar oluşturur.

Birim testi çerçeveleri içinde, Setup test paketinizin içindeki her birim testi öncesinde çağrılır. Bazıları bunu yararlı bir araç olarak görse de genellikle bu araç şişirilmiş ve testleri okumak zor olur. Her testin, testi çalıştırmaya devam etmek için genellikle farklı gereksinimleri vardır. Ne yazık Setup ki, sizi her test için tam olarak aynı gereksinimleri kullanmaya karşı kullanır.

Not

xUnit, sürüm 2.x'den sonra 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 Act dahil etmeye deneyin. Tek bir eylemi kullanmaya yönelik yaygın yaklaşımlar şunlardır:

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

Neden?

  • Test başarısız olduğunda hangi Eylemin başarısız olduğu net değildir.
  • Testin yalnızca tek bir çalışma durumuna odaklanmasına dikkati sağlar.
  • Testlerin neden başarısız olduğuyla ilgili tüm resmi verir.

Birden Çok Eylemin tek tek Onaylandı olması gerekir ve tüm Onayların yürütülecek olduğu garanti edilemez. Çoğu birim testi çerçevesinde, bir birim testinde Onay başarısız olduğunda devam etme testleri otomatik olarak başarısız olarak kabul edilir. Aslında çalışan işlevler başarısız olarak gösterildiği için bu 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öntemleriyle özel yöntemleri doğrulama

Çoğu durumda, özel bir yöntemi test etmek zorunda değildir. Özel yöntemler bir uygulama ayrıntılarıdır. Bunu şu şekilde düşünebilirsiniz: özel yöntemler hiçbir zaman yalıtken mevcut değildir. Bir noktada, uygulamasının bir parçası olarak özel yöntemi çağıran genel bir yöntem olacak. Dikkatniz gereken şey, özel yönteme çağrıda bulunduran genel yöntemin sonucu.

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

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

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

Yöntemin beklendiği gibi çalıştığından emin olmak istediğiniz için ilk tepkiniz için test TrimInput yazmaya başlamak olabilir. Ancak, beklemeyersiniz bir şekilde işlemek ve gereksiz bir test işlemek ParseLogLine sanitizedInput tamamen TrimInput mümkündür.

Gerçek test genele yönelik yönteme göre yapılmalı ParseLogLine çünkü sonunda bu konuda ilgilenmeniz gerekir.

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üyorsanız genel yöntemi bulun ve testlerinizi bu yönteme göre yazın. Özel bir yöntemin beklenen sonucu döndüren olması, sonunda özel yöntemi çağıran sistemin sonucu doğru kullandığı anlamına gelir.

Statik başvuruları saplama

Birim testinin ilkelerinden biri, test kapsamında sistem üzerinde tam denetime sahip olmasıdır. Üretim kodu statik başvurulara çağrılar (örneğin, ) dahil olduğunda bu soruna neden DateTime.Now olabilir. Aşağıdaki kodu düşünün

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

Bu kod nasıl birim test edilebilir? Aşağıdakiler gibi bir yaklaşım denemesi

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 testlerle ilgili birkaç sorun olduğunu hemen fark edesiniz.

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

Bu sorunları çözmek için üretim kodunuzla bir bütün haline dönüştürebilirsiniz. Bir yaklaşım, bir arabirimde denetim yapmanız gereken kodu sarmanın ve üretim kodunun bu arabirime bağlı olmasını kullanmaktı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

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 paketinin üzerinde tam denetimi vardır DateTime.Now ve yöntemine çağrı yapıldığında herhangi bir değer bulunabilir.