11 Nisan 2020 Cumartesi

Birim(Unit) Test ile Veri Kümeleri - xUnit (Inline - Member - Class Data)

    S.A. Arkadaşlar,
    Bugün unit/xunit (birim) test ile bir konuya odaklanmak istedim. Bu konunun amacı, birim test nedir? Neden yazılır? Faydaları nelerdir? vb. gibi bilgiler olmayacak. Odaklanacağımız konu birim testler de çoğu zaman olması gereken girdi değerleri üzerine yoğunlaşacağız. Bazen bir kereye mahsus bir sabit değer, bazen de birden fazla ve farklı değerler girdi ihtiyacı olabilmektedir. Şimdi adım adım olması gerekene doğru ilerlemeye çalışalım, buyurun birlikte ilerleyelim..
   En basit haliyle elimizde toplama işlemi yapan bir metod var ve biz bu metodu test etmek istiyoruz.  Bunun için öncelikle sınıflarımızı oluşturalım. Daha sonra bunlar üzerinden adım adım ilerleyeceğiz.
public class Calculator
{
    public int Add(int value1, int value2)
    {
        return value1 + value2;
    }
}

     Şimdi ise test sınıfımızı oluşturalım.
public class CalculatorTests
{
    [Fact] //xunit ile bu metodun test metodu olduğunu belirtiyoruz.
    public void CanAdd()
    {
        var calculator = new Calculator();

        int value1 = 1;
        int value2 = 2;

        var result = calculator.Add(value1, value2);

        Assert.Equal(3, result);
    }
}

      Yukarıda görmüş olduğunuz basit bir örnektir. Verilen girdilere göre, belirlenen çıktı oluşacak mı sorusunun cevabını veriyor. Peki biz buna birden çok değer girmek istersek ne yapacaktık. Belki de en basiti ve ilk akla gelen, direk olarak girdileri içine yazmak ve kodu uzatmak, ama bu tahmin edileceği üzere çok doğru bir yöntem değildir. O zaman ne yapmalı?

[Theory]//Farklı değerler gireceğini belirtir. Tek başına kullanılmaz.
[InlineData(1, 2, 3)] // bu değerlerin her birini parametre olarak girer.
[InlineData(-4, -6, -10)] // -4 ile -6 toplanır -10 bulunur.
[InlineData(-2, 2, 0)] // farklı sayıda parametre girmek hata verir.
[InlineData(int.MinValue, -1, int.MaxValue)]
public void CanAddTheory(int value1, int value2, int expected)
{
    var calculator = new Calculator();

    var result = calculator.Add(value1, value2);

    Assert.Equal(expected, result);
}

      Açıkçasını söylemek gerekirse böyle çok daha güzel olmadı mı? Bence oldu, fakat bu satırların daha da arttığını varsayarak iyice uzayacaktır ve tekrardan göze kötü görünmeye başlayacaktır. Bunun için başka bir çözüm var mı peki?
[Theory]
[MemberData(typeof(Data))] // kullanılacak yapının adı belirlenir.
public void CanAddTheoryClassData(int value1, int value2, int expected)
{
    var calculator = new Calculator();

    var result = calculator.Add(value1, value2);

    Assert.Equal(expected, result);
}
public static IEnumerable<object[]> Data =>
   new List<object[]>
   {
        new object[] { 1, 2, 3 },
        new object[] { -4, -6, -10 },
        new object[] { -2, 2, 0 },
        new object[] { int.MinValue, -1, int.MaxValue }
   };
}

       Şimdi burada çok bir şey değişmemiş gibi gözükse de kod okunurluğu açısından çok daha güzel olacaktır. Şöyle ki, kodun bu kısmını aşağıya alıp testler arasında daha rahat dolaşabilirsiniz. Bir tık sanki daha güzel oldu diye düşünüyorum.

     Şuan benim yazıyı asıl yazmama vesile olan kısma geldik :) Dikkat ediyorsanız parametreler hep int tipinde oluyor. Her hangi referans bazlı bir değişken tanımlarsanız hata alacaksınız, fakat bu benim için yeterli değil. Ben parametre olarak bir obje geçmek istiyorum. O zaman şimdi ne yapmamız lazım?

public class Response
{
   public bool IsValid { get; set; }
   public List<string> Errors { get; set; }
}
public class Request
{
   public Address Address { get; set; }
   public Phone Phone { get; set; }
}
public class Service
{
    public Response Validate(Request request)
    {
        var response = new Response()
        {
            Errors = new List<string>()
        };
        if (request == null)
        {
            response.Errors.Add("Request cannot be null");
        }
        else if (request.Address == null)
        {
            response.Errors.Add("Request cannot be null");
        }
        else if (request.Phone == null)
        {
            response.Errors.Add("Request cannot be null");
        }
        response.IsValid = response.Errors.Count == 0;

        return response;
    }
}
[Theory]
[ClassData(typeof(Data))] // Sınıfın tipi belirlenir.
public void TestValidateRequestNull(AllocateAndGetLabelRequest request)
{
    var service = new Service();
    var response = service.Validate(request);
    Assert.False(response.IsValid);
}
public class Data : IEnumerable<object[]>
{
    public IEnumerator<object[]> GetEnumerator()
    {
        yield return new object[]
        {
               null
        };
        yield return new object[]
        {
                new Request{ Address = null }
        };

        yield return new object[]
        {
                new Request{ Address = new Address(), Phone = null }
        };
    }
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
   
     Bu kodları yazarken üşenmedim desem yalan olur :) ama en güzel bu şekilde anlaşılacağını düşündüm. Çünkü bazen gerçek hayat senaryoları ile örneklerde anlatılan örnekler çok farklı olabiliyor. Benim karşılaştığım problemi kısaca özetleyeyim. Elimde örnektekinden çok daha büyük bir request objesi var. Bunun içinde de bir çok farklı obje var. Birim testin mantığı da her bir koda tek tek girip sarmalanmamış (coverge edilmemiş) bir yer bırakmaması üzerine kuruludur. Service sınıfında Validate metodu içinde ben de bunu yapıyorum ve her bir objeyi kontrol etmem lazım. Kontrol ettiklerim için de birim testlerini yazmam lazım. Bunu yukarıda belirttiğim ama içime sinmeyen şekillerde de yapabilirdim, fakat güzel bir yapıya geçirmenin daha uygun olduğunu düşündüm.

    Bunun ayrıca şöyle bir avantajı da var. Bu sınıfımızı istersek farklı bir data klasör altına alıp bütün deneme verilerini oraya alarak belirli bir düzen oturtabiliriz. Böyle birim test içindeki bu tarz kodları silerek okunurluğu artırabilir, daha derli toplu bir yapıya kavuşabiliriz.

    Tekrardan birim testimiz dönecek olursak, üzerinde ClassData attributi tanımlanmış ve o da Data sınıfını gösteriyor. Data sınıfı içindeki objeyi her bir if yapısına girecek şekilde değer ataması yaptım. Sırayla if'leri tek tek gezecek ve bu şekilde kod daha iyi test edilmiş olacaktır.

    En başta da dediğim gibi bunu yapmanın bir sürü yöntemi var, ama daha güzel bir yapı olduğuna inandığım için bu şekilde yapmayı tercih ettim. Tabi ki daha farklı çözümlere de açığım.

    Unit ve Integration testlerle ilgili daha fazlası için videomu inceleyebilirsiniz.

    Test edilebilirliği yüksek sistemler kurmak dileğiyle.

Hiç yorum yok:

Yorum Gönder