HttpClient Unit/Integration Testing

Batıkan Duman
5 min readApr 26, 2020

--

Asp.Net Core uygulamalarında, dış sistemlerle konuşturmak amaçlı kullandığımız HttpClient nesnelerinin, integration testlerinin hangi durumlarda nasıl yapılması gerektiğini açıklayacağım.

Not: Yazıyı sadece Asp.Net Core nezdinde düşünmemek gerek, aynı built-in DI yapısını console uygulamalarında da kullanırsanız, aşağıdaki yazdıklarım geçerli olacaktır.

Yazıyı okurken daha önceden bilmeniz (önem sırasına göre) gereken kavramlar;

  • Asp.Net Core
  • .Net Core DI yapısı
  • HttpClient sınıfı
  • IHttpClientFactory arayüzü

Web uygulamasında HttpClient’ın kullanım yöntemleri

Aşağıdaki yöntemler ile HttpClient üretip kullanabiliyoruz. [1]

  • Basit kullanım
  • Named Client
  • Typed Client
  • Generated clients

Bizim yazacağımız testler, yöntem fark etmeksizin doğru çalışmalı. Fakat typed client ve generated clients için özel sınıflar üretiyoruz, DI ile bu sınıfların arayüzlerini, kullanacağımız sınıflara inject ediyoruz. Injection sırasında IHttpClientFactory araya girip bize bu özel sınıflardan, transient nesneler üretiyor. [2]

Bu sebeple testing yaparken internette yaygın olarak şöyle bir yöntem ile karşılaştım: DI üzerindeki interfacelerin, implementasyonlarını testing esnasında mock implementasyonlarıyla değiştirip izolasyonu bu şekilde sağlamışlar.

Peki bu özel sınıflara yazdığımız özel kodlar ? Bir nevi bunların test edilmesini atlamış olmuyor muyuz? Code coverage yapıyorsanız, zaten cover edilmediğini de göreceksiniz.

Peki izolasyonu nereden yapmalı?

Son HttpMessageHandler ağa çıkarken güvenlik, proxy gibi ayarları içeriyor. Biz bir öncesindeki handler ile mocking yapıcağız.

Görselde işaretlediğim gibi, ağa çıkmadan önceki son aşama, bizim izolasyon için tercih edeceğimiz yer olmalı. Eğer servis sanallaştırma tarzı yapılar kullanmıyorsanız tabi :) Kullanıyorsanız da ağ hataları, sanallaştırmayı configure etmemeden kaynaklı sorunlar flaky testlere sebep olabilir. Ayrıca teste ait configurasyonun test kodunda kalmasının tercih edilmesi de bu işlemin kod tarafında yapılmasını destekler niteliktedir.

Peki, HttpClient’a HttpMessageHandler ekleyip ağa çıkmasını engellersek üstüne bir de istediğimiz cevabı da döndürmesini sağlarsak, amacımıza ulaşmış olmaz mıyız? O zaman biraz kod yazalım. :)

Örnek bir web uygulamasının oluşturulması

Visual Studio’nun bize sunduğu templatelerden bir web application (mvc) oluşturalım.

.Net Core 3.1 & Standart Web Application (MVC) template ile.

Testini yapacağımız bir typed client içeren senaryo oluşturalım hemen. https://putsreq.com/BZZj9LJlULrb6ZxFE0Xi adresinde her istek attığımızda aşağıdaki formatta rastgele json dönmekte.

Buradaki answer değeri “yes” ya da “no” şeklinde her istekte değişmektedir. Bu sebeple, böyle bir akışın testini, flaky teste düşmeden gerçek dünya koşullarında yapmak pek mümkün değil. Test kodumuz assertion’larını yes cevabına göre beklerken no gelebilir.

Uygulamada yes ve no olması durumunda farklı bir akış gerçekleştirdiğimizi varsayalım.

Aşağıdaki gibi Client kodları oluşturup DI’a ekleyelim.

Rastgele boolean değer döndüren bir interface.
IRandomizer’dan implemente edilen YesNoApi kullanan Client sınıfı.
YesNoClient’ımızı DI’a ekledik.

Son olarak controller sınıfında oluşturduğumuz client’ı kullanalım ve ürettiği rastgele değeri önyüze taşıyalım.

ViewData’ya rastgele değeri ekledik.
Index sayfasında her yenilemede rastgele True/False dönmekte.

Integration test ortamının hazırlanması

Solution’a WebApplicationSample.IntegrationTests adında bir unit test projesi oluşturalım,

Microsoft’un fonksiyonel testleri yapmak için in-memory uygulamayı ayağa kaldırmaya yarayan yardımcı kütüphanesini nuget üzerinden ekleyelim.

Microsoft.AspNetCore.Mvc.Testing

Ayrıca, WebApplicationSample projesini, test projesine, proje referansı olarak ekleyelim.

Son aşamada test projesinin bağımlılıkları.

In-Memory bir uygulama ayağa kaldırırken, mevcuttaki uygulama configurasyonlarını kullanmaya devam edeceğimiz, istersek de değişiklik yapabileceğimiz bir StartUp sınıfı gerekiyor. Bu sayede izolasyonu sağlayabilelim.

Microsoft.AspNetCore.Mvc.Testing kütüphanesinin WebApplicationFactory sınıfı tam da bu işe yarıyor. Aşağıdaki kodda in-memory uygulamayı DI değişikliğine hazırladık diyebiliriz.

Test kodumuzu da yazalım. Burada CreateClient methodu, in-memoryde kalkan uygulamaya istek atabilmek için bize HttpClient nesnesi döndürüyor. Uygulamadaki endpointlere göre programatik olarak istek atabiliyoruz.

Ve şimdi çalıştırma zamanı..

İlk kez çalıştırma sonucu.
N. kez çalıştırma sonucu.

Görüldüğü üzere flaky teste maruz kaldık. Başta bahsettiğim HttpMessageHandler ile istekte araya girerek, aşağıdaki yapıyı sağlamaya çalışalım.

Çözüm

Global bir HttpMessageHandler sınıfı ile tüm http isteklere test case bazlı belirleyeceğimiz HttpMessageHandler’ları eklemek.

Öncelikle Global HttpMessageHandler kodlayıp, daha sonra DI’a ekleyelim.

Global Mocklayıcı

Static bir request, response method listesi tutarak, giden tüm istekleri bu listeden tek tek geçiriyoruz. Mevcut uygulamanızda başka HttpMessageHandler’lar varsa onlar çalışmaya devam edecektir. Son eklediğimiz Global handler, en son çalışacaktır.

Methodların içinde kesme işlemini (null değilse return et kodundaki gibi) yapabiliriz veya eklediğimiz koşulları sağlamıyorsa bir sonraki handler’a devam ettirebiliriz. Eğer Mocks adlı handler listesinde herhangi bir kesme olmazsa dış network’e çıkacaktır.

DI her request için global HttpMessageHandler’dan üretip akışa dahil ediyor, bu sebeple lifetime’ı transient ya da scoped olması gerekiyor. Aşağıdaki kodda tüm HttpClient’ların HttpMessageHandler zincirine global mocklayıcımızı da eklemiş olduk.

Şimdi ise test case bazlı cevapları ayarlayalım.

Burada request bazlı istediğimiz response’u dönmesini sağlayabiliyoruz. False test case’ini de ekleyip teardown methodunda her test çalıştırma sonrasında global mocker sınıfındaki mock listesini temizletelim.

Debug ekranında aşağıdaki gibi normalde sitenin dönmeyeceği bir json döndü. Burada HttpMessageHandler ile araya girip bir sonraki handler’ın çalışmasını engelleyerek, response’u içerde oluşturmuş olduk. Bu sayede Client sınıfının içindeki kodları da cover ettik.

b değişkeni, stream ile gelen http response’u görmek amaçlı.
Son durumda test sonuçları.

Sonuç

Bunun dışında bir çok yöntem mevcut, ConfigureAll yaptığımız aşamada configurasyonu client bazlı yapabiliriz veya Moq kütüphanesini kullanarak DI’daki mevcut HttpClient’ı remove edip daha sonra wraplediğimiz mock object’i koyabiliriz (aynı ayarları tekrar Startup’da yapmak koşuluyla) gibi gibi.

Fakat bu yöntemlerde ServiceCollection build edilmeden önce yapılması gerekiyor. Yukarıda bahsettiğim yöntemde tüm dış network için data veya uri bazlı mocking yapabiliyoruz ki bana göre en önemli kısmı test case’in içinde data preparation yapabilmemiz. Test case’i kendini açıklar nitelikte oluyor.

Not: Yukarıdaki static tanımlı Mocks nesnesi, thread-safe değildir. Paralel test koşturacaksanız, ekstra geliştirme yapmanız gerekir.

--

--