🧪 Testing — Öğrenme & Mülakat Rehberi

Unit/Integration/Contract/Performance testleri, Testcontainers ve WireMock ile gerçekçi izolasyon; coverage ve mutation ile kalite ölçümü. Hem sahada kullan hem de mülakatta ışılda.

🔎 Genel Bakış

  • Hızlı geri bildirim için unit test, gerçekçi güven için integration.
  • Dış servis bağımlılıklarını stub (WireMock) veya contract ile yönet.
  • DB/Queue gibi gerçek bağımlılıklar için Testcontainers.
  • Kaliteyi coverage + mutation (pitest) ile izle.
  • Performans için Gatling/JMeter ve SLO/SLA metrikleri belirle.

🧱 Test Piramidi

KatmanHedefÖzellikÖrnek
UnitFonksiyonel doğrulukMilisaniye, izoleJUnit5 + Mockito
Service/SliceKatman doğrulamaHızlı, kısmi Spring konteyner@DataJpaTest, @WebMvcTest
IntegrationGerçek bağımlılıklarDakika, daha yavaşSpringBootTest + Testcontainers
ContractAPI uyumuTüketici/sağlayıcı anlaşmasıPact / Spring Cloud Contract
PerformanceKapsite/latencyYük/stress/soakGatling, JMeter

🧩 Unit Test (JUnit 5, AssertJ, Mockito)

İyi Uygulamalar

  • Hızlı, deterministik, izole. Random varsa seed sabitle.
  • Test isimleri davranış anlatmalı: shouldCalculateTotal_whenItemsGiven()
  • Arrange–Act–Assert (AAA), tek assert konusu.
class PriceCalculatorTest {

  private final PriceCalculator calc = new PriceCalculator(new TaxService());

  @Test
  void shouldCalculateTotal_whenItemsGiven() {
    var items = List.of(new Item("A", 100), new Item("B", 50));
    var total = calc.total(items);
    assertThat(total).isEqualTo(159.0); // 150 + %6 vergi + yuvarlama
  }

  @Test
  void shouldThrow_whenEmptyItems() {
    assertThatThrownBy(() -> calc.total(List.of()))
      .isInstanceOf(IllegalArgumentException.class);
  }
}

Mockito ile İzolasyon

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

  @Mock PaymentGateway gateway;
  @InjectMocks OrderService service;

  @Test
  void shouldMarkPaid_whenGatewayOk() {
    when(gateway.pay(any())).thenReturn(PaymentResult.ok());
    var status = service.pay(123L);
    verify(gateway).pay(any());
    assertThat(status).isEqualTo(Status.PAID);
  }
}

🍰 Spring Slice Testleri

  • @WebMvcTest: Sadece MVC katmanı (Controller, Advice). Service/Repo mocklanır.
  • @DataJpaTest: JPA/Hibernate + in-memory DB (H2 tuzaklarına dikkat), veya Testcontainers ile gerçek sürüm.
@WebMvcTest(ProductController.class)
class ProductControllerWebTest {

  @Autowired MockMvc mvc;
  @MockBean ProductService service;

  @Test
  void shouldReturn200_withList() throws Exception {
    when(service.list()).thenReturn(List.of(new Product(1L,"Pen")));
    mvc.perform(get("/products"))
       .andExpect(status().isOk())
       .andExpect(jsonPath("$[0].name").value("Pen"));
  }
}
@DataJpaTest
class ProductRepositoryTest {

  @Autowired TestEntityManager em;
  @Autowired ProductRepository repo;

  @Test
  void shouldFindByName() {
    em.persist(new Product(null,"Pen"));
    var p = repo.findByName("Pen");
    assertThat(p).isPresent();
  }
}
H2 Tuzağı: Prod DB (PostgreSQL/Oracle/MSSQL) ile H2 farkları tip/SQL davranışı bozabilir. Kritik sorgular için Testcontainers ile gerçek sürüme karşı test edin.

🔗 Integration Test (SpringBootTest)

  • Tüm konteyner (port açmadan webEnvironment = MOCK hız kazandırır).
  • Gerçek DB/Queue/HTTP için Testcontainers & WireMock.
  • Test verisi için @Sql / Factory/Builder pattern.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class OrderFlowIT {

  @Autowired MockMvc mvc;

  @Test
  void endToEnd_shouldWork() throws Exception {
    mvc.perform(post("/orders").contentType("application/json")
        .content("{\"sku\":\"A1\",\"qty\":2}"))
       .andExpect(status().isCreated());

    mvc.perform(get("/orders/1"))
       .andExpect(status().isOk())
       .andExpect(jsonPath("$.status").value("CREATED"));
  }
}

🐳 Testcontainers

PostgreSQL ve Kafka Örneği

@Testcontainers
@SpringBootTest
class BillingIT {

  @Container static PostgreSQLContainer<?> pg =
      new PostgreSQLContainer<>("postgres:16-alpine")
         .withDatabaseName("appdb")
         .withUsername("app")
         .withPassword("app");

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

  @DynamicPropertySource
  static void register(DynamicPropertyRegistry r){
    r.add("spring.datasource.url", pg::getJdbcUrl);
    r.add("spring.datasource.username", pg::getUsername);
    r.add("spring.datasource.password", pg::getPassword);
    r.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
  }

  @Test void contextLoads(){ /* ... */ }
}
  • Reusable containers için TESTCONTAINERS_REUSE_ENABLE=true ve ~/.testcontainers.properties.
  • CI’da Docker runner gerekir.

🧵 WireMock (HTTP Stub/Mock)

Standalone (JUnit) Kullanım

@ExtendWith(WireMockExtension.class)
@WireMockSettings(port = 8089)
class ExternalClientTest {

  @RegisterExtension static WireMockExtension wm =
      WireMockExtension.newInstance().options(wireMockConfig().port(8089)).build();

  @Test
  void shouldCallStub() {
    wm.stubFor(get(urlEqualTo("/rates?base=USD"))
      .willReturn(okJson("{\"TRY\": 33.2}")));

    var rate = new RatesClient("http://localhost:8089").getTryRate("USD");
    assertThat(rate).isEqualTo(33.2);
  }
}

JSON Mapping Örneği

{
  "request": { "method": "GET", "urlPath": "/rates", "queryParameters": { "base": { "equalTo": "USD" } } },
  "response": { "status": 200, "headers": { "Content-Type": "application/json" }, "jsonBody": {"TRY":33.2} }
}
İpuçları: Record & Replay ile gerçek istekleri yakala; fixedDelayMilliseconds ile gecikme simüle et; hata senaryolarını (503, timeout) mutlaka test et.

📜 Contract Testing (Pact / Spring Cloud Contract)

Consumer-Driven Contract — Pact

@Pact(consumer = "cart-service")
public RequestResponsePact getProductPact(PactDslWithProvider p) {
  return p.given("product exists")
    .uponReceiving("get product by id")
      .path("/products/42").method("GET")
    .willRespondWith().status(200)
      .headers(Map.of("Content-Type","application/json"))
      .body("{\"id\":42,\"name\":\"Pen\"}")
    .toPact();
}

@PactTestFor(providerName="product-service", port="9090")
@Test void verifyContract(MockServer server){
  ProductClient c = new ProductClient(server.getUrl());
  Product p = c.get(42L);
  assertThat(p.getName()).isEqualTo("Pen");
}

Spring Cloud Contract — Producer

Contract.make {
  request {
    method 'GET'
    url '/products/42'
  }
  response {
    status 200
    headers { contentType(applicationJson()) }
    body(id: 42, name: 'Pen')
  }
}

Producer tarafı sözleşmeden stub üretir; consumer testlerinde bu stub kullanılır. CI’da sözleşme doğrulaması kırmızıya düşürür.

🚀 Performance Test (Gatling / JMeter)

Hedefler

  • Latency: p50/p95/p99; Throughput: RPS; Error Rate & Resource: CPU/RAM/GC/IO.
  • SLA/SLO tanımla (örn. p95 < 300ms).

Gatling Örneği

class ProductSimulation extends Simulation {
  val httpConf = http.baseUrl("http://localhost:8080")
  val scn = scenario("List products")
    .exec(http("GET /products").get("/products").check(status.is(200)))

  setUp(
    scn.inject(
      rampUsersPerSec(0) to 200 during (60 seconds),
      constantUsersPerSec(200) during (120 seconds)
    )
  ).protocols(httpConf)
}
İpuçları: Warmup → ölçü; sistem kaynaklarını izleyin (JFR/Async-Profiler). GC tuning ve DB index’leri performansı dramatik etkiler.

📊 Coverage & Mutation

JaCoCo (Line/Branch)

<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.11</version>
  <executions>
    <execution>
      <goals><goal>prepare-agent</goal></goals>
    </execution>
    <execution>
      <id>report</id>
      <phase>verify</phase>
      <goals><goal>report</goal></goals>
    </execution>
  </executions>
</plugin>

Mutation Testing (PIT)

<plugin>
  <groupId>org.pitest</groupId>
  <artifactId>pitest-maven</artifactId>
  <version>1.15.8</version>
  <configuration>
    <targetClasses><param>com.acme.*</param></targetClasses>
    <targetTests><param>com.acme.*</param></targetTests>
    <mutators><mutator>STRONGER</mutator></mutators>
    <threads>4</threads>
  </configuration>
</plugin>

Not: Yüksek coverage, iyi test demek değildir; mutation score gerçek kaliteyi gösterir.

🤖 CI Entegrasyonu

  • Stages: unit → slice → integration → contract → performance (opsiyonel nightly).
  • Raporlar: JUnit XML, JaCoCo HTML/XML, PIT raporu, Gatling grafikleri.
name: build
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      docker: { }
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '21' }
      - name: Run tests
        run: mvn -B -DskipITs=false verify
      - name: Upload reports
        uses: actions/upload-artifact@v4
        with:
          name: reports
          path: |
            **/target/site/jacoco/*
            **/target/pit-reports/*
            **/target/gatling/*

❓ Mülakat Soruları & Kısa Yanıtlar

1) Unit vs Integration farkı?

Unit izole ve hızlı; integration gerçek bağımlılıklarla sistem davranışını doğrular.

2) H2 yerine neden Testcontainers?

Prod DB davranış farklılıklarını (tip, index, fonksiyon, SQL dialekt) yakalamak için.

3) WireMock vs Contract Testing?

WireMock lokal stub; contract (Pact/SCC) iki tarafın anlaştığı API sözleşmesini CI'da garanti eder.

4) Mutation testing neyi ölçer?

Testlerin hataları yakalama gücünü (öldürülen mutant oranı); satır kapsamından daha anlamlıdır.

5) Flaky test nasıl azaltılır?

Deterministik saat/tarih, sabit seed, async beklemelerde timeouts/polling, izolasyon ve temiz test verisi.

6) Performance testte hangi metrikler?

p50/p95/p99 latency, throughput (RPS), error rate, CPU/RAM/GC, DB bekleme süreleri.