Unit Testi

Unit testi tek metodun veya kısa bir mantıksal akışı test etmek için kullanılır. Sistemin tamamını test etmek yerine, unit testi sahte veriler kullanarak gerçek metodun yapması gereken işi doğru yapıp yapmadığını test eder.

Örneğin TodoController'in iki bağımlılığı vardır. Bunlar : ItodoItemService ve UserManager'dır. TodoItemService ise ApplicationDbContext'e bağımlıdır. ( Şu şekilde bağlımlılık grafiğini çizebiliriz. TodoController->TodoItemService->ApplicationDbContext)

Normalde ASP.Core bağımlılık enjeksiyon sistemi TodoController veya TodoItemService oluşturulduğundayukarıda bulunan tüm bağımlılık grafiğini enjekte eder.

Fakat unit tesi yazarken bu enjeksiyonları elle yapmak gerekmektedir. Bu da mantığı sınıf veya method bazında izole edebileceğiniz anlamına gelir. (Test yazarken yanlışlıkla veri tabanına yazmak istemezsiniz.)

Test Projesi Oluşturma

Test için genelde ayrı bir proje oluşturulur. Yeni test projesi eskiden oluşturduğunuz ana projenin yanına ( içine değil) oluşturulur.

Eğer şu anda projenizin ana dizinindeyseniz cd ile bir üst klasöre çıkın. Sonra aşağıdaki komutları uygulayarak yeni bir test projesi oluşturun.

mkdir AspNetCoreTodo.UnitTests
cd AspNetCoreTodo.UnitTests
dotnet new xunit

xUnit.NET unit ve entegrasyon testi için kullanılan popüler bir test iskelettir. Her projemizde olduğu gibi bu proje de aslında NuGet paketlerinden oluşur. dotnet new xunit test için gerekli olan herşeyi sizin için oluşturur.

Klasör yapınız şu anda aşağıdaki gibi olmalı:

AspNetCoreTodo/
    AspNetCoreTodo/
        AspNetCoreTodo.csproj
        Controllers/
        (etc...)

    AspNetCoreTodo.UnitTests/
        AspNetCoreTodo.UnitTests.csproj

Test projesinde kullanacağınız sınıflar ana projede olduğundan dolayı bunları ana projeye atfetmeniz gerekmekte.

dotnet add reference ../AspNetCoreTodo/AspNetCoreTodo.csproj

Ardından UnitTest1.cs dosyasınız silin. Artık test yazmaya hazırsınız.

Servis testi Yazma

TodoItemService içindeki AddItemAsync metodunun mantığına bakın:

public async Task<bool> AddItemAsync(NewTodoItem newItem, ApplicationUser user)
{
    var entity = new TodoItem
    {
        Id = Guid.NewGuid(),
        OwnerId = user.Id,
        IsDone = false,
        Title = newItem.Title,
        DueAt = DateTimeOffset.Now.AddDays(3)
    };

    _context.Items.Add(entity);

    var saveResult = await _context.SaveChangesAsync();
    return saveResult == 1;
}

Bu metod gelen verilere bakarak yeni bir nesne üretir ve bunu veri tabanına yazar.

  • OwnerId giriş yapmış kullanının ID'si olmalı.

  • Yeni madde her zaman başlangıçta tamamlanmamış olmalı. (IsDone = false)

  • Başlık gelen objeden kopyalanmalı : newItem.Title

  • Her yeni gelen madde bitiş zamanı(DueAt) 3 gün sonra olarak ayarlanmalı.

Bu tipte alınan karalara iş mantığı(Business Logic) denir. Tabi bunun yanında iş mantığı hesaplamaları da içerir.

Bu karalar mantığın oluşmasına yardımcı olur. Örneğin başka birisi AddItemAsync içerisinde değişiklik yapıyor fakat daha önceki kararlardan haberi yok. Bu basit servisler için problem oluşturmayabilir. Fakat büyük servislerde bunun problem oluşturduğunu söylenebilir.

TodoItemService metodunu test eden sınıfı yeni oluşturduğunuz projede şu şekilde oluşturun:

AspNetCoreTodo.UnitTests/TodoItemServiceShould.cs

using System.Threading.Tasks;
using AspNetCoreTodo.Data;
using AspNetCoreTodo.Models;
using AspNetCoreTodo.Services;
using Microsoft.EntityFrameworkCore;
using Xunit;

namespace AspNetCoreTodo.UnitTests
{
    public class TodoItemServiceShould
    {
        [Fact]
        public async Task AddNewItem()
        {
            // ...
        }
    }
}

[Fact] özelliği xUnit.NET paketinden gelmektedir, ve bu özelliği ekleyerek bu metodun test metodu olduğunu belirtmiş oldunuz.

Testleri isimlendirmek için bir çok yöntem bulunmaktadır. Hepsinin pozitif ve negatif yanları vardır. Bu projede sondan ekleme ile test sınıflarını oluşturacaksınız. Örnek TodoItemServiceShould, elbette siz istediğiniz yöntemi kullanabilirsiniz.

TodoItemService ApplicationDbContext'e bağımlıdır, yani canlıda bulunan veri tabanına. Bunu test için kullanmak doğru bir yöntem değildir. Bunun yerine Entity İskeletinde bulunan in-memory veri tabanı sağlayıcısını kullanabilirsiniz. Tüm veri tabanı hafıza'da yer aldığından. Test her başladığında temizlenir. Fakat TodoItemService bunun farkını anlamayacaktır.

DbContextOptionsBuilder ile in-memory veri tabanı oluşturup sonrasında AddItem'a çağrı yapabilirsiniz.

var options = new DbContextOptionsBuilder<ApplicationDbContext>()
    .UseInMemoryDatabase(databaseName: "Test_AddNewItem").Options;

// Set up a context (connection to the DB) for writing
using (var inMemoryContext = new ApplicationDbContext(options))
{
    var service = new TodoItemService(inMemoryContext);

    var fakeUser = new ApplicationUser
    {
        Id = "fake-000",
        UserName = "fake@fake"
    };

    await service.AddItemAsync(new NewTodoItem { Title = "Testing?" }, fakeUser);
}

Son satır Testing? adında yeni bir yapılacak maddesi ekler ve in-memory veri tabanına bunu kaydetmesini söyler. Bu iş mantığının doğru çalışabilmesi için kaydılan veriyi veri tabanından almalı ve kontrol etmelisiniz.

// Use a separate context to read the data back from the DB
using (var inMemoryContext = new ApplicationDbContext(options))
{
    Assert.Equal(1, await inMemoryContext.Items.CountAsync());

    var item = await inMemoryContext.Items.FirstAsync();
    Assert.Equal("Testing?", item.Title);
    Assert.Equal(false, item.IsDone);
    Assert.True(DateTimeOffset.Now.AddDays(3) - item.DueAt < TimeSpan.FromSeconds(1));
}

Öncelikle mantık testi: Sadece 1 tane veri olmalı, sonrasında bu verileri FirstAsync ile almalı ve beklenen değerler uygunmu bu kontrol edilmeli.

Tarihi kontrol etmek biraz çetrefilli, iki tarihin eşitli milisaniye bazında neredeyse her zaman yanlış olacağından. Bunu aralık kontrolç olarak yapmak daya bantıklıdır. Son satırda bunu görebilirsiniz.

Hem unit testi hem de entegrasyon testi AAA kalıbını takip eder: objeler ve veriler önce ayarlanır, sonra bazı işlemler yapılır ve son olarak Assert ile bunların kontrolleri, beklenen değerleri karşılayıp karşışamadıkları kontrol edilir.

AddNewItem'ın son hali:

AspNetCoreTodo.UnitTests/TodoItemServiceShould.cs

public class TodoItemServiceShould
{
    [Fact]
    public async Task AddNewItem()
    {
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseInMemoryDatabase(databaseName: "Test_AddNewItem")
            .Options;

        // Set up a context (connection to the DB) for writing
        using (var inMemoryContext = new ApplicationDbContext(options))
        {
            var service = new TodoItemService(inMemoryContext);
            await service.AddItemAsync(new NewTodoItem { Title = "Testing?" }, null);
        }

        // Use a separate context to read the data back from the DB
        using (var inMemoryContext = new ApplicationDbContext(options))
        {
            Assert.Equal(1, await inMemoryContext.Items.CountAsync());

            var item = await inMemoryContext.Items.FirstAsync();
            Assert.Equal("Testing?", item.Title);
            Assert.Equal(false, item.IsDone);
            Assert.True(DateTimeOffset.Now.AddDays(3) - item.DueAt < TimeSpan.FromSeconds(1));
        }
    }
}

Testi çalıştırma

Terminalde ( şu anda AspNetCoreTodo.UnitTests klasöründe olduğunuzu varsayarak) aşağıdaki kodu çalıştırın.

dotnet test

Test komutu tüm projeyi kontrol ederk test olarak tanımladığınız ( [Fact]) metodları veya sınıfları bulur ve aşağıdaki gibi bir çıktı veririr.

Starting test execution, please wait...
[xUnit.net 00:00:00.7595476]   Discovering: AspNetCoreTodo.UnitTests
[xUnit.net 00:00:00.8511683]   Discovered:  AspNetCoreTodo.UnitTests
[xUnit.net 00:00:00.9222450]   Starting:    AspNetCoreTodo.UnitTests
[xUnit.net 00:00:01.3862430]   Finished:    AspNetCoreTodo.UnitTests

Total tests: 1. Passed: 1. Failed: 0. Skipped: 0.
Test Run Successful.
Test execution time: 1.9074 Seconds

TodoItemService'i kapsayan (Coverage) bir test sunmuş oldunuz. Artık aşağıdaki testleri yazabilirsiniz.

  • MarkDoneAsync eğer paslanan ID yok ise false dönsün.

  • MarkDoneAsync eğer gerçekten var olan bir maddeyi tamamlandı olarak işaretler ise true dönsün.

  • GetIncompleteItemsAsync sadece belirli bir kullanıcıya ait verileri döndürür.

Last updated