In: Genel


Uygulamanın güvenilirliğini ve kararlılığını korumak için birim testini yeterli bir çözüm olarak görmüyor musunuz? Birim testlerinin tüm durumları kapsaması gerektiği varsayımında bir şekilde veya bir yerde potansiyel bir hatanın gizlendiğinden korkuyor musunuz? Ayrıca proje gereksinimleri için Kafka ile alay etmek yeterli değil mi? Tek bir yanıt bile ‘evet’ ise, o zaman TestContainers ve Spring için Gömülü Kafka kullanarak Kafka için Entegrasyon Testlerinin nasıl kurulacağına dair güzel ve kolay bir kılavuza hoş geldiniz!

TestContainers nedir?

TestContainers, harici kaynakların entegrasyonu ve test edilmesi için gerekli tüm çözümleri sağlama konusunda uzmanlaşmış, açık kaynaklı bir Java kitaplığıdır. Bu, gerçek bir veritabanını, web sunucusunu ve hatta bir olay veri yolu ortamını taklit edebildiğimiz ve bunu uygulama işlevselliğini test etmek için güvenilir bir yer olarak değerlendirebildiğimiz anlamına gelir. Tüm bu süslü özellikler, konteynerler olarak tanımlanan docker görüntülerine bağlanır. Veritabanı katmanını gerçek MongoDB ile test etmemiz gerekiyor mu? Endişelenmeyin, bunun için bir test kapsayıcımız var. UI testlerini de unutamayız – Selenium Container gerçekten ihtiyacımız olan her şeyi yapacak.
Bizim durumumuzda Kafka Testcontainer’a odaklanacağız.

Gömülü Kafka nedir?

Adından da anlaşılacağı gibi, tam işlevselliğe sahip normal bir aracı olarak kullanılmaya hazır bir bellek içi Kafka örneği ile ilgileneceğiz. Her zamanki gibi üreticiler ve tüketicilerle çalışmamıza izin vererek entegrasyon testlerimizi hafif hale getiriyor.

Başlamadan önce

Testimizin konsepti basit – Kafka tüketicisini ve üreticisini iki farklı yaklaşım kullanarak test etmek ve bunları gerçek durumlarda nasıl kullanabileceğimizi kontrol etmek istiyorum.

Kafka Mesajları, Avro şemaları kullanılarak serileştirilir.

Gömülü Kafka – Yapımcı Testi

Konsept kolaydır – bir Kafka Avro serileştirilmiş mesajını iletmek için bir servis yöntemini çağıran denetleyici ile basit bir proje oluşturalım.

Bağımlılıklar:

dependencies {
implementation "org.apache.avro:avro:1.10.1"
implementation("io.confluent:kafka-avro-serializer:6.1.0")
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.kafka:spring-kafka'
implementation('org.springframework.cloud:spring-cloud-stream:3.1.1')
implementation('org.springframework.cloud:spring-cloud-stream-binder-kafka:3.1.1')

implementation('org.springframework.boot:spring-boot-starter-web:2.4.3')
implementation 'org.projectlombok:lombok:1.18.16'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.cloud:spring-cloud-stream-test-support:3.1.1')
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.kafka:spring-kafka-test'
}

Ayrıca Avro için harika bir eklentiden bahsetmeye değer. İşte eklentiler bölümü:

plugins {
	id 'org.springframework.boot' version '2.6.8'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
	id "com.github.davidmc24.gradle.plugin.avro" version "1.3.0"
}

Avro Plugin, şema otomatik oluşturmayı destekler. Bu olmazsa olmazlardan.

Eklentiye bağlantı: https://github.com/davidmc24/gradle-avro-plugin

Şimdi Avro şemasını tanımlayalım:

{
  "namespace": "com.grapeup.myawesome.myawesomeproducer",
  "type": "record",
  "name": "RegisterRequest",
  "fields": [
    {"name": "id", "type": "long"},
    {"name": "address", "type": "string", "avro.java.string": "String"
    }

  ]
}

YapımcıHizmetimiz yalnızca bir şablon kullanarak Kafka’ya mesaj göndermeye odaklanacak, bu kısımla ilgili heyecan verici bir şey yok. Ana işlevsellik sadece bu satırı kullanarak yapılabilir:

ListenableFuture<SendResult<String, RegisterRequest>> future = this.kafkaTemplate.send("register-request", kafkaMessage);

Test özelliklerini unutamayız:

spring:
  main:
    allow-bean-definition-overriding: true
  kafka:
    consumer:
      group-id: group_id
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: com.grapeup.myawesome.myawesomeconsumer.common.CustomKafkaAvroDeserializer
    producer:
      auto.register.schemas: true
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: com.grapeup.myawesome.myawesomeconsumer.common.CustomKafkaAvroSerializer
    properties:
      specific.avro.reader: true

Bahsedilen test özelliklerinde gördüğümüz gibi, KafkaMessages için özel bir seri hale getirici/seri hale getirici ilan ediyoruz. Avro ile Kafka kullanılması şiddetle tavsiye edilir – JSON’ların nesne yapısını korumasına izin vermeyin, Avro gibi uygar eşleştirici ve nesne tanımını kullanalım.

Serileştirici:

public class CustomKafkaAvroSerializer extends KafkaAvroSerializer {
    public CustomKafkaAvroSerializer() {
        super();
        super.schemaRegistry = new MockSchemaRegistryClient();
    }

    public CustomKafkaAvroSerializer(SchemaRegistryClient client) {
        super(new MockSchemaRegistryClient());
    }

    public CustomKafkaAvroSerializer(SchemaRegistryClient client, Map<String, ?> props) {
        super(new MockSchemaRegistryClient(), props);
    }
}

seri kaldırıcı:

public class CustomKafkaAvroSerializer extends KafkaAvroSerializer {
    public CustomKafkaAvroSerializer() {
        super();
        super.schemaRegistry = new MockSchemaRegistryClient();
    }

    public CustomKafkaAvroSerializer(SchemaRegistryClient client) {
        super(new MockSchemaRegistryClient());
    }

    public CustomKafkaAvroSerializer(SchemaRegistryClient client, Map<String, ?> props) {
        super(new MockSchemaRegistryClient(), props);
    }
}

Ve testimizi yazmaya başlamak için her şeye sahibiz.

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ActiveProfiles("test")
@EmbeddedKafka(partitions = 1, topics = {"register-request"})
class ProducerControllerTest {

Tek yapmamız gereken, listelenen konular ve bölümlerle @EmbeddedKafka ek açıklaması eklemek. Uygulama Bağlamı, Kafka Broker’ı sağlanan yapılandırmayla aynı şekilde önyükleyecektir. @TestInstance’ın özel bir dikkatle kullanılması gerektiğini unutmayın. Lifecycle.PER_CLASS, her test yöntemi için aynı nesneleri/bağlamı oluşturmaktan kaçınacaktır. Testlerin çok zaman alıcı olup olmadığını kontrol etmeye değer.

Consumer<String, RegisterRequest> consumerServiceTest;
@BeforeEach
void setUp() {
DefaultKafkaConsumerFactory<String, RegisterRequest> consumer = new DefaultKafkaConsumerFactory<>(kafkaProperties.buildConsumerProperties();

consumerServiceTest = consumer.createConsumer();
consumerServiceTest.subscribe(Collections.singletonList(TOPIC_NAME));
}

Burada, Avro şema dönüş türüne göre test tüketicisini bildirebiliriz. Tüm Kafka özellikleri .yml dosyasında zaten sağlanmıştır. Bu tüketici, üreticinin gerçekten bir mesaj gönderip göndermediğini kontrol etmek için kullanılacaktır.

İşte gerçek test yöntemi:

@Test
void whenValidInput_therReturns200() throws Exception {
        RegisterRequestDto request = RegisterRequestDto.builder()
                .id(12)
                .address("tempAddress")
                .build();

        mockMvc.perform(
                post("/register-request")
                      .contentType("application/json")
                      .content(objectMapper.writeValueAsBytes(request)))
                .andExpect(status().isOk());

      ConsumerRecord<String, RegisterRequest> consumedRegisterRequest =  KafkaTestUtils.getSingleRecord(consumerServiceTest, TOPIC_NAME);

        RegisterRequest valueReceived = consumedRegisterRequest.value();

        assertEquals(12, valueReceived.getId());
        assertEquals("tempAddress", valueReceived.getAddress());
    }

Öncelikle uç noktamızda bir eylem gerçekleştirmek için MockMvc kullanıyoruz. Bu uç nokta, iletileri Kafka’ya iletmek için ProducerService’i kullanır. KafkaConsumer, üreticinin beklendiği gibi çalışıp çalışmadığını doğrulamak için kullanılır. İşte bu kadar – gömülü Kafka ile tamamen çalışan bir testimiz var.

Test Konteynerleri – Tüketici Testi

TestContainer’lar, dockerize edilmeye hazır bağımsız docker görüntüleri gibi değildir. Aşağıdaki test senaryosu bir MongoDB görüntüsü ile geliştirilecektir. Kafka akışında herhangi bir şey olduktan hemen sonra verilerimizi neden veritabanında tutmuyoruz?

Bağımlılıklar önceki örnekten çok farklı değildir. Test kapları için aşağıdaki adımlar gereklidir:

testImplementation 'org.testcontainers:junit-jupiter'
	testImplementation 'org.testcontainers:kafka'
	testImplementation 'org.testcontainers:mongodb'

ext {
	set('testcontainersVersion', "1.17.1")
}

dependencyManagement {
	imports {
		mavenBom "org.testcontainers:testcontainers-bom:${testcontainersVersion}"
	}
}

Şimdi Tüketici kısmına odaklanalım. Test durumu basit olacak – Kafka mesajını almaktan ve ayrıştırılan yükü MongoDB koleksiyonunda depolamaktan bir tüketici hizmeti sorumlu olacak. Şimdilik KafkaListeners hakkında bilmemiz gereken tek şey şu açıklama:

@KafkaListener(topics = "register-request")

Açıklama işlemcisinin işlevselliği ile, yöntemimizde bir dinleyici oluşturmaktan KafkaListenerContainerFactory sorumlu olacaktır. Bu andan itibaren yöntemimiz, yaklaşan herhangi bir Kafka mesajına belirtilen konuyla tepki verecektir.

Avro serileştirici ve seri hale getirici yapılandırmaları önceki testtekiyle aynıdır.

TestContainer ile ilgili olarak, aşağıdaki ek açıklamalarla başlamalıyız:

@SpringBootTest
@ActiveProfiles("test")
@Testcontainers
public class AbstractIntegrationTest {

Başlatma sırasında, yapılandırılmış tüm TestContainers modülleri etkinleştirilecektir. Bu, seçilen kaynağın tam işletim ortamına erişeceğimiz anlamına gelir. Örnek olarak:

@Autowired
private KafkaListenerEndpointRegistry kafkaListenerEndpointRegistry;

@Container
public static KafkaContainer kafkaContainer = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1"));

@Container
static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.4.2").withExposedPorts(27017);

Testi başlatmanın bir sonucu olarak, sağlanan konfigürasyonla iki liman işçisi konteynerinin başlamasını bekleyebiliriz.

Mongo kapsayıcı için gerçekten önemli olan şey, bize sadece basit bir bağlantı uri kullanarak veritabanına tam erişim sağlar. Böyle bir özellik sayesinde koleksiyonlarımızdaki mevcut durumun ne olduğunu, hatta hata ayıklama modunda ve hazır kesme noktalarında bile bir göz atabiliyoruz.
Ryuk konteynerine de bir göz atın – overwatch gibi çalışır ve konteynerlerimizin doğru şekilde başlayıp başlamadığını kontrol eder.

Ve işte konfigürasyonun son kısmı:

@DynamicPropertySource
static void dataSourceProperties(DynamicPropertyRegistry registry) {
   registry.add("spring.kafka.bootstrap-servers", kafkaContainer::getBootstrapServers);
   registry.add("spring.kafka.consumer.bootstrap-servers", kafkaContainer::getBootstrapServers);
   registry.add("spring.kafka.producer.bootstrap-servers", kafkaContainer::getBootstrapServers);
   registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl);
}

static {
   kafkaContainer.start();
   mongoDBContainer.start();

   mongoDBContainer.waitingFor(Wait.forListeningPort()
           .withStartupTimeout(Duration.ofSeconds(180L)));
}

@BeforeTestClass
public void beforeTest() {

   kafkaListenerEndpointRegistry.getListenerContainers().forEach(
           messageListenerContainer -> {
               ContainerTestUtils
                       .waitForAssignment(messageListenerContainer, 1);

           }
   );
}

@AfterAll
static void tearDown() {
   kafkaContainer.stop();
   mongoDBContainer.stop();
}

DynamicPropertySource bize, test yaşam döngüsü boyunca gerekli tüm ortam değişkenlerini ayarlama seçeneği sunar. TestContainers için herhangi bir yapılandırma amacı için kesinlikle gereklidir. Ayrıca, BeforeTestClass kafkaListenerEndpointRegistry, kapsayıcı başlatma sırasında her dinleyicinin beklenen bölümleri almasını bekler.

Ve Kafka test kapsayıcı yolculuğunun son kısmı – testin ana gövdesi:

@Test
public void containerStartsAndPublicPortIsAvailable() throws Exception {
   writeToTopic("register-request", RegisterRequest.newBuilder().setId(123).setAddress("dummyAddress").build());

   //Wait for KafkaListener
   TimeUnit.SECONDS.sleep(5);
   Assertions.assertEquals(1, taxiRepository.findAll().size());

}

private KafkaProducer<String, RegisterRequest> createProducer() {
   return new KafkaProducer<>(kafkaProperties.buildProducerProperties());
}

private void writeToTopic(String topicName, RegisterRequest... registerRequests) {

   try (KafkaProducer<String, RegisterRequest> producer = createProducer()) {
       Arrays.stream(registerRequests)
               .forEach(registerRequest -> {
                           ProducerRecord<String, RegisterRequest> record = new ProducerRecord<>(topicName, registerRequest);
                           producer.send(record);
                       }
               );
   }
}

Özel üretici, mesajımızı KafkaBroker’a yazmaktan sorumludur. Ayrıca, tüketicilere mesajları düzgün bir şekilde ele almaları için biraz zaman verilmesi önerilir. Gördüğümüz gibi, mesaj sadece dinleyici tarafından tüketilmedi, aynı zamanda MongoDB koleksiyonunda da saklandı.

Sonuçlar

Gördüğümüz gibi, entegrasyon testleri için mevcut çözümlerin projelerde uygulanması ve sürdürülmesi oldukça kolaydır. Sadece birim testleri tutmanın ve kod/mantık kalitesinin bir işareti olarak kapsanan tüm satırlarda saymanın bir anlamı yoktur. Şimdi soru şu: Gömülü bir çözüm mü yoksa TestContainers mı kullanmalıyız? Her şeyden önce “Gömülü” kelimesine odaklanmanızı öneririm. Mükemmel bir entegrasyon testi olarak, tüm özellikleri/özellikleri içeren üretim ortamının neredeyse ideal bir kopyasını elde etmek istiyoruz. Bellek içi çözümler iyidir, ancak çoğunlukla büyük iş projeleri için yeterli değildir. Kesinlikle, Gömülü hizmetlerin avantajı, bellekte herhangi bir şey olduğunda bu tür testleri uygulamanın ve yapılandırmayı korumanın kolay yoludur.
TestContainer’lar ilk bakışta abartı gibi görünse de bize en önemli özelliği, yani ayrı bir ortamı veriyorlar. Mevcut liman işçisi görüntülerine bile güvenmemize gerek yok – istersek özel olanları kullanabiliriz. Bu, potansiyel test senaryoları için büyük bir gelişmedir.
Jenkins’e ne dersin? Jenkins’te TestContainers kullanmaktan da korkmak için bir neden yok. Jenkins aracıları için yapılandırmayı ne kadar kolay ayarlayabileceğimize dair TestContainers belgelerini kontrol etmenizi şiddetle tavsiye ederim.
Özetlemek gerekirse – TestContainers’ı kullanmak için herhangi bir engelleyici veya istenmeyen koşul yoksa tereddüt etmeyin. Entegrasyon testi sözleşmeleriyle tüm hizmetlerin yönetilmesi ve güvence altına alınması her zaman iyidir.

Bir cevap yazın

Ready to Grow Your Business?

We Serve our Clients’ Best Interests with the Best Marketing Solutions. Find out More

How Can We Help You?

Need to bounce off ideas for an upcoming project or digital campaign? Looking to transform your business with the implementation of full potential digital marketing?

For any career inquiries, please visit our careers page here.
[contact-form-7 404 "Bulunamadı"]