Spring Framework
Integration Testing
Bean Overriding
Java Testing
Test Configuration

Overriding beans in Integration tests

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Integration tests often need the real Spring container but not every real dependency inside it. A payment client may need to become a stub, a clock may need to become deterministic, or an email sender may need to become a no-op bean.

Spring gives you several ways to replace beans in tests, but they are not interchangeable. The best choice depends on whether you want a mock, a test-only implementation, or a profile-specific graph.

The Simplest Option: @MockBean

If the goal is to replace one dependency with a Mockito mock inside a Spring Boot test, @MockBean is usually the most direct tool:

java
1import static org.mockito.BDDMockito.given;
2
3import org.junit.jupiter.api.Test;
4import org.springframework.beans.factory.annotation.Autowired;
5import org.springframework.boot.test.context.SpringBootTest;
6import org.springframework.boot.test.mock.mockito.MockBean;
7
8@SpringBootTest
9class BillingServiceTest {
10
11    @MockBean
12    PaymentGateway paymentGateway;
13
14    @Autowired
15    BillingService billingService;
16
17    @Test
18    void chargesUsingMockedGateway() {
19        given(paymentGateway.charge("order-1")).willReturn("ok");
20
21        String result = billingService.charge("order-1");
22
23        org.junit.jupiter.api.Assertions.assertEquals("ok", result);
24    }
25}

This replaces the existing bean in the application context and injects the mock wherever that type is used.

Use @TestConfiguration for Real Test Implementations

If a mock is too thin and you want a small in-memory or deterministic implementation, use @TestConfiguration:

java
1import java.time.Clock;
2import java.time.Instant;
3import java.time.ZoneOffset;
4
5import org.springframework.boot.test.context.TestConfiguration;
6import org.springframework.context.annotation.Bean;
7import org.springframework.context.annotation.Primary;
8
9@TestConfiguration
10class FixedClockConfig {
11
12    @Bean
13    @Primary
14    Clock testClock() {
15        return Clock.fixed(
16            Instant.parse("2024-01-01T00:00:00Z"),
17            ZoneOffset.UTC
18        );
19    }
20}

Then import it into the test:

java
1import org.springframework.boot.test.context.SpringBootTest;
2import org.springframework.context.annotation.Import;
3
4@SpringBootTest
5@Import(FixedClockConfig.class)
6class ReportServiceTest {
7}

This approach is especially good when you want realistic behavior without hitting external infrastructure.

Why @Primary Often Matters

If the application already defines a bean of the same type, your test bean may need @Primary so Spring knows which one to inject by default. Without it, you may get a "multiple beans of type" error unless you also use qualifiers.

@Primary is a tie-breaker, not a complete override mechanism by itself. It is most useful when paired with @TestConfiguration.

Profiles Work for Larger Test Graphs

If your test environment differs in several coordinated ways, a Spring profile can be cleaner than many one-off overrides.

java
1import org.springframework.context.annotation.Bean;
2import org.springframework.context.annotation.Configuration;
3import org.springframework.context.annotation.Profile;
4
5@Configuration
6@Profile("integration-test")
7class IntegrationTestConfig {
8
9    @Bean
10    EmailSender emailSender() {
11        return message -> System.out.println("captured email: " + message);
12    }
13}

Activate it in the test:

java
1import org.springframework.test.context.ActiveProfiles;
2
3@SpringBootTest
4@ActiveProfiles("integration-test")
5class NotificationFlowTest {
6}

Profiles are useful when many beans need to change together, but they can become harder to reason about if overused.

Choose the Smallest Override That Solves the Problem

A good rule of thumb is:

  • use @MockBean for one dependency you want to stub or verify
  • use @TestConfiguration for small but real test implementations
  • use profiles when the whole test environment changes as a unit

That keeps tests readable and prevents a heavy test-specific container setup from drifting too far from production.

Keep Context Reuse in Mind

Spring test startup can be expensive. If every test class overrides beans differently, context caching becomes less effective and the suite slows down.

Try to group tests that share the same overridden context. This is not only faster; it also makes test wiring more consistent.

Common Pitfalls

The biggest mistake is overriding too much. If the test replaces half the application graph, it stops being a meaningful integration test.

Another common problem is forgetting @Primary or qualifiers when multiple beans of the same type exist. That leads to ambiguous injection errors that look unrelated to the test itself.

Teams also use profiles for tiny one-off overrides that would be clearer with @MockBean or @TestConfiguration. Profiles are powerful, but they add global state to the test context.

Finally, do not mock the class under test. Override its collaborators, then let the real target bean run inside the container.

Summary

  • '@MockBean is the quickest way to replace one bean with a Mockito mock in a Boot integration test.'
  • '@TestConfiguration is better when you want a small real test implementation.'
  • '@Primary helps Spring choose the test bean when multiple candidates exist.'
  • Profiles are useful for larger test-specific wiring changes.
  • Override the smallest possible part of the context to keep integration tests credible and fast.

Course illustration
Course illustration

All Rights Reserved.