İçeriğe geç →

Microservice Mimarilerinde Consumer Driven Contracts Testing Nedir? ve C# ile Implementasyon

Merhaba arkadaşlar.

Yine microservice mimarilerine yönelik bir konu ile karşınızdayım. Sizlerde biliyorsunuz ki son dönemlerde neredeyse tüm ilgi alanımı, microservice mimarileri ve MQ(Message Queue) tabanlı sistemlere yoğunlaştırdım. Bu sistemler her ne kadar bir çok derdimizi çözseler bile, asla kusursuz bir şey olmadığı gibi beraberlerinde getirdiği bazı ek maliyetler de bulunmaktadır.

Microservice mimarilerine geçilmesinde, belki en önemli concern’lerden birisi de test konusudur. Peki nasıl?

consumer-contract-test-geek

Sistemimizde -n tane servis’in var olduğunu düşünelim ve hepsi loosely coupled bir şekilde hayatlarını sürdürmektedir. Biliyoruz ki bitmek bilmeyen business ihtiyaçlarının karşısında, sisteme sürekli ek özellikler katılmaktadır. Bunların yanı sıra gerçek hayatta legacy application’ların, bir anda yeni mimarilere de geçirilebilmesi de mümkün olmamaktadır. Bu sebep ile her bir modülü parça parça microservis’lere taşımamız gerekebileceği için sürekli bir deployment ihtiyacı doğacaktır. Peki bu deployment’lar sırasında hangi servisin nereyi etkileyeceğini nasıl bilebiliriz ve ilgili servisi tüketen diğer servislerin bozulmayacağını nasıl test edebiliriz?

En sık karşılaşılan durumlardan birisi, endpoint’lerin client’ın etkileşimi olmaksızın değiştirilmesidir.

Örneğin:

  • GET /api/customer şeklinde iken, GET /api/customers olarak endpoint’in değiştirilmesi
  • GET /api/customers/1 şeklinde müşteri bilgilerini dönerken, artık GET /api/customers/1/detail şeklinde dönülmesi gibi senaryolar

İşte bu tarz durumlar karşısında insan bazen, keşke monolith bir application geliştiriyor olsaydık demiyor değil. 🙂 İşin geyiği bir yana monolith bir application için integration test’ler yazmak, birden çok distributed microservice’lere implemente etmekten çok daha basit.

Konumuza geri dönecek olursak eğer, microservice yapılarında bu tarz problemler için aslında yeni olmayan bir yaklaşım/pattern mevcuttur. Bir kaç dönemdir microservice mimarilerinin popülerleşmesi ile önem kazanan bu yaklaşım, Consumer Driven Contracts Testing olarak geçmektedir.

Consumer Driven Contract temel olarak API contract’larının kullanımlarını client’lara tanımlayarak, değişimler sırasında yaşanabilecek problemlerin bir nebze önüne geçebilmeye yardımcı olmaktadır. Consumer Driven Contracts’ı .NET tarafında implementasyonu için Pact isminde harika bir framework mevcuttur. Makale sırasında gerçekleştirecek olduğumuz örneğimizde, Pact framework’ünü kullanarak contract specification, contract verification ve contract distribution nasıl gerçekleştirilir gibi konuları ele alıyor olacağız.

Consumer Driven Contracts Testing Nasıl Çalışır?

Consumer Driven Contracts’ın çalışma mantığı basic concept olarak aşağıdaki gibidir.

  1. Consumer, ilgili service’in request ve response’undan ne beklediğini tanımlar
  2. Provider ve Consumer bu contract’lar için verify işlemini gerçekleştirir
  3. Sonrasında ise Provider, ilgili contract’ların yerine getirildiğini sürekli olarak doğrular ve bu sayede hangi Consumer’ların değişimler karşısında nasıl etkilendikleri görünür kılınır

Pact Framework’ünün Bazı Avantajları

  • End-to-end olarak ortamları kurmaya gerek yoktur
  • Manuel testing gerektirmez
  • Contract’ların üretimi ve doğrulanması Pact tarafından otomatik olarak gerçekleştirilir
  • Oluşturulan Pact’ler sayesinde de, readable bir şekilde Api Contract dokümantasyonu da oluşturulmuş olunur

Bunlara ek olarak, farklı teknolojiler ile geliştirilmiş service’ler arasındaki entegrasyonu da mümkün kılar. Pact initial olarak Ruby için geliştirilmiş bir framework’dür ve şuan bir çok programlama dilinde yaygın bir hale gelmiş durumdadır. Contract verify işlemi için oluşturulacak olan pact file’ı JSON formatında olacağı için, farklı platformlar arasında da kolaylıkla implemente edilebilmesi mümkündür.

Pact Framework’ünün Implementasyonu

1) Consumer ve Pact’lerinin Oluşturulması

define-consumer-expectations

Implementasyon sırasında ilk gerçekleştirecek olduğumuz kısım, yukarıdaki diyagramın sol tarafı olacaktır. Burada sonrasında oluşturacak olduğumuz Provider’ı yani API’ı consume edecek bir Consumer yazacağız. Consumer içerisinde API contract’larına yönelik request ve response’ların Pact’leri çıkartıp, file system’da verify işlemi için persist edeceğiz.

İkinci kısımda oluşturacak olduğumuz Provider, müşteri bilgilerini çeken bir API olsun. Bu API’ın GET method’u “/api/customers/1” URI’ı ile, “1” numaralı id’ye sahip customer’ı dönsün. Öncelikle ilk adım olarak serialization işlemlerinde kolaylık sağlaması açısından, API’dan repsonse olarak dönüyor olacağımız contract’ı ekleyerek başlayalım. Bunun için “ConsumerDrivenContractsTestingSample” isminde yeni bir blank solution oluşturup, içerisine “Contracts” isminde bir class library ekleyelim.

Eklemiş olduğumuz bu library içerisinde, “Customer.cs” isminde yeni bir class ekleyelim ve aşağıdaki gibi kodlayalım.

Contract kısmı bu kadar.

Diyagramın ilk kısımdaki Consumer tarafını kodlamaya başlayabiliriz. Solution üzerine structured görünebilmesi açısından “Service Consumer” isminde bir klasör oluşturup, içerisine “CustomerApiServiceConsumer” isminde bir class library ekleyelim. Bu library içerisinde, biraz önce bahsetmiş olduğum Customer API’ı consume edebilmemizi sağlayan client kısmını kodlayacağız. Client’ı oluşturma işlemine başlamadan önce REST call’ları sırasında, kolaylık sağlaması açısından Nuget Package Manager üzerinden “RestSharp” paketini “CustomerApiServiceConsumer” library’sine kuralım ve “Contracts” library’sini referans olarak da gösterelim.

Kurulum işleminden sonra “CustomerApiClient.cs” isminde yeni bir class ekleyelim ve client’ı aşağıdaki gibi kodlayalım.

Oluşturmuş olduğumuz “CustomerApiClient” içerisinde “RestSharp” ın client’ını kullanarak, “/api/customers/{id}” resource’una basic bir GET request’i yapıyoruz. Request sonucunda ise geriye “Customer” result’ını dönüyoruz.

Şimdi sıra geldi test projesini oluşturmaya. Oluşturacak olduğumuz bu test projesi, servis’i kullanacak olan service consumer’ı temsil ediyor olacak ve burada “Customer API” a yönelik Pact‘leri çıkartıp, test case’lerini yazacağız. Solution üzerindeki “Service Consumer” klasörünün içerisine, “CustomerApiServiceConsumer.Tests” isminde yeni bir Unit Test projesi oluşturalım. Unit Test projesinin üzerine, öncelikle test işlemlerinde bazı fonksiyonlarından yararlanabilmek için “xunit” test framework’ünü ve “xunit.runner.visualstudio” paketini, Nuget Package Manager üzerinden kuralım. Kurulum işleminin ardından makale girişinde de bahsetmiş olduğumuz, Consumer Driven Contracts Testing yazabilmemize olanak sağlayan “PactNet” paketini de Nuget Package Manager üzerinden kuralım. Nuget üzerindeki kurulumların ardından, “Contracts” ve “CustomerApiServiceConsumer” library’lerini referans olarak ekleyelim.

Burada kodlamaya ilk olarak Customer API’ın pact’ini oluşturmak ile başlayacağız. Bu kısımlarda referans aldığım yer ise, Pact framework’ünün .net implementasyon kısmının bulunduğu github adresidir. Makale sonunda sizlerle paylaşıyor olacağım. Konumuza devam edecek olursak, “ConsumerCustomerApiPact.cs” isminde yeni bir class ekleyelim ve aşağıdaki gibi kodlayalım.

“ConsumerCustomerApiPact” class’ı içerisinde bulunan “PactBuilder” ile, bir adet mock servis ayağa kaldırıyoruz. Bu mock servis, “MockServerPort” property’si üzerinden almış olduğu port bilgisi ile arka planında Nancy Self Hosting paketini kullanarak oluşmaktadır. Buradaki dikkat edilmesi gereken nokta ise “PactBuilder.ServiceConsumer()” method’unda servis’i consume edecek yerin neresi olduğu, “HasPactWith()” method’unda ise hangi provider ile pact’e sahip olduğunu belirtiyoruz. Dispose method’u ile gerçekleştirilen deferred execution işlemi ile bu pact’i kullanacak olan test case’inin, ilgili test senaryosunu gerçekleştirmesinden sonra Customer API’a ait pact’leri oluşturması sağlanmaktadır.

Şimdi yine “CustomerApiServiceConsumer.Tests” projesi içerisine, “CustomerApiConsumerTests.cs” isminde bir Unit Test class’ı ekleyelim ve aşağıdaki gibi kodlayalım.

“xunit” test paketi ile gelen “IClassFixture” interface’inin içerisine set ettiğimiz “ConsumerCustomerApiPact” class’ını, herhangi bir test case’i execute olmadan önce pact class’ını execute ediyor ve constructor üzerinden pact’in instance’ını inject ediyor. “MockProviderService.ClearInteractions()” method’u ile de, önceden kayıt edilmiş bir interaction varsa, test run edilmeden önce temizlemektedir.

Şimdi gelelim asıl test case’ine. “GetCustomer_WhenTheCustomerIdGreaterThanZero_ReturnCustomer” ismindeki test case’i ile isminden de anlaşılabileceği gibi, customer id’si sıfırdan büyük ise ilgili customer’ı getirmesini istiyoruz. Buradaki işlemleri fluent bir şekilde “ConsumerCustomerApiPact” içerisinden gelen “IMockProviderService” ile gerçekleştiriyoruz. Burada yer alan “Given()” ve “UponReceiving()” fluent method’ları ile, pact’in readability açısından anlaşılır olabilmesi için ne alınacağını ve ne alındığı gibi bilgileri giriyoruz.”With()” ve “WillRespondWith” fluent method’ları ile ise, provider’a nasıl bir request geçeceğimizi ve bunun sonucunda nasıl bir response elde edeceğimizi tanımlıyoruz. Dikkat ederseniz bu noktada aslında bir nevi provider’ın endpoint dokümantasyonunu da oluşturuyor gibiyiz. Aslında gibiyiz de değil, oluşturuyoruz her bir endpoint için. 🙂

Sonrasında ise artık, öncesinde oluşturmuş olduğumuz client’ı kullanarak, “_mockProviderServiceBaseUri” ile mock servis üzerinden “consumer.Get(1);” method’unu çağırıyoruz. Bu işlemin sonucunda eğer result null değilse, pact’i oluşturabilmesi için interaction’ı verify ederek, mock provider üzerinde tekrardan kayıtlı hale getiriyoruz.

Test Explorer üzerinden “GetCustomer_WhenTheCustomerIdGreaterThanZero_ReturnCustomer” method’unu çalıştıralım ve sorunsuz bir şekilde implementasyonu gerçekleştirdi isek, test’in başarılı bir şekilde geçtiğini görebileceğiz.

test-explorer

Yukarıdaki resimde görüldüğü gibi test’i çalıştırdığımda başarılı bir şekilde test işlemi gerçekleşti. Şimdi asıl bu işlemin sonucunda hatırlarsak “ConsumerCustomerApiPact” oluştururken Dispose kısmında gerçekleştirilen deferred execution ile “MockProviderService” üzerine register edilmiş request ve beklenen response senaryoları buradan”Build()” method’u ile tetikleniyordu. Bu işlem pact file’ının “\CustomerApiServiceConsumer.Tests\pacts” dizini altında aşağıdaki gibi oluşmasını sağlamıştır.

Pact dosyası ne kadar da okunabilir değil mi? Artık Provider kısmını hazırlamaya başlayabiliriz.

2) Provider’ın Oluşturulması ve Verification

verify-expectations-on-provider

Artık bu kısımda Provider üzerinden Consumer’ın oluşturmuş olduğu pact’leri alarak, test API server’ı üzerinden pact’ler doğrultusunda verify işlemini gerçekleştireceğiz. Solution üzerine “Service Provider” isminde yeni bir klasör daha ekleyerek, içerisine “CustomerApi.Host” isminde bir console application projesi oluşturalım. Projeyi oluşturduktan sonra Nuget Package Manager üzerinden “Microsoft.AspNet.WebApi.OwinSelfHost” paketini kuralım.

Kurulum işlemlerinin ardından API’ı owin ile host edebilmek için “Startup.cs” isminde yeni bir class ekleyelim ve aşağıdaki gibi kodlayalım.

Startup class’ı ile, owin üzerinden API’ın host işlemini gerçekleştireceğiz. Host işlemi için “Program.cs” class’ını aşağıdaki gibi güncelleyelim.

“Microsoft.Owin.Hosting” namespace’i altında bulunan “WebApp” ile API’ı, localhost üzerinden “8090” portu üzerinden host edeceğiz. Şimdi sıra geldi controller’ı eklemeye. Bunun için “Controllers” isminde yeni bir klasör oluşturup, “CustomerController.cs” class’ını içerisine ekleyelim ve aşağıdaki gibi kodlayalım.

Makale girişinde de bahsettiğimiz gibi artık “api/customers/{id}” URI’ı ile, “0” dan büyük bir id değeri geldiğinde geriye “Gökhan Gökalp” kullanıcısını dönecektir.

Test edebilmek için “CustomerApi.Host” projesini çalıştıralım ve “http://localhost:8090/api/customers/1” URI’ına bir GET request’inde bulunalım.

customer-response

Yukarıdaki resimde olduğu gibi “1” numaralı id’ye sahip “Gökhan Gökalp” kullanıcısının, response olarak geldiğini görüyoruz.

Şimdi “CustomerApi.Host” projesi için bir Unit Test projesi oluşturacağız ve burada consumer’ın oluşturmuş olduğu pact’lerin verify işlemlerini gerçekleştireceğiz. “Service Provider” klasörü içerisine “CustomerApi.Tests” isminde yeni bir Unit Test projesi oluşturalım ve projenin oluşturma işleminin ardından Nuget Package Manager üzerinden “xunit”, “xunit.runner.visualstudio” “PactNet” ve “Microsoft.Owin.Testing” paketlerini kuralım. Paketlerin kurulum işleminin tamamlanmasından sonra “CustomerApi.Host” projesini de referans olarak ekleyelim.

İhtiyaç duyduğumuz paketler hazır olduğuna göre “CustomerTests.cs” isminde bir Unit Test class’ı ekleyelim ve aşağıdaki gibi kodlayalım.

Burada oluşturmuş olduğumuz “EnsureCustomerApiHonoursPactWithConsumer” method’unun içerisinde bir “PactVerifier” tanımlıyoruz. Sonrasında ise verifier üzerinden provider’ın state’ini, “ServiceProvider()” fluent method’u ile hangi provider olduğunu ve Owin TestServer’ı üzerinden gelen http client’ını parameter olarak set ediyoruz. “HonoursPactWith()” fluent method’u ile pact’i oluşturan consumer’ın name’ini ve “PactUri()” fluent method’u ile de oluşturulmuş olan pact’in URI adresini parametre olarak set ediyoruz.

Tüm parametrelerin set edilmesinden sonra “Verify()” method’unu çağırarak, provider’ın mock test server’ı üzerinden consumer tarafından oluşturulmuş olan pact’lerin, verify işleminin gerçekleştirilmesini sağlamış oluyoruz. Tüm implementasyonumuz tamamlanmış durumda. Hatırlarsak consumer tarafını implemente ederken, test etmek amaçlı Unit Test case’ini çalıştırmıştık ve başarılı bir şekilde pact file’ı oluşmuştu. Şimdi ise verify işlemini deneyebilmek için gelin “EnsureCustomerApiHonoursPactWithConsumer” test case’ini çalıştıralım ve sonucuna bir bakalım.

verify-test

Evet bu test case’i de başarılı bir şekilde sonuçlandı.

Bu demek oluyor ki pact üzerinde consumer’ın provider’dan beklemiş olduğu request ve response modelleri, mock test server’ı üzerinden çalıştırılarak başarılı bir şekilde verify edildi. Şimdi gelelim gerçek hayatta karşılaşabileceğimiz bir problemi test etmeye. Diyelim ki Customer API müşteri detaylarını artık “/api/customers/1” şeklinde değil de, “/api/customers/1/detail” şeklinde veriyor olarak güncellensin. Bunun için hemen ilgili controller’ı açarak aşağıdaki gibi route’u güncelleyelim.

Controller’ı güncelleme işleminden sonra solution’ı derleyelim ve tekrardan “EnsureCustomerApiHonoursPactWithConsumer” test case’ini çalıştıralım.

verify-test-fail

Ups! Bu sefer test case’i başarılı bir şekilde çalıştırılamadı. Hatanın detaylı açıklamasını ise test case’inin bulunduğu projenin klasörü altında, “logs” isminde yeni bir klasör oluşturarak “providerName.host_verifier” isminde bir log dosyası içerisinde tutmaktadır.

Bu case için PactNet’in oluşturmuş olduğu “customerapi.host_verifier.log” dosyası ise aşağıdaki gibidir.

Yukarıdaki log satırları aslında gayet anlaşılır bir şekilde açık. PactNet burada, consumer’ın ne istediği ve ne beklediğini provider tarafından sağlamaya çalışıyor. “Failures” altında ise HTTP status’ü olarak “200” beklediğini ancak “404” aldığını ve “1” numaralı müşteriye ait bilgileri alması gerekirken, “No HTTP resource was found…” şeklinde bir hata mesajı ile karşılaştığını söylüyor. Ne kadar harika değil mi?

Consumer Driven Contract Testing bize, değişen ihtiyaçlar karşısında deployment yapmadan önce nerelerin etkilenebileceğini hızlı bir şekilde ulaşabilmemizi sağlamaktadır. Özellikle microservice yapılarının bulunduğu ortamlarda, nerelerin etkilenebileceği tarz concern’lerin giderilebilmesi için kullanılması gerekmektedir diye düşünüyorum son zamanlarda. Bunlara ek olarak birde az önce “PactUri()” fluent method’u ile consumer’ın oluşturmuş olduğu pact’in adresini set etmiştik. Bu pact’leri dilersek network üzerinde ortak bir yerde de persist ederek, verify işlemini oradan gerçekleştirebilmek de mümkündür.

Umarım yararlı ve keyifli bir makale olmuştur. Makale sırasında gerçekleştirmiş olduğumuz örnek projeye, aşağıdaki github hesabım üzerinden erişebilirsiniz.

https://github.com/GokGokalp/consumer-driven-contracts-testing-sample

Kaynaklar:

https://github.com/SEEK-Jobs/pact-net
https://martinfowler.com/articles/consumerDrivenContracts.html
https://tech.affinitas.de/?p=51

Bu makale toplam (1262) kez okunmuştur.

16
0



Kategori: .NET Asp.Net Web API Microservices Test Driven Development

Yorumlar

Bir Cevap Yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

*