JakartaEE Uygulamaları için Gömülü Entegrasyon ve E2E Test Geliştirimi

Kurumsal uygulamalarda uçtan uca testler(end-to-end), gerçek kullanım durumlarını kapsadığı sürece önemlidir. Bu nedenle, şirketler daha çok entegrasyon testlerine odaklansalar da, uçtan uca testler ihmal edilmemektedir. Günümüzde Sistem Entegrasyon Testi olarak adlandırılan bir diğer yaygın ihtiyaç, bir yazılım sisteminin modülleri arasındaki etkileşimleri doğrulamaktır.

Dürüst olmak gerekirse, geliştirici ortamının dışında (örneğin bir CI/CD pipeline’nında), tüm bu ihtiyaçları karşılamak Spring Framework ile karşılaştırıldığında JakartaEE’de (daha önce Java EE) biraz daha zordur. Yerleşik gömülü sunucu desteği olmadığı(doğal olarak) için, ek bağımlılıklar ve yapılandırmalar sağlamanız gerekir. Bu makalede, Arquillian ve MicroShed test çerçevelerini tanıtmaya, yukarıda değinmeye çalıştığım ihtiyaçları karşılama noktasında nasıl kullanabileceğinizi açıklamaya çalışacağım. Ele alacağım örneği GitHub’da bulabilirsiniz.

 

Başlamadan

Yukarıda belirtilen ihtiyaçları somutlaştırmak için iki servisten oluşan bir microservice örneğini ele alacağız.

  • Order Servisi
  • Validation Servisi

Order servisi, istemciden aldığı talep nesnesi içindeki kredi kartı numarası Validation servisine doğrulandıktan sonra, nesneyi veritabanına kazımaktan sorumludur. Validation servisi, Order servisi’nden iletilen kredi kartı numarasını doğrulamaktan sorumludur. Kısa bir bilgi ile, numaranın doğrulanıp onaylanmadığına dair bir bayrak döndürür.

Validation servisinin görevi nedeniyle başka bir hizmete veya sistem bileşenine bağımlı olmadığı hemen fark edilebilir. Tersine, Order servisi Validation servisine bağımlıdır ve ek olarak onaylanmış nesneyi veritabanına kazımakla yükümlüdür. Dolayısıyla, Validation servisinin belirtilen işlevsel gereksinimle uyumluluğunu değerlendirmek için bir entegrasyon testine ihtiyacımız varken, Order servisinde, servis diğer sistem bileşenlerine bağımlı olduğu için, entegrasyon testiyle yetinemeyiz. Sistemin tüm katmanlardaki hataları tespit etmek için uçtan uca bir teste ihtiyacımız var.

Bu nedenlerle entegrasyon testi için Arquillian ve uçtan uca test için MicroShed Test kullanacağız.

 

Arquillian ile Gömülü Testler Nasıl Yazılır?

Arquillian, Java uygulamaları için bir test çerçevesidir. Başlıca faydası, uygulama sunucusu yaşam döngüsünü sizin için işlemektir. Bunu konteyner adaptörleri ile sağlanmaktadır. Arquillian’ın birçok konteyner adaptörü var. Adaptör, kullanmak istediğiniz uygulama sunucusuna bağlı olduğundan, adaptörlerin ayrıntıları ve bunların nasıl yapılandırılacağı bu makalenin kapsamı dışındadır. Arquillian konteyner adaptörleri hakkında daha fazla bilgiyi burada bulabilirsiniz.

Ele alacağımız örnekte, Arquillian bağımlılıklarını ve Arquillian Yönetilen Kapsayıcı kurulumunu yönetmek için Liberty Maven plug-in‘i ile Liberty Yönetilen Konteyner Bağdaştırıcısını kullandım. Tüm bu yapılandırma ayrıntılarını depoda bulabilirsiniz.

Şimdi Arquillian ile nasıl test yazılacağına odaklanalım. Validation servisini bir end-point ve OrderController sınıfının işlevleri açısından doğrulamak için test geliştireceğiz. Test aşağıdaki gibi görünür:

@RunWith(Arquillian.class) //(1)
public class ValidationServiceIT 
{
   private final Client client;
   private static final String PATH = "api/validation/{cardNumber}";  
   
   public ValidationServiceIT() {
       this.client = ClientBuilder.newClient();
       client.register(JsrJsonpProvider.class);
   }

   @ArquillianResource //(2)
   private URL baseURL;

   @Deployment //(3)
   public static WebArchive createDeployment() {
       final WebArchive archive = ShrinkWrap.create(WebArchive.class,
               "arquillian-validation-service.war") //(4)
               .addClasses(ValidationController.class, ValidationService.class); //(5)
       return archive;
   }

   @Test
   @RunAsClient //(6)
   @InSequence(1) //(7)
   public void invalidCardNumberTest() {
       final WebTarget webTarget = client.target(baseURL.toString()).path(PATH)
               .resolveTemplate("cardNumber", "hello");
       final Response response = webTarget.request().get();
       final JsonObject result = response.readEntity(JsonObject.class);
       Assert.assertFalse(result.getBoolean("approval"));
   }

   @Test
   @RunAsClient
   @InSequence(2)
   public void validCardNumberTest() {
       final WebTarget webTarget = client.target(baseURL.toString()).path(PATH)
               .resolveTemplate("cardNumber", "12345");
       final Response response = webTarget.request().get();
       final JsonObject result = response.readEntity(JsonObject.class);
       Assert.assertTrue(result.getBoolean("approval"));
   }
}

Numaralandırılmış bölümleri açıklayarak kodu ayrıntılı biçimde inceleyelim.

1) İlk olarak, @RunWith ek açıklaması ile JUnit’e testleri Arquillian kullanarak çalıştırmasını söyleriz, böylece JUnit testleri JUnit koşucusu yerine Arquillian koşucusu ile çalıştırır.

2) Ana bilgisayar adı, bağlantı noktası ve web arşivi bilgilerini hardcoded tanımlamak için @ArquillianResource ek açıklamasını kullanırız. Bu sayede temel URL’yi elde ederiz (örneğimizde http://localhost:9090/arquillian-validation-service/) .

3) Uygulamamızı kullandığımız sunucuya(örneğimizde Open Liberty) deploy etmek için web arşivi döndüren bir metoda tanımlamalıyız. Yönteme @Deployment notasyonu uygulanmalı ve erişim belirteçleri public static olup, argüman almamalıdır. createDeployment metodu bu gereksinimleri karşılar.

4) ShrinkWrap.create yönteminin ikinci parametresi olarak iletilen arquillian-validation-service.war adına dikkat edin. Rastgele oluşturulmuş bir web arşivi adı istemiyorsanız bir ad sağlamalısınız.

5) Enjeksiyon başarısızlıklarından kaçınmak için, testimizin ihtiyaç duyduğu bağımlılıkları eklemeliyiz çünkü Arquillian, unit testlerin aksine tüm sınıfyoluna(classpath) bakmıyor. AddClass, addClassess, addPackages, addAsResource, addAsWebInfResourc, vb. metotları kullanarak test bağımlılıklarını ekleyebiliriz.

6) Validation servisini bir end-point olarak doğrulamak istediğimiz için, invalidCardNumberTest ve validCardNumberTest metotlarına @RunAsClient notasyonunu uyguladık. Notasyon, test senaryolarının istemci tarafında çalıştırılacağını belirtir, bu nedenle bu testler yönetilen kapsayıcıya karşı yürütülür.

7) Test sırasını garanti etmek için @InSequence notasyonunu kullanıyoruz.

Hem invalidCardNumberTest hem de validCardNumberTest metotlarında, kart numarasını doğrulamak için kart numaralarını baseURL + api / validation / {cardNumber} end-pointine göndeririz. Test, sayıların geçerliliğini end-pointten döndürülen nesnenin onay alanına göre kontrol eder.

Hepsi bu. Test senaryoları çalışmaya hazırdır. Mvn verfy komutunu çalıştırdıktan sonra, konsol çıktısında testlerin geçildiğini görebilirsiniz.

 

MicroShed Testi ile E2E Testleri Nasıl Yazılır?

Başka bir test çerçevesi, Java mikro hizmet uygulamaları için MicroShed’dir. Testcontainers üzerine bina edilmiştir, böylece uygulamanız bir Docker konteynerinin içinde çalışır. MicroShed’in ana yararı bu noktada ortaya çıkar, konteynırize edilmiş uygulamanızı konteyner dışından kullanmanıza izin verir, böylece true-to-production testler yapılmasını sağlar.

Örneğimizde, Order servisinin Validation servisine ve bir veritabanına bağımlı olduğunu unutmayın. Bu örnek için MicroShed’i tam olarak bu yüzden seçtik, çünkü bu bileşenleri Docker konteynerlerinde çalıştırarak Validation servisi ve veritabanına test ortamında erişmemizi sağlıyor.

MicroShed testi için minimum gereksinimler @MicroShedTest notasyonuna sahip bir sınıf ve erişim belirteçleri public static olan bir ApplicationContainer nesnesidir.

Yapılandırma ve test sınıflarımızı inceleyelim.

public class AppContainerConfig implements SharedContainerConfiguration // (1)
{
   private static final String IMAGE_NAME = "hakdogan/validation-service:01"; 
   private static final String SERVICE_NAME = "validation-service"; 
   private static final String POSTGRES_NETWORK_ALIASES = "postgres";
   private static final String POSTGRES_USER = "testUser";
   private static final String POSTGRES_PASSWORD = "testPassword";
   private static final String POSTGRES_DB = "orderDB";
   private static final int VALIDATION_SERVICE_HTTP_PORT = 9080;
   private static final int VALIDATION_SERVICE_HTTPS_PORT = 9443;
   private static final int APPLICATION_SERVICE_HTTP_PORT = 9082;
   private static final int APPLICATION_SERVICE_HTTPS_PORT = 9445;
   private static final int POSTGRES_DEFAULT_PORT = 5432;

   private static Network network = Network.newNetwork();

   @Container //(2)
   public static GenericContainer validationService = new GenericContainer(IMAGE_NAME) //(3)
                   .withNetwork(network) //(4)
                   .withNetworkAliases(SERVICE_NAME) //(5)
                   .withEnv("HTTP_PORT", String.valueOf(VALIDATION_SERVICE_HTTP_PORT)) //(6)
                   .withEnv("HTTPS_PORT", String.valueOf(VALIDATION_SERVICE_HTTPS_PORT)) //(7)
                   .withExposedPorts(VALIDATION_SERVICE_HTTP_PORT) //(8)
                   .waitingFor(Wait.forListeningPort()); //(9)

   @Container
   public static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>() //(10)
           .withNetwork(network)
           .withNetworkAliases(POSTGRES_NETWORK_ALIASES)
           .withUsername(POSTGRES_USER)
           .withPassword(POSTGRES_PASSWORD)
           .withDatabaseName(POSTGRES_DB)
           .withExposedPorts(POSTGRES_DEFAULT_PORT);

   @Container
   public static ApplicationContainer app = new ApplicationContainer() //(11)
           .withNetwork(network)
           .withEnv("POSTGRES_HOSTNAME", POSTGRES_NETWORK_ALIASES)
           .withEnv("POSTGRES_PORT", String.valueOf(POSTGRES_DEFAULT_PORT))
           .withEnv("POSTGRES_USER", POSTGRES_USER)
           .withEnv("POSTGRES_PASSWORD", POSTGRES_PASSWORD)
           .withEnv("POSTGRES_DB", POSTGRES_DB)
           .withEnv("VALIDATION_SERVICE_HOSTNAME", SERVICE_NAME)
           .withEnv("HTTP_PORT", String.valueOf(APPLICATION_SERVICE_HTTP_PORT))
           .withEnv("HTTPS_PORT", String.valueOf(APPLICATION_SERVICE_HTTPS_PORT))
           .withExposedPorts(APPLICATION_SERVICE_HTTP_PORT)
           .withAppContextRoot("/")
           .waitingFor(Wait.forListeningPort())
           .dependsOn(validationService, postgres); //(12)
}

Numaralandırılmış bölümleri açıklayarak kodu ayrıntılı biçimde inceleyelim.

1) Her test sınıfı için yeni bir kapsayıcı başlatmamak adına SharedContainerConfiguration nesnesini implement eden bir nesne kullanıyoruz. Bu şekilde, birden fazla test sınıfı aynı kapsayıcı örneklerini paylaşabilir.

2) @Container notasyonu, Testcontainers tarafından yönetilmesi gereken konteynerleri işaretlemek için kullanılır. Uygulama konteyneri dışında, Order servisinin bağımlı olduğu bileşenler için bu ortak yapılandırma sınıfında iki konteyner tanımladık, bunlar Validation servisi ve Postgresql veritabanı. Notasyonun hepsinde kullanıldığına dikkat edin.

3) IMAGE_NAME değişkeni, önceden konteynırize edilmiş Validation servisinin Docker imaj adını içerir.

4) Network nesnesini özel bir ağ oluşturmak için kullanıyoruz çünkü uygulamamızın bağımlı bileşenleriyle iletişim kurması gerekiyor. Bu nedenle, bu bileşenleri uygulama konteyneri ile aynı ağa yerleştiriyoruz.

5) SERVICE_NAME değişkeni, uygulama konteynerinde, Validation servisine erişmek için kullanılacak ismi içerir.

6–7) HTTP_PORT ve HTTPS_PORT ortam değişkenleri, uygulama sunucusuna (örneğimizde Open Liberty) hangi bağlantı noktalarının kullanılacağını bildirir. Bunlar daha önce server.xml dosyasına yer tutucu olarak eklenmiştir.

8) Testcontainers’a Validasyon servisinin HTTP portunu erişilebilir kılmasını söylüyoruz.

9) Testcontainers’a, bekleme stratejisi olarak, konteynerin kullanılmaya hazır olup olmadığını kontrol etmek için dışa açılan portları dinlemesini söylüyoruz.

10) Başlangıç parametreleriyle bir Postgres konteyneri tanımlıyoruz.

11) Uygulamamızın(Order servisi) kapsayıcısını tanımlıyoruz. Bunu çeşitli şekillerde tanımlayabilirsiniz. Reponuza bir Dockerfile eklemek veya imaj adını bir argüman olarak ApplicationContainer nesnesinin yapıcısına iletmek veya uygulama kapsayıcısı oluşturmak için varsayılan mantığı sağlayacak bir çalışma zamanı seçeneği olarak satıcıya özgü bağdaştırıcılar kullanmak seçenekleriniz arasında. Biz bu örnekte, otomatik olarak test edilebilir bir konteyner imajı oluşturmak için pom.xml dosyasına microshed-testing-liberty bağımlığı ekleyerek son seçeneği kullandık. Burada, MicroShed’in diğer çalışma zamanı seçeneklerini bulabilirsiniz.

12) Bu ayarla, uygulama konteynırının Validasyon servisi ve Postgres konteynerlerine bağımlı olduğunu belirtiyoruz.

Şimdi test sınıfımızı inceleyelim. Aşağıdaki gibi görünüyor.

 

@MicroShedTest //(1)
@SharedContainerConfig(AppContainerConfig.class) //(2)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) //(3)
public class SystemTest
{
   @RESTClient //(4)
   public static OrderController orderController;
   
   @Test
   @Order(1)
   public void invalidCardNumberTest(){
       final OrderDemand order = new OrderDemand(1, 1, "", "hello");
       final Response response = orderController.saveOrder(order);
       Assert.assertEquals(false, response.readEntity(JsonObject.class).getBoolean("approval"));
   }

   @Test
   @Order(2)
   public void validCardNumberTest(){
       final OrderDemand order = new OrderDemand(1, 1, "", "1234567");
       final Response response = orderController.saveOrder(order);
       Assert.assertNotNull(response.readEntity(OrderDemand.class).getId());
   }
  
   @Test
   @Order(3)
   public void getAllOrderTest(){
       final Response response = orderController.getAllOrders();
       final List list = response.readEntity(List.class);
       Assert.assertFalse(list.isEmpty());
   }

   @Test
   @Order(4)
   public void getAllOrderByProductIdTest(){
       final Response response = orderController.getAllOrdersByProductId(1);
       final List list = response.readEntity(List.class);
       Assert.assertFalse(list.isEmpty());
    }
}

Numaralandırılmış bölümleri açıklayarak kodu ayrıntılı biçimde inceleyelim.

1) Notasyon ile test sınıfının MicroShed Testing kullandığını belirtiyoruz.

2) Test sınıfı tarafından kullanılacak paylaşılan yapılandırmamızı tanımlıyoruz.

3) MicroShed Testing, JUnit Jupiter ile çalışıyor. Test sırasını garanti etmek için order metodunu kullanacağımızı TestMethodOrder notasyonuyla tanımlıyoruz.

4) Notasyon, JAX-RS REST Client’ı için bir enjeksiyon noktası tanımlar. Enjekte edilen nesneye yapılan herhangi bir metot çağrısı HTTP üzerinden eşdeğer bir REST isteğine çevrilecektir. Notasyonlu alan public statik tanımlanmalı ve final olmamalıdır.

Hem invalidCardNumberTest hem de validCardNumberTest metotlarında, api/order/save end-pointini @RESTClient notasyonlu orderController referansı ile çağırırız. Bu end-point, Validasyon hizmetini çağırarak doğrulama işlemini tetikler, ardından kart numarası geçerliyse istek gövdesi ile iletilen nesneyi veritabanına kazır.

Hem getAllOrderTest hem de getAllOrderByProductIdTest yöntemlerinde, geçerli kredi kartı numarasını içeren test durumundan sonra, nesnenin veritabanına kazınıp kazınmadığını kontrol ederiz.

Bu şekilde uçtan uca bir testi gerçekleştirmiş olduk. Mvn verfy komutunu çalıştırdıktan sonra, konsol çıktısında testlerin geçildiğini görebilirsiniz.

 

Sonuç

Gömülü testler, günümüzün microservice ve çok katmanlı mimariler dünyasında, özellikle CI/CD ortamlarında neredeyse vazgeçilmezdir. Arquillian ve MicroShed gibi test çerçeveleri (elbette ve Testcontainers) JakartaEE uygulamaları için bu önemli ihtiyaca cevap verir. İki çerçeveyi karşılaştırırsak, bazı farklılıkları çabucak gözden geçirebiliriz.

Arquillian, CDI konteyneri tarafından yönetilen tüm nesneleri test sınıfınıza enjekte etmenizi sağlar. MicroShed, test sınıfına sadece JAX-RS Resource nesnelerini enjekte eder. Arquillian’da XML yapılandırması uzun ve ayrıntılıdır, MicroShed, XML yapılandırması gerektirmez. Arquillian, uygulamanızın bağımlı olduğu sistem bileşenlerini test etme seçeneği sunmaz, ancak MicroShed sunar. Arquillian’ın güçlü bir topluluğu olduğunu söyleyebiliriz, ancak MicroShed için aynı şeyi söyleyemeyiz.

 

Referanslar

Arquillian Guides

MicroShed Testing

No Comments

Post a Comment

Comment
Name
Email
Website