What's your process for writing testable Java code (mention JUnit/Mockito basics)?

8 minintermediatetestingjunitmockitobehavioral

Quick Answer

Favor dependency injection (constructor injection especially) over hard-coded 'new' calls to collaborators, so tests can substitute mocks/fakes; keep methods focused on one responsibility so each test stays small and readable; write tests with JUnit (5's @Test, @BeforeEach, parameterized tests) covering the happy path plus meaningful edge cases; use Mockito to mock external dependencies (databases, HTTP clients) so unit tests stay fast and deterministic; and reserve integration tests for verifying real wiring between components.

Detailed Answer

Designing for testability:

  • Depend on abstractions, injected rather than constructed internally — a class that does new PaymentGateway() inside a method can't be tested without hitting the real gateway; a class that receives a PaymentGateway via its constructor can be tested with a mock or fake in its place.
class OrderService {
    private final PaymentGateway gateway;
    OrderService(PaymentGateway gateway) { this.gateway = gateway; } // constructor injection
}
  • Keep methods focused on a single responsibility — smaller, more focused methods are naturally easier to write small, readable tests for, and don't require an elaborate setup just to exercise one behavior.
  • Avoid static/global mutable state where possible — it's difficult to isolate between tests and often forces test execution order dependencies.

Writing the tests (JUnit 5):

@Test
void chargesCorrectAmountOnSuccessfulOrder() {
    PaymentGateway mockGateway = mock(PaymentGateway.class);
    when(mockGateway.charge(any(), eq(100.0))).thenReturn(true);

    OrderService service = new OrderService(mockGateway);
    boolean result = service.placeOrder(new Order(100.0));

    assertTrue(result);
    verify(mockGateway).charge(any(), eq(100.0));
}
  • @Test/@BeforeEach/@AfterEach for structuring setup/teardown; @ParameterizedTest for running the same test logic across multiple input/expected-output pairs without duplicating test methods.
  • Cover the happy path plus meaningful edge cases (empty input, boundary values, error conditions) — not just the one obvious scenario.

Mockito lets you replace real collaborators (databases, HTTP clients, other services) with controllable mocks in unit tests, so tests stay fast, deterministic, and isolated from external systems: mock(Type.class) creates the mock, when(...).thenReturn(...) stubs behavior, and verify(...) asserts a specific interaction happened.

Where to draw the line: unit tests (with mocks) for fast, isolated verification of a class's logic; a smaller number of integration tests for verifying that real components are actually wired together correctly — over-mocking everything, including simple value objects, is itself a common testability anti-pattern.