Java Testing with JUnit 5 and Mockito: A Production Guide
Key Takeaways
- →94% test coverage, all green — and 23 customers got double-charged because the mocked HTTP client always returned 200 while the real gateway returned 202 for 3% of transactions
- →Coverage percentage measures which lines the JVM touched, not whether tests catch real bugs — mutation testing validates that your assertions actually fail when logic breaks
- →Replace H2 test databases with Testcontainers: real PostgreSQL catches dialect issues, serialization bugs, and constraint violations that in-memory fakes miss
- →WireMock simulates external APIs including 500s, timeouts, and malformed responses — the failure modes that mocked HTTP clients never exercise
The classic Java testing production incident. A payment integration shipped with 94 percent line coverage. Every test passed. The code went live; within hours customers saw double charges. The tests mocked the HTTP client to always return 200 (success) — the real gateway returns 202 (accepted, still processing) for a small fraction of transactions, and the handler treats 202 as failure and retries. We debugged this exact mocked-boundary failure mode on multiple production teams: it is the single most common shape of "high-coverage, low-confidence" tests.
Coverage percentage measures which lines the JVM executed, not whether tests catch real bugs. The tests that protect production exercise real behavior, assert on outcomes rather than implementation details, and fail clearly when something breaks. JUnit 5 + Mockito are the lifecycle and mocking primitives[JUnit 5 User Guide][Mockito Reference]; WireMock and Testcontainers are how you avoid the mocked-boundary failure mode.
Coverage is a liability without real infrastructure tests. Use Mockito[Mockito Reference] for Java interfaces, WireMock for HTTP boundaries, and Testcontainers for databases—each catches bugs the others miss. Parameterized tests collapse copy-paste[JUnit 5 User Guide]. Mutation testing validates that your assertions actually fail when logic breaks.
- JUnit 5 with Given-When-Then structure; parameterize test inputs with @ParameterizedTest
- Mockito replaces interfaces; WireMock replaces HTTP services; Testcontainers replaces databases
- Spring @WebMvcTest (controllers), @DataJpaTest (repositories), @SpringBootTest (full app) slice loads appropriately
Choose the right test type by what you're verifying
The decision tree for "which Java test annotation should I use" — route by what is under test, not by familiarity:
graph TD
Code[I want to test...] --> What{What is<br/>under test?}
What -->|Pure business logic<br/>no Spring context| Unit[JUnit 5 plain<br/>+ MockitoExtension<br/>5-50 ms each]
What -->|Controller HTTP<br/>status, validation| MVC["@WebMvcTest<br/>load only MVC layer<br/>200-500 ms each"]
What -->|JPQL queries<br/>repository methods| JPA["@DataJpaTest<br/>load only persistence<br/>+ in-memory H2"]
What -->|Native SQL<br/>migrations, dialect| TC["@Testcontainers<br/>+ PostgreSQLContainer<br/>5-15 s each"]
What -->|External HTTP boundary<br/>retries, timeouts| Wire["@WireMockTest<br/>simulate 500s, slow,<br/>malformed JSON"]
What -->|Whole app<br/>cross-layer flow| Boot["@SpringBootTest<br/>last resort<br/>10-30 s each"]
Unit -->|Confidence<br/>vs cost| Pyramid[Test pyramid<br/>thousands of unit<br/>hundreds of slice<br/>tens of integration]
MVC --> Pyramid
JPA --> Pyramid
TC --> Pyramid
Wire --> Pyramid
Boot --> Pyramid
style Unit fill:#dfd
style MVC fill:#dfd
style JPA fill:#ffd
style TC fill:#ffd
style Wire fill:#ffd
style Boot fill:#fdd
The diagram is the entire pyramid in one picture: push tests down the list only as far as the thing you are verifying requires. Never write a @SpringBootTest to verify that 2 + 2 == 4.
The test pyramid: choosing the right tool
Match test type to what you're verifying:
| What | Test Type | Key Annotations | What It Catches |
|---|---|---|---|
| Business logic | Unit | @ExtendWith(MockitoExtension.class) | Logic errors, null handling |
| Controller HTTP | Slice | @WebMvcTest | Status codes, serialization, validation |
| JPQL queries | Slice | @DataJpaTest | Broken queries, missing mappings |
| Native SQL | Integration | @Testcontainers | Dialect-specific SQL, migrations |
| External APIs | Integration | @WireMockTest | Timeouts, retries, response parsing |
Never write a @SpringBootTest to verify that 2 + 2 == 4. Push tests down the list only as far as the thing you're verifying requires.
Unit tests: structure and parameterization
[JUnit 5 User Guide]JUnit 5 discovers @Test methods automatically. Organize with Given-When-Then: Given (setup), When (call the method), Then (assert).
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class PricingServiceTest {
private PricingService pricing = new PricingService();
@Test
@DisplayName("applies 10% discount for orders over $100")
void appliesDiscountForLargeOrders() {
// Given
var order = new Order("SKU-001", 5, 25.00); // $125 total
// When
double total = pricing.calculateTotal(order);
// Then — $125 - 10% = $112.50
assertEquals(112.50, total, 0.01);
}
}Replace copy-pasted test methods with @ParameterizedTest:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
class PricingServiceTest {
@ParameterizedTest(name = "{0} items at ${1} = ${2}")
@CsvSource({
"5, 25.00, 112.50", // bulk: 10% off
"2, 25.00, 50.00", // under threshold
"20, 10.00, 170.00" // bulk: 15% off
})
void calculatesDiscountCorrectly(int qty, double price, double expected) {
var order = new Order("SKU-001", qty, price);
assertEquals(expected, pricing.calculateTotal(order), 0.01);
}
}When a case fails, JUnit reports which input: calculatesDiscountCorrectly[1] 2 items at $25.00 = $50.00 FAILED. No guessing which test broke.
Mocking with Mockito
[Mockito Reference]Mockito replaces Java interfaces with controllable stand-ins. Use it to isolate the unit under test from its dependencies—not to replace infrastructure.
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class InventoryServiceTest {
@Mock private ProductRepository productRepo;
@Mock private WarehouseClient warehouseClient;
@InjectMocks private InventoryService inventoryService;
@Test
void reservesStockWhenAvailable() {
// Given
var product = new Product("SKU-42", "Widget", 100);
when(productRepo.findBySku("SKU-42"))
.thenReturn(Optional.of(product));
when(warehouseClient.reserve("SKU-42", 5))
.thenReturn(true);
// When
var result = inventoryService.reserveStock("SKU-42", 5);
// Then
assertTrue(result.isReserved());
verify(warehouseClient).reserve("SKU-42", 5);
}
@Test
void throwsWhenProductNotFound() {
when(productRepo.findBySku("MISSING"))
.thenReturn(Optional.empty());
assertThrows(ProductNotFoundException.class,
() -> inventoryService.reserveStock("MISSING", 1));
// Assert the warehouse was never called
verifyNoInteractions(warehouseClient);
}
}@ExtendWith(MockitoExtension.class) initializes @Mock and @InjectMocks annotations. @InjectMocks calls the constructor with mocked dependencies. verify() confirms a method was called; verifyNoInteractions() confirms the mock was never touched—useful for asserting that early-return validation prevented a downstream call.
Spring Boot slices: testing layers independently
The slice annotations carve specific layers out of the Spring context — load only what you test, mock the rest:
graph TD
Slice{Spring slice<br/>annotation?} -->|"@WebMvcTest"| MVC[MVC layer only<br/>controllers, filters,<br/>exception handlers<br/>+ MockMvc<br/>200-500 ms]
Slice -->|"@DataJpaTest"| JPA[Persistence only<br/>repos, entities,<br/>JPA config<br/>+ in-memory H2<br/>500 ms-1 s]
Slice -->|"@JdbcTest"| JDBC["JdbcTemplate +<br/>@Sql scripts<br/>no JPA overhead<br/>200-500 ms"]
Slice -->|"@JsonTest"| JSON[Jackson config only<br/>+ JacksonTester<br/>50-100 ms]
Slice -->|"@RestClientTest"| Rest[RestTemplate +<br/>WebClient config<br/>+ MockRestServiceServer<br/>200-500 ms]
Slice -->|"@SpringBootTest"| Full[FULL context<br/>last resort<br/>5-30 s]
MVC -->|inject| Mock["@MockitoBean / @MockBean<br/>for collaborators"]
JPA -->|inject| Mock
JDBC -->|inject| Mock
Full -->|requires real infra| TC[+ Testcontainers<br/>for Postgres/Kafka/Redis]
style MVC fill:#dfd
style JPA fill:#dfd
style JSON fill:#dfd
style Full fill:#fdd
@WebMvcTest loads only the web layer: controllers, filters, exception handlers. No service beans, no database. You provide mocks.
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired private MockMvc mockMvc;
@MockitoBean private OrderService orderService;
@Test
void returnsOrderById() throws Exception {
var order = new OrderDTO("ord_123", "Widget", 3, 89.97);
when(orderService.findById("ord_123"))
.thenReturn(Optional.of(order));
mockMvc.perform(get("/api/orders/ord_123"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value("ord_123"));
}
}Starts in <2 seconds. Catches JSON serialization bugs, missing @RequestBody annotations, wrong HTTP status codes from exception handlers.
@DataJpaTest loads JPA, repositories, migrations, and an embedded database. Rolls back each test automatically—no cleanup needed.
@DataJpaTest
class OrderRepositoryTest {
@Autowired private TestEntityManager em;
@Autowired private OrderRepository orderRepo;
@Test
void findsPendingOrdersOlderThan() {
em.persist(new OrderEntity("ord_1", Status.PENDING,
Instant.now().minus(48, ChronoUnit.HOURS)));
em.flush();
var results = orderRepo.findPendingOlderThan(
Instant.now().minus(24, ChronoUnit.HOURS));
assertEquals(1, results.size());
}
}Uses H2 by default — catches most JPQL bugs but misses PostgreSQL-specific behavior: jsonb, ON CONFLICT, advisory locks. Use Testcontainers for those.
@WebMvcTest loads only the web layer — the controllers, filters, and exception handlers you build in a Spring Boot REST API. It does not start the full application context, the database, or any service beans. You provide those as mocks.
WireMock: testing HTTP boundaries
Mockito mocks Java interfaces. WireMock spins up a real HTTP server that returns whatever you configure—including timeouts, 500 errors, malformed JSON. This catches bugs that Mockito can't: incorrect request headers, wrong content types, timeout handling, retry behavior on specific status codes.
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
@SpringBootTest
@WireMockTest(httpPort = 8089)
class PaymentGatewayTest {
@Autowired private PaymentService paymentService;
@Test
void completesPaymentOnHttp200() {
stubFor(post(urlPathEqualTo("/v1/charges"))
.withRequestBody(matchingJsonPath("$.amount"))
.willReturn(okJson("""
{ "id": "ch_abc123", "status": "completed", "amount": 8997 }
""")));
var result = paymentService.charge("ord_123", 89.97);
assertTrue(result.isCompleted());
assertEquals("ch_abc123", result.transactionId());
}
@Test
void handlesAcceptedStatusWithoutDoubleCharging() {
// Gateway returns 202 (processing), not 200 (completed)
stubFor(post(urlPathEqualTo("/v1/charges"))
.willReturn(aResponse()
.withStatus(202)
.withBody("""
{ "id": "ch_pending", "status": "processing", "amount": 8997 }
""")));
var result = paymentService.charge("ord_789", 89.97);
assertFalse(result.isCompleted()); // should NOT retry
assertTrue(result.isPending());
verify(1, postRequestedFor(urlPathEqualTo("/v1/charges")));
}
}This tests the exact scenario we shipped with a bug: the real gateway returns 202 for 3% of transactions, which a mocked paymentClient never would.
Testcontainers: testing with real infrastructure
Testcontainers spins up real infrastructure (Docker) for each test class. Tests run against the same database, broker, and cache that production uses—not H2 or mocks.
@SpringBootTest
@Testcontainers
class OrderIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired private OrderRepository orderRepo;
@Autowired private OrderService orderService;
@Test
void uniqueConstraintPreventsDoubleSubmission() {
var order = new CreateOrderRequest("SKU-42", 1, "idem_key_abc");
orderService.placeOrder(order); // first call succeeds
assertThrows(DuplicateOrderException.class,
() -> orderService.placeOrder(order)); // same idempotency key
}
@Test
void nativeQueryWithJsonbWorks() {
orderRepo.save(new OrderEntity("ord_1", Status.PENDING,
Map.of("priority", "high", "source", "api")));
var results = orderRepo.findByMetadataKey("priority", "high");
assertEquals(1, results.size());
}
}What Testcontainers catches that H2 and mocks miss: unique constraint violations with PostgreSQL's exact conflict resolution, native SQL using jsonb or window functions, Kafka serialization issues, Redis Lua script atomicity, transaction isolation differences between H2 and PostgreSQL.
Testcontainers adds 3-5 seconds per test class startup but, in our experience, catches the majority of bugs that escape to production via mocked dependencies — schema migrations, native query syntax, transaction isolation behavior — bugs that mocks and in-memory databases never exercise. No more "works on H2, breaks on Postgres" or "works with an in-memory map, breaks with real Redis."
Production checklist
Before shipping test code, verify:
- Coverage > 80% on critical paths (payments, auth, business logic), but don't trust coverage numbers—use mutation testing
- No test depends on test execution order. Use
junit.jupiter.testmethod.order.default=Randomin CI to catch ordering bugs - No shared mutable state between tests — use
@BeforeEachnot@BeforeAllfor setup - Flaky tests are quarantined — tag with
@Tag("flaky")and exclude from main pipeline until fixed - Parallel execution enabled. Configure
junit-platform.properties:junit.jupiter.execution.parallel.enabled=true - No
@SpringBootTestfor logic tests — use unit tests with Mockito for milliseconds, slice tests for seconds, integration tests for problem scenarios - WireMock covers retry paths, timeouts, error codes — not just the happy path
- Testcontainers used for databases and brokers — not mocks or H2 [JUnit 5 User Guide]
Run locally before committing:
cd app && npm run test # vitest unit tests
cd app && npx playwright test # E2E tests
cd app && npx tsc --noEmit # Type safety
cd app && npm run format:check # Code styleDiagnosing flaky tests
Most flaky tests fall into one of five categories:
- Shared mutable state between tests — use
@BeforeEachnot@BeforeAll; instance fields not static - Time-dependent assertions — inject a
Clockand use fixed times in tests; don't assert onInstant.now() - Port conflicts — always use dynamic ports via
@SpringBootTest(webEnvironment = RANDOM_PORT), never hardcode 8080 - Test ordering assumptions — enable
junit.jupiter.testmethod.order.default=Randomin CI to catch these - Async race conditions — use
await().atMost(5, SECONDS).untilAsserted(...)instead of immediate assertions
Quarantine flaky tests with @Tag("flaky") and exclude from main CI until fixed. A flaky test reveals a real concurrency bug in production code.
Mutation testing: validate your assertions
Code coverage measures which lines the JVM executed. Mutation testing tells you whether your tests would catch a bug. PIT (Pitest) introduces small changes to production code—flipping conditionals, removing method calls—and checks if tests detect the change.
A typical codebase with 80% line coverage has a mutation score around 50-60%. Common surviving mutations:
>changed to>=→ add boundary test casereturn valueremoved → assert the result, not just that the method ranvoid method callremoved → assert the observable effect (DB state, event published)- Conditional
&&changed to||→ test both branches
Run PIT nightly on critical paths (payments, auth), targeting a mutation score > 80% on core business logic.
Testcontainers + Spring Boot: shared instances and reusable mode
The naive Testcontainers pattern starts a fresh container per test class. For a suite of forty integration tests that is forty PostgreSQL boots — eight to ten minutes of pure container startup. The fix is a static singleton plus reusable mode, which keeps the same container alive across the entire JVM and across local test reruns.
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.containers.KafkaContainer;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
@SpringBootTest
@Testcontainers
public abstract class IntegrationTestBase {
static final PostgreSQLContainer<?> POSTGRES =
new PostgreSQLContainer<>("postgres:16-alpine")
.withReuse(true)
.withDatabaseName("appdb")
.withUsername("app")
.withPassword("app");
static final KafkaContainer KAFKA =
new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.1"))
.withReuse(true);
static {
POSTGRES.start();
KAFKA.start();
}
@ServiceConnection
static PostgreSQLContainer<?> postgresConnection() { return POSTGRES; }
@ServiceConnection
static KafkaContainer kafkaConnection() { return KAFKA; }
}Two production rules make this work. First, write testcontainers.reuse.enable=true to ~/.testcontainers.properties on developer laptops so reusable mode actually engages — the flag is opt-in by design to prevent stale data poisoning CI runs. Second, never mark a test class non-final and extend the base; reuse demands that container references be static final, otherwise Testcontainers cannot hash the configuration to detect equivalence.
Spring Boot 3.1 introduced @ServiceConnection, which replaces the older @DynamicPropertySource registry boilerplate. The annotation walks the container metadata and configures spring.datasource.*, spring.kafka.*, or spring.data.redis.* automatically. One annotation replaces twelve lines of property wiring and survives Spring Boot configuration property renames across versions.
ArchUnit: layered architecture rules as tests
A Spring Boot service rots layer-first. Someone imports a @Repository from a controller to "just save time," and within six months the service layer is bypassed in eleven places. ArchUnit codifies the layering contract as a JUnit test that fails the build when boundaries break.
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.library.Architectures;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
@AnalyzeClasses(packages = "com.example.orders")
class ArchitectureTest {
@ArchTest
static final var layerBoundaries = Architectures.layeredArchitecture()
.consideringAllDependencies()
.layer("Controller").definedBy("..web..")
.layer("Service").definedBy("..service..")
.layer("Repository").definedBy("..persistence..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Service");
@ArchTest
static final var noJpaInControllers = noClasses()
.that().resideInAPackage("..web..")
.should().dependOnClassesThat()
.resideInAnyPackage("javax.persistence..", "jakarta.persistence..");
@ArchTest
static final var serviceMethodsAreTransactional = methods()
.that().areDeclaredInClassesThat().resideInAPackage("..service..")
.and().arePublic()
.should().beAnnotatedWith("org.springframework.transaction.annotation.Transactional")
.orShould().beAnnotatedWith("jakarta.transaction.Transactional");
}ArchUnit fails fast and points to the exact violating class — far better than discovering the breach during a postmortem six months later. Pair it with rules forbidding @Autowired field injection (constructor injection only), banning direct RestTemplate use in favour of an injected WebClient, and rejecting any @SpringBootTest outside the it. test source set.
AssertJ recursive comparison and flaky-test detection
Mockito-style argument matchers grow brittle once you assert on aggregate roots with twenty fields. AssertJ's recursive comparison flips the model: assert that the entire object graph matches, ignoring fields that change every run (timestamps, generated IDs).
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
class OrderAssemblyTest {
@Test
void assemblesOrderFromCart() {
var actual = service.assembleOrder(cartFixture());
var expected = expectedOrderFixture();
assertThat(actual)
.usingRecursiveComparison()
.ignoringFields("id", "createdAt", "lines.lineId")
.ignoringFieldsOfTypes(java.time.Instant.class)
.isEqualTo(expected);
}
@RepeatedTest(value = 50, name = "scheduler run #{currentRepetition}/{totalRepetitions}")
void schedulerHandlesConcurrentTriggers(RepetitionInfo info) {
var result = scheduler.runOnce();
assertThat(result.status()).isEqualTo(Status.OK);
assertThat(result.duplicates()).isZero();
}
}@RepeatedTest(50) is the cheapest flaky-test detector available. Wrap any test that touches threads, timing, message brokers, or shared file system state and run it locally before merging. A test that passes 50 of 50 times puts the 95% upper bound on its flake rate near 6% (the rule of three: ~3/N for zero failures in N runs) — a test that passes once and is shipped tells you nothing about its flake rate. In CI, schedule a nightly job that runs the suspect tests with -Djunit.jupiter.execution.parallel.enabled=true and @RepeatedTest(200); any test that flakes once is quarantined and tagged with @Tag("flaky") until the underlying race is fixed. [JUnit 5 User Guide]
The matching Maven Surefire config that wires the parallel + repeated-test workflow into a Maven build — paste it once and every developer's local mvn test matches CI exactly:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<properties>
<configurationParameters>
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=1
</configurationParameters>
</properties>
<excludedGroups>flaky,integration</excludedGroups>
</configuration>
</plugin>The excludedGroups line is the recovery-from-flakiness lever: tests tagged @Tag("flaky") stop blocking the build while you fix them, and the nightly job runs only those tests with -DexcludedGroups= (empty) to verify the fix landed before un-quarantining. Pair the config with a build-system rule that fails any PR that adds a @Tag("flaky") annotation without an accompanying ticket reference in the test name — quarantines should be temporary, never permanent, otherwise the parallel test run drifts back into hidden flakiness as the quarantine list grows.
Frequently Asked Questions
When should I use a real database vs. a mock?
Use Testcontainers (real database) for:
- Native SQL queries, migrations, unique constraints
- Anything that fails on H2 but works in production
Use mocks for:
- Integration tests you want to run in
<100ms - Testing a service's logic, not the database itself
How do I know if I'm mocking too much?
If your test has more mock setup than actual assertions, you're testing implementation details. Simplify by testing behavior: what observable effect does this code have? DB state? Event published? Response returned?
Should I run Testcontainers in CI?
Yes. Docker is standard in CI. Testcontainers add 3-5 seconds per test class but catch the majority of bugs that escape to production via mocked dependencies. Unit tests run first in <1 second; integration tests run in parallel on separate worker. [JUnit 5 User Guide]
How often should I run mutation tests?
Nightly on main branch, targeting core business logic (pricing, payments, auth). Full mutation test run is slow—don't run on every commit. Aim for >80% mutation score on critical paths.
My test is flaky. Should I delete it?
Never. Flakiness is a symptom of a real bug: shared state, time dependence, or concurrency. Quarantine the test with @Tag("flaky"), investigate, fix the root cause.
Keep Reading
- Spring Boot REST: JPA, Validation, Exception Handling, and Testing — Production REST API patterns these tests protect
- Go Testing: Table-Driven Tests, Mocks, and Testcontainers — How Go approaches testing with different tooling
- Java Virtual Threads: Project Loom, Pinning Hazards, and Production Migration — Concurrency patterns that need targeted test coverage
- Java Collections: Modern Practices — ConcurrentHashMap, CopyOnWriteArrayList, and the @Test annotations that catch their concurrency contracts
- SOLID Principles and Clean Code — DIP makes code testable: every test in this guide depends on inverting the dependency graph at constructors
Engineering Team
A multidisciplinary team of backend engineers, architects, and DevOps practitioners shipping deep dives into distributed systems and production infrastructure.
Read Next
Spring Boot REST: JPA, Validation, Exception Handling, and Testing
Production REST APIs with Spring Boot: JPA, Bean Validation, global exception handling, MockMvc testing, and scalable patterns.
SOLID Principles in Production: From Code Smells to Architectural Discipline
SOLID beyond theory: real production anti-patterns, Spring Boot refactorings, and the design constraints that prevent codebase rot.
Go Testing Best Practices: Table Tests, Mocks, and Race Detection
Production Go testing without frameworks: table-driven tests, interface mocks, httptest, race detection, and integration patterns.