Caner Tosuner

Leave your code better than you found it

Asp.Net Core Web Api Integration Test Nedir Nasıl Yazılır

Unit test veya integration test projenizde bulunan business logic'lerin belirtilen input'lar çerçevesinde ne şekilde çalışması gerektiğini garanti ettiğimiz yapılar olup projelerimiz için oldukça önemli bir Must layer olmaktadır. Daha önceki yazılarımızda unit test konusuna değinip örnek proje üzerinde işlemiştik. Bu yazımızın konusu ise asp.net core uygulamarı için integration test nasıl yazılır.

Integration test; projenizde yer alan birden fazla modülün belirtilen input'lar ile beklendiği şekilde çalışmasını test eden yapıdır. Diğer bir deyişle, bir biri ile bağlı şekilde çalışan modüler yapıları ve onların sahip oldukları business logic'leri toplu bir şekilde ele alıp doğruluğunu veya yanlışlığını test etmemizi sağlayan yapıdır.

Örnek olarak verecek olursak; bir CustomerApi projemiz var end-point'ler üzerinden database'de crud işlemleri yapmakta. Bu end-point'lerin testini uçtan uca request handle'dan başlayıp business-layer'da yer alan logic'lerin bulunduğu metodları ve repository-layer üzerinden database'de ki ilgili column'ların doğruluğuna kadar olan bu birbirine bağlı iki sistemin (database & api) uçtan uca kontrollerini integration-test yazarak yapabiliriz.

Daha önceki unit test yazımızda yaptığımız örnek üzerinden ilerleyelim. O yazımızdaki örnekte asp.net core 2.1 kullanarak CustomerApi adında bir proje oluşturup unit testlerini yazmıştık. Yine aynı örnek üzerinden integration test projeleri oluşurup test metotlarımızı yazalım.

Solution açıldıktan sonra test klasörü içerisine Customer.Api.IntegrationTest adında bir xUnit test projesi oluşturalım

 

CustomerController.cs içerisinde CRUD işlemlerini yapan metotlarımız mevcut ve sırasıyla bu metotlar için integration test metotlarını yazacağımız CustomerControllerTests.cs sınıfını oluşturalım.

Asp..net core ile birlikte TestServer.cs diye bir sınıf hayatımıza girdi ve bu sınıf ile birlikte tıpkı gerçekten iis veya kestrel'de bir web-api uygulaması host edermiş gibi CustomerApi uygulamamızı integration testler için bir stup projesi ayağa kaldırmamıza olanak sunan bir sınıftır.

Bu sınıfı kullanabilmek için nuget üzerinden Microsoft.AspNetCore.TestHost kütüphanesini projemiz referanslarına ekleyelim. Kurulum tamamladıktan sonra constructor içerisinde aşağıdaki gibi testServer ve rest-call'ları yapmamızı sağlayacak olan httpClient imp. yapalım.

public class CustomerControllerTests
{
    private readonly HttpClient _client;

    public CustomerControllerTests()
    {
       var testServer = new TestServer(new WebHostBuilder()
        .UseStartup<TestStartup>()
        .UseEnvironment("Development"));
       _client = testServer.CreateClient();
    }
}

TestServer sınıfının initialize olması için IWebHostBuilder'a ihtiyacı var ve CustomerApi projemizde bulunan Startup.cs sınıfı uygulamayı ayağa kaldırırken bize bir IWebHostBuilder return etmekte. Bizimde amacımız bi CustomerApi projesi run etmek olduğundan IntegrationTest projemizin referanslarına CustomerApi projesini ekleyip Startup.cs'i kullanması gerektiğini belirtmek. Ancak mevcut test/qa database'ini kullanmak istemiyoruz, o database'i dirty data ile doldurmak işimize gelmez. Bunun için TestStartup adında Startup.cs den türeyen bir sınıf oluşturarak test için Entity Framework InMemoryDatabase özelliğini kullanabilmesini sağlayalım.

public class TestStartup : Startup
{
    public TestStartup(IConfiguration configuration) : base(configuration)
    {  }

    public override void ConfigureDatabase(IServiceCollection services)
    {
        services.AddDbContext<CustomerDbContext>(options =>
            options.UseInMemoryDatabase("customerDb_test"));
    }
}

İlk olarak httpPost isteği alarak customerInsert işlemi yapan end-point için test metodumuzu yazalım.

[Fact]
public async Task Post_Should_Return_OK_With_Empty_Response_When_Insert_Success()
{
    var expectedResult = string.Empty;
    var expectedStatusCode = HttpStatusCode.OK;

    // Arrange
    var request = new CustomerDto
    {
        FullName = "Caner Tosuner",
        CityCode = "Ist",
        BirthDate = new DateTime(1990, 1, 1)
    };
    var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");

    // Act
    var response = await _client.PostAsync("/api/customer", content);

    var actualStatusCode = response.StatusCode;
    var actualResult = await response.Content.ReadAsStringAsync();

    // Assert
    Assert.Equal(expectedResult, actualResult);
    Assert.Equal(expectedStatusCode, actualStatusCode);
}

FullName alanı boş gönderildiğinde throw edilen exception için integrationtest metodunu aşağıdaki gibi yazalım.

[Fact]
public async Task Post_Should_Return_FAIL_With_Error_Response_When_Insert_FullName_Is_Empty()
{
    var expected = "Fields are not valid to create a new customer.";

    // Arrange
    var request = new CustomerDto
    {
        FullName = string.Empty,
        CityCode = "Ist",
        BirthDate = new DateTime(1990, 1, 1)
    };
    var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");

    // Act
    var exception = await Assert.ThrowsAsync<Exception>(async () => await _client.PostAsync("/api/customer", content));
    
    // Assert
    Assert.Equal(expected, exception.Message);
}

Şimdi ise biraz daha karmaşık bir test metodu yazalım. Insert işlemi success olduktan sonra getAll metoduna call yaparak dönen listede bir adım önce insert ettiğimiz customer'ın olduğunu test eden metodu aşağıdaki gibi yazalım.

[Theory]
[InlineData("/api/customer", "/api/customer")]
public async Task Get_Should_Return_OK_With_Inserted_Customer_When_Insert_Success(string postUrl,string getUrl)
{
    var expectedResult = string.Empty;
    var expectedStatusCode = HttpStatusCode.OK;

    // Arrange-1
    var request = new CustomerDto
    {
        FullName = "Caner Tosuner",
        CityCode = "Ist",
        BirthDate = new DateTime(1990, 1, 1)
    };
    var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");

    // Act-1
    var response = await _client.PostAsync(postUrl, content);

    var actualStatusCode = response.StatusCode;
    var actualResult = await response.Content.ReadAsStringAsync();

    // Assert-1
    Assert.Equal(expectedResult, actualResult);
    Assert.Equal(expectedStatusCode, actualStatusCode);


            
    // Act-2
    var responseGet = await _client.GetAsync(getUrl);

    var actualGetResult = await responseGet.Content.ReadAsStringAsync();
    var getResultList = JsonConvert.DeserializeObject<List<CustomerDto>>(actualGetResult);

    var insertedCustomer = getResultList.Any(c => c.FullName == request.FullName);

    // Assert-2
    Assert.NotEmpty(getResultList);
    Assert.True(insertedCustomer);
}

Bütün api end-point'leri için ise aşağıdaki gibi bir test metodu yazarak sırasıyla insert, getAll, update, getbycityCode metotlarını uçtan uca olan integration testimizi yazabiliriz.

[Theory]
[InlineData("/api/customer", "/api/customer")]
public async Task Insert_GetAll_Update_GetByCityCode_Should_Return_Expected_Result(string postUrl, string getUrl)
{
    #region Insert
    var expectedResult = string.Empty;
    var expectedStatusCode = HttpStatusCode.OK;

    // Arrange-1
    var request = new CustomerDto
    {
        FullName = "Caner Tosuner",
        CityCode = "Ist",
        BirthDate = new DateTime(1990, 1, 1)
    };
    var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");

    // Act-1
    var response = await _client.PostAsync(postUrl, content);

    var actualStatusCode = response.StatusCode;
    var actualResult = await response.Content.ReadAsStringAsync();

    // Assert-1
    Assert.Equal(expectedResult, actualResult);
    Assert.Equal(expectedStatusCode, actualStatusCode);
    #endregion



    #region GetAll
    // Act-2
    var responseGet = await _client.GetAsync(getUrl);
    responseGet.EnsureSuccessStatusCode();

    var actualGetResult = await responseGet.Content.ReadAsStringAsync();
    var getResultList = JsonConvert.DeserializeObject<List<CustomerDto>>(actualGetResult);

    var insertedCustomerExist = getResultList.Any(c => c.CityCode == request.CityCode);

    // Assert-2
    Assert.NotEmpty(getResultList);
    Assert.True(insertedCustomerExist);
    #endregion



    #region Update
    // Arrange-3
    var insertedCustomer = getResultList.Single(c => c.CityCode == request.CityCode);
    var requestUpdate = new CustomerDto
    {
        FullName = "Ali Tosuner",
        CityCode = "Ist",
        BirthDate = new DateTime(1994, 1, 1),
        Id = insertedCustomer.Id
    };
    var contentUpdate = new StringContent(JsonConvert.SerializeObject(requestUpdate), Encoding.UTF8, "application/json");

    // Act-3
    var responseUpdate = await _client.PutAsync(postUrl, contentUpdate);
    responseUpdate.EnsureSuccessStatusCode();
    var updateActualResult = await responseUpdate.Content.ReadAsAsync<CustomerDto>();

    // Assert-3
    Assert.Equal(updateActualResult.FullName, requestUpdate.FullName);
    #endregion



    #region GetByCityCode
    // Act-2
    var responseGetByCityCode = await _client.GetAsync("/api/customer/getbycitycode/"+requestUpdate.CityCode);
    responseGetByCityCode.EnsureSuccessStatusCode();

    var actualGetByCityCodeResult = await responseGetByCityCode.Content.ReadAsStringAsync();
    var getByCityCodeResultList = JsonConvert.DeserializeObject<List<CustomerDto>>(actualGetByCityCodeResult);

    var updatedCustomerExist = getByCityCodeResultList.Any(c => c.CityCode == request.CityCode);

    // Assert-2
    Assert.NotEmpty(getByCityCodeResultList);
    Assert.True(updatedCustomerExist);
    #endregion

}

Yazdığımız bütün testleri run ettiğimizde ise aşağıdaki gibi hepsinin pass statüsünde olduğunu görebiliriz.

Yazının başında da belirtiğimiz gibi test yazmak projelerimiz için oldukça önemlidir. Birbirine bağlı yapılar için integration test yazarak uçtan uca olan bütün process'i test edip less bug, more refactoring için çok önemli bir adım atmış oluruz. Tabi bunu sadece integration test yazarak değil beraberinde unit-test metotlarınıda yazarak yapmamız çok daha keyifli olur.

Source Code