Integration testi, bir yazılım uygulamasının farklı bileşenlerinin veya modüllerinin bir araya getirildiğinde doğru şekilde çalışıp çalışmadığını kontrol etmek için yapılan bir test türüdür. Bu test, bileşenlerin birbirleriyle ve sistemin geri kalanıyla uyumlu bir şekilde çalışıp çalışmadığını değerlendirir.
Integration testinin ana amaçları şunlardır:
Bileşenler arası iletişimi doğrulamak
Veri akışını kontrol etmek
Bileşenlerin birlikte çalıştığında beklenen işlevselliği sağladığını görmek
Arayüzler arasındaki uyumu test etmek
Entegrasyon sırasında ortaya çıkabilecek hataları tespit etmek
Bu testler genellikle birim testlerinden sonra ve sistem testlerinden önce gerçekleştirilir.
Spring Cloud mikroservis projesinde integration testi yapmak için birkaç önemli adım ve yaklaşım vardır. Size bu süreci açıklayacağım:
Test Ortamının Hazırlanması:
Genellikle Docker kullanılarak bağımlılıklar (veritabanı, message broker gibi) ayağa kaldırılır.
TestContainers kütüphanesi bu iş için sıkça tercih edilir.
Test Konfigürasyonu:
@SpringBootTest annotation'ı kullanılarak tüm uygulama context'i yüklenir.
@AutoConfigureMockMvc ile HTTP isteklerini simüle etmek için MockMvc hazırlanır.
Bağımlılıkların Yönetimi:
Diğer mikroservislere olan bağımlılıklar için mock servisler oluşturulabilir.
Wiremock gibi araçlar kullanılarak dış servisler simüle edilebilir.
Veritabanı Testleri:
In-memory veritabanı (H2 gibi) kullanılabilir.
Flyway veya Liquibase ile şema yönetimi yapılabilir.
Mesajlaşma Testleri:
Kafka, RabbitMQ gibi mesajlaşma sistemleri için embedded versiyonlar kullanılabilir.
API Testleri:
REST API'ler MockMvc ile test edilir.
GraphQL API'ler için özel test araçları kullanılabilir.
Basit bir örnek göstermek isterim. Aşağıdaki kod, bir kullanıcı servisi için integration test örneğidir:
@SpringBootTest
@AutoConfigureMockMvc
public class UserServiceIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserRepository userRepository;
@Test
public void testCreateUser() throws Exception {
String userJson = "{\"username\":\"testuser\",\"email\":\"test@example.com\"}";
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(userJson))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.username").value("testuser"))
.andExpect(jsonPath("$.email").value("test@example.com"));
User savedUser = userRepository.findByUsername("testuser");
assertNotNull(savedUser);
assertEquals("test@example.com", savedUser.getEmail());
}
@Test
public void testGetUser() throws Exception {
User user = new User("existinguser", "existing@example.com");
userRepository.save(user);
mockMvc.perform(get("/api/users/" + user.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("existinguser"))
.andExpect(jsonPath("$.email").value("existing@example.com"));
}
}
Bu örnekte:
@SpringBootTest ile tüm uygulama context'i yüklenir.
@AutoConfigureMockMvc ile HTTP isteklerini simüle etmek için MockMvc hazırlanır.
UserRepository gerçek veritabanı ile etkileşime girer (in-memory veya test veritabanı olabilir).
İki test metodu bulunmaktadır:
testCreateUser(): Yeni bir kullanıcı oluşturma işlemini test eder.
testGetUser(): Var olan bir kullanıcıyı getirme işlemini test eder.
Bu tür testler, servisin beklenen şekilde çalıştığını, veritabanı işlemlerinin doğru gerçekleştiğini ve API'nin düzgün yanıt verdiğini doğrular.
Daha karmaşık senaryolarda, mesajlaşma sistemleri, dış servis çağrıları ve diğer mikroservislerle etkileşimler de test edilebilir. Bu durumda, test konfigürasyonu daha kapsamlı olacaktır.
Bu sefer, mikroservisler arası iletişimi, mesajlaşma sistemini ve dış servis çağrılarını içeren bir senaryo düşünelim.
Örneğimizde, bir e-ticaret sisteminin sipariş servisi olsun. Bu servis:
Yeni siparişleri alır
Stok servisini kontrol eder
Ödeme servisini çağırır
Sipariş onaylandığında bir Kafka topic'ine mesaj gönderir
İşte bu senaryoyu test eden bir integration test örneği:
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(locations = "classpath:application-test.properties")
@DirtiesContext
public class OrderServiceIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private OrderRepository orderRepository;
@MockBean
private StockServiceClient stockServiceClient;
@MockBean
private PaymentServiceClient paymentServiceClient;
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Autowired
private EmbeddedKafkaBroker embeddedKafkaBroker;
private KafkaMessageListenerContainer<String, String> container;
private BlockingQueue<ConsumerRecord<String, String>> records;
@BeforeEach
void setUp() {
// Kafka consumer ayarları
Map<String, Object> configs = new HashMap<>(KafkaTestUtils.consumerProps("test-group", "true", embeddedKafkaBroker));
ConsumerFactory<String, String> consumerFactory = new DefaultKafkaConsumerFactory<>(configs, new StringDeserializer(), new StringDeserializer());
ContainerProperties containerProperties = new ContainerProperties("order-confirmed");
container = new KafkaMessageListenerContainer<>(consumerFactory, containerProperties);
records = new LinkedBlockingQueue<>();
container.setupMessageListener((MessageListener<String, String>) records::add);
container.start();
}
@AfterEach
void tearDown() {
container.stop();
}
@Test
public void testCreateOrder() throws Exception {
// Mock dış servis yanıtları
when(stockServiceClient.checkStock(anyString(), anyInt())).thenReturn(true);
when(paymentServiceClient.processPayment(anyString(), anyDouble())).thenReturn(true);
String orderJson = "{\"productId\":\"prod-1\",\"quantity\":2,\"totalPrice\":100.00}";
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(orderJson))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.status").value("CONFIRMED"));
// Veritabanı kontrolü
Order savedOrder = orderRepository.findByProductId("prod-1");
assertNotNull(savedOrder);
assertEquals(OrderStatus.CONFIRMED, savedOrder.getStatus());
// Kafka mesaj kontrolü
ConsumerRecord<String, String> record = records.poll(5, TimeUnit.SECONDS);
assertNotNull(record);
assertEquals("order-confirmed", record.topic());
assertTrue(record.value().contains("prod-1"));
// Dış servis çağrıları kontrolü
verify(stockServiceClient).checkStock("prod-1", 2);
verify(paymentServiceClient).processPayment(anyString(), eq(100.00));
}
@Test
public void testCreateOrderWithInsufficientStock() throws Exception {
when(stockServiceClient.checkStock(anyString(), anyInt())).thenReturn(false);
String orderJson = "{\"productId\":\"prod-2\",\"quantity\":5,\"totalPrice\":250.00}";
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(orderJson))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value("Insufficient stock"));
// Veritabanı kontrolü
Order savedOrder = orderRepository.findByProductId("prod-2");
assertNull(savedOrder);
// Ödeme servisi çağrılmadığından emin olma
verify(paymentServiceClient, never()).processPayment(anyString(), anyDouble());
}
}
Bu örnek, daha karmaşık bir mikroservis ortamını simüle eder ve şunları test eder:
Sipariş oluşturma API'si
Stok servisi ile iletişim (mock edilmiş)
Ödeme servisi ile iletişim (mock edilmiş)
Kafka'ya mesaj gönderimi
Veritabanı işlemleri
Önemli noktalar:
@MockBean kullanarak dış servisleri (stok ve ödeme) mock ediyoruz.
Embedded Kafka broker kullanarak gerçek mesajlaşmayı test ediyoruz.
Farklı senaryolar için ayrı test metotları yazıyoruz (başarılı sipariş ve stok yetersizliği).
Asenkron işlemleri (Kafka mesajı) test etmek için bekleme mekanizması kullanıyoruz.
Bu tür kapsamlı testler, mikroservis mimarisinde farklı bileşenlerin bir arada nasıl çalıştığını doğrulamak için çok önemlidir. Ayrıca, bu testler sayesinde:
Servisler arası iletişimin doğru çalıştığından emin oluruz.
Mesajlaşma sisteminin beklenen şekilde kullanıldığını doğrularız.
Hata senaryolarının düzgün yönetildiğini kontrol ederiz.
Veritabanı işlemlerinin transaction yönetimini test ederiz.
Test Verilerinin Yönetimi:
Test verilerinin doğru yönetimi, integration testlerinin güvenilirliği ve tekrarlanabilirliği için kritiktir. İşte bazı önemli stratejiler:
a) Veritabanı Sıfırlama:
Her test öncesinde veritabanını sıfırlamak için @DirtiesContext veya @Transactional annotasyonları kullanılabilir.
Alternatif olarak, özel bir @BeforeEach metodu ile veritabanı temizlenebilir.
b) Test Veri Setleri:
SQL script'leri veya Java tabanlı veri yükleyicileri kullanarak test verilerini yükleyin.
Flyway veya Liquibase gibi araçlarla şema ve veri yönetimini otomatikleştirin.
Örnek Uygulama:
@SpringBootTest
@Sql(scripts = "/test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
public class OrderServiceIntegrationTest {
@Autowired
private TestDataLoader testDataLoader;
@BeforeEach
void setUp() {
testDataLoader.loadTestData();
}
// ... test metotları ...
}
// TestDataLoader sınıfı
@Component
public class TestDataLoader {
@Autowired
private OrderRepository orderRepository;
public void loadTestData() {
Order order1 = new Order("prod-1", 2, 100.00);
Order order2 = new Order("prod-2", 1, 50.00);
orderRepository.saveAll(Arrays.asList(order1, order2));
}
}
<!-- pom.xml -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<parallel>classes</parallel>
<threadCount>4</threadCount>
<perCoreThreadCount>true</perCoreThreadCount>
</configuration>
</plugin>
</plugins>
</build>
<!-- application-test.properties -->
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
}
}
stage('Unit Tests') {
steps {
sh 'mvn test'
}
}
stage('Integration Tests') {
steps {
sh 'mvn verify -Dspring.profiles.active=test'
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh './deploy.sh'
}
}
}
post {
always {
junit '**/target/surefire-reports/*.xml'
jacoco()
}
}
}