Test Patterns: Unit vs Component vs Integration

Overview

Three distinct test patterns are used in the auth-service, each with different levels of Spring context loading, speed, and isolation.


1. Unit Test — Pure JUnit + Mockito

Examples: JwtWebFilterTest, SecurityConfigTest

@ExtendWith(MockitoExtension.class)
class JwtWebFilterTest {
    @Mock
    private ServiceClient serviceClient;
    @InjectMocks
    private CustomUserDetailService customUserDetailService;
}
Aspect Detail
Spring Context None — no Spring loaded at all
Dependencies All mocked with @Mock
Injection @InjectMocks (Mockito creates the object, injects mocks)
Speed Fastest (~milliseconds)
What it tests Single class logic in isolation
Needs env vars No
Needs services running No

2. Component Test — Partial Spring Context

Example: ReactiveAuthenticationManagerTest

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {AuthServiceConfig.class, CustomUserDetailService.class})
class ReactiveAuthenticationManagerTest {
    @MockitoBean
    private ServiceClient serviceClient;
    @Autowired
    private ReactiveAuthenticationManager reactiveAuthenticationManager;
}
Aspect Detail
Spring Context Partial — only listed classes loaded
Dependencies Mix of real beans (@Autowired) and mocks (@MockitoBean)
Injection Spring DI — real wiring between beans
Speed Fast (~2 seconds)
What it tests Bean wiring and interaction between 2-3 real components
Needs env vars No — JwtUtil, AuthController etc. never created
Needs services running No

Key Annotations Explained

  • @ExtendWith(SpringExtension.class) — Tells JUnit 5 to use Spring’s test support. Enables @Autowired and @MockitoBean. Lightweight alternative to @SpringBootTest.
  • @ContextConfiguration(classes = {...}) — Tells Spring exactly which classes to load. Only those beans are created — nothing else.

Why not @SpringBootTest?

@SpringBootTest loads the entire application — all beans, all configs, component scan. This pulls in JwtUtil which needs JWT_SECRET, AuthController which needs AuthService, etc. For a test that only needs ReactiveAuthenticationManager + CustomUserDetailService, loading the full context is wasteful and requires unnecessary env vars.


3. Integration Test — No Spring, Real HTTP

Examples: LoginFlowIT, RegistrationFlowIT, LogoutFlowIT

@Slf4j
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class LoginFlowIT {
    private static final RestTemplate restTemplate = new RestTemplate();
}
Aspect Detail
Spring Context None in the test — services run externally
Dependencies Real running services (auth, member, PostgreSQL)
Injection None — uses RestTemplate for real HTTP calls
Speed Slowest (depends on network + service response)
What it tests Full end-to-end flow across multiple services
Needs env vars Yes — env.mac for Google credentials
Needs services running Yes — auth-service, member-service, Eureka, etc.

Key Annotations Explained

  • @Slf4j — Lombok generates a log field for logging (log.info(...))
  • @TestInstance(Lifecycle.PER_CLASS) — JUnit creates one test instance for all methods (instead of new instance per method). Lets @BeforeAll/@AfterAll be non-static, and lets methods share state (e.g., tokens from login used in refresh)
  • @TestMethodOrder(OrderAnnotation.class) — Tests run in @Order(1), @Order(2), @Order(3) sequence. Critical because login must happen before refresh, refresh before revoke

4. Stress Test — Sequential/Concurrent Load

Example: StressTestIT

@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class StressTestIT {
    private static final RestTemplate restTemplate = new RestTemplate();
    private static final int TOTAL_FLOWS = 5;
}
Aspect Detail
Spring Context None — services run externally
Dependencies Real running services (auth, member, PostgreSQL)
Injection None — uses RestTemplate for real HTTP calls
Speed Slowest (multiple full flows)
What it tests Repeated full flows (Login → Refresh → Revoke) to verify stability under load
Needs env vars Yes — env.mac for Google credentials
Needs services running Yes — auth-service, member-service, Eureka, etc.

How it differs from Integration Test

Integration Test Stress Test
Goal Verify correctness of a single flow Verify stability under repeated/concurrent load
Repetitions 1 flow per test method Multiple flows (5+ sequential, optionally concurrent)
Metrics Pass/fail only Response times (avg, P50, P90, P99), throughput, success rate
Warmup No Yes — primes connection pools before measurement
Report JUnit pass/fail Detailed report with per-step timing saved to file
Setup/Cleanup Per-flow test data Registers user once in @BeforeAll, deletes in @AfterAll

Side-by-Side Comparison

Unit Component Integration Stress
Spring Context None Partial (2-3 beans) None (external) None (external)
Real Beans 0 2-3 All (running externally) All (running externally)
Mocked Everything Service boundaries only Nothing Nothing
HTTP Calls No No Yes Yes (repeated)
Services Running No No Yes Yes
Speed ~ms ~2s ~seconds ~10+ seconds
Goal Logic correctness Bean wiring correctness E2E flow correctness Stability + performance under load
Key Annotation @ExtendWith(MockitoExtension) @ExtendWith(SpringExtension) + @ContextConfiguration @TestInstance(PER_CLASS) + @TestMethodOrder @TestMethodOrder + warmup/report phases

5. Full Spring Context Test (Not Used Yet)

This pattern is not currently used in the project, but is worth documenting for future reference.

What it would look like

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("mock")
class AuthEndpointTest {
    @Autowired
    private WebTestClient webTestClient;

    @MockitoBean
    private ServiceClient serviceClient;

    @Test
    void login_shouldReturnTokens_whenValidGoogleIdToken() {
        // Mock member-service response
        when(serviceClient.postReactive(...)).thenReturn(Mono.just(mockResponse));

        webTestClient.post().uri("/auth/token")
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(loginRequest)
            .exchange()
            .expectStatus().isOk()
            .expectBody()
            .jsonPath("$.accessToken").isNotEmpty()
            .jsonPath("$.refreshToken").isNotEmpty();
    }
}
Aspect Detail
Spring Context Full — all beans loaded (AuthController, AuthService, JwtUtil, SecurityConfig, etc.)
Server Embedded Netty on random port
Dependencies Real beans + mocked external boundaries (@MockitoBean on ServiceClient)
HTTP Client WebTestClient (in-process, no real network)
Needs env vars Yes — JWT_SECRET, GOOGLE_CLIENT_ID, token expirations
Needs services running No — member-service mocked via @MockitoBean
Speed ~5-10 seconds (full context startup)

Where it fits

Unit  →  Component  →  Full Context  →  Integration  →  Stress
(no Spring)  (partial Spring)  (full Spring,       (real services,    (repeated real
                                mocked boundaries)   real HTTP)         flows + metrics)

Potential example use cases

  1. Test the full reactive chain from controller to service: Verify that AuthController.issueToken() correctly calls AuthService, which calls ReactiveAuthenticationManager, which calls CustomUserDetailService — all wired by real Spring DI with only ServiceClient mocked
  2. Test SecurityWebFilterChain with real endpoints: Verify that open endpoints (/auth/token, /auth/users) are accessible without JWT, while protected endpoints require ADMIN role
  3. Test error handling across layers: Verify that a ServiceClientException from member-service correctly propagates through AuthService and returns the right HTTP status and error body from AuthController
  4. Test JWT token generation and validation round-trip: Verify that JwtUtil.generate() creates a valid token that JwtWebFilter can parse and set in ReactiveSecurityContextHolder

Why we don’t use it yet

The current test strategy covers the needed scenarios:

  • Bean wiring correctness → Component tests handle this
  • End-to-end flow → Integration tests handle this with real services

A full context test would be useful when we need to test internal wiring + HTTP layer + error handling together without requiring all services to be running.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top