Skip to content
This repository was archived by the owner on Mar 22, 2026. It is now read-only.

Latest commit

 

History

History
118 lines (87 loc) · 8.26 KB

File metadata and controls

118 lines (87 loc) · 8.26 KB

Spring: Testing Conventions

This document defines technological conventions for Spring/JUnit projects that align well with the Ergonomic Approach (EA).

HTTP in Tests

In HTTP test clients (*HttpApi), use RestTestClient for Spring Boot 4 or WebTestClient for Spring Boot 3. Test cases must not call WebTestClient or RestTestClient directly. Test cases call HTTP entry points through *HttpApi fixtures APIs. For a step-by-step procedure, see ../../../skills/refactoring-http-tests-to-httpapi/SKILL.md.

When the project uses MockMvcWebTestClient (MockMvc-backed WebTestClient) to speed up tests, treat it as a WebTestClient implementation detail inside shared test infrastructure. Do not leak MockMvc or MockMvcWebTestClient knowledge into test cases. For a step-by-step procedure, see ../../../skills/migrating-spring-http-tests-to-mockmvc/SKILL.md.

Test Fixture Wiring

Wire fixture components (*TestApi, *FixturePresets) into the Spring test context via a dedicated *Conf class in test sources. Prefer @ComponentScan (or @Import) on the config class, and include it in @SpringBootTest(classes = [...]) (or import it from a base test). In Spring Boot tests, prefer @TestConfiguration for such configs. With @ComponentScan without explicit packages, scanning defaults to the package of the config class.

Avoid bean name collisions between scanned @Component fixtures and explicit @Bean factory methods.

  • By default, FooBar becomes bean name fooBar.
  • For @Bean methods, the default bean name is the factory method name.
  • A scanned @Component class UsersTestApi and a @Bean fun usersTestApi(): UsersTestApi share the same bean name and may override or conflict.
  • Prefer unique names for factory beans (for example @Bean(name = ["integrationDbTestApi"])) or rename the method to avoid colliding with fixture component names.
  • When a fixture bean is unexpectedly missing at runtime (NoSuchBeanDefinitionException), first suspect a naming collision before rewriting the wiring.

Avoid introducing an @Component that aggregates multiple fixture beans only to make injection “convenient”. Such an aggregator defeats spring.main.lazy-initialization and pulls unrelated fixture code into tests that do not need it.

Example.

@TestConfiguration
@ComponentScan
class TestFixturesConf

@SpringBootTest(classes = [App::class, TestFixturesConf::class])
abstract class BaseIntegrationTest

Inject the specific fixture beans you need (for example OrdersFixturePresets and UsersTestApi) directly into each test class.

Database in Tests

When integration tests use a real DB via Testcontainers, prefer reusing a single DB container and a single pooled DataSource across test application contexts. This reduces suite time and avoids repeated DB wiring work when different @SpringBootTest(classes = ...) combinations exist. Use a dedicated @TestConfiguration with a DataSource @Bean, and keep the actual HikariDataSource in a JVM-singleton lazy holder.

Enable bean overriding for the test profile when a production DataSource exists. See reusable-test-datasource.md for a reference implementation and failure modes.

*HttpApi Design

Public *HttpApi methods accept and return the same Kotlin types as the corresponding controller method parameters and return type. Do not introduce intermediate *Request or *Response DTOs in tests or fixtures when the controller already defines the transport contract. Use the *ForResponse pattern to expose a response spec for HTTP-level assertions in tests. Keep one canonical request-building implementation for each operation. When negative and edge cases need raw or out-of-contract transport values, add an explicit escape hatch to *ForResponse (for example raw query parameters as Map<String, String?>). If a scenario depends on a parameter being omitted entirely, add a raw or relaxed overload that can omit keys instead of serializing a typed request object with defaults. Do not silently turn “parameter omitted” into “parameter sent with a default value” inside typed helpers. Prefer typed overloads that delegate to the canonical method. Do not introduce a new *HttpApi method for each invalid parameter case. Use the *ForError pattern for negative cases to validate the error contract and return a typed error representation or a response spec. For non-deterministic test cases (usually due to concurrency) where a given HTTP call may legitimately succeed or return an expected error, prefer the *ForOutcome pattern. *ForOutcome returns a typed value outcome (for example HttpOutcome<SuccessBody, ErrorBody>). Expected errors are returned as values, while unexpected responses throw exceptions (fail-fast). Keep the set of expected errors centralized and reusable (for example only(HttpStatus.CONFLICT with <errorCode>)). For generic Kotlin types, decode using ParameterizedTypeReference<T> (or a project helper built on it).

WebTestClient request conventions (inside *HttpApi)

Prefer a dedicated request builder DSL for cross-cutting concerns such as authentication headers. If many call sites build Authorization: Bearer ..., extract a helper like authorized(token) in shared test code and use it consistently.

Prefer simple URI templates over uri { uriBuilder -> ... } when the URI is a static path with a small number of query parameters. This keeps diffs small. This reduces incidental complexity in migrations.

Do not use ThreadLocal to pick routing, base URLs, or per-test client configuration. Make the client and its base configuration explicit (constructor parameters or per-test wiring in shared infra).

If the project uses a custom Jackson ObjectMapper, ensure the WebTestClient codecs use it. If the codecs are not aligned, migrations often fail with CodecException or InvalidDefinitionException and hide real behavior changes.

Spring MVC binding pitfall: @ModelAttribute and Kotlin default arguments

In Spring MVC controllers, do not rely on a Kotlin default argument on an @ModelAttribute parameter to represent omitted request input. Spring request binding and Kotlin call-site defaults are separate mechanisms. Model omission and default semantics via the bound type, explicit request parameters, or another framework-supported mechanism instead of a controller-method default argument. Verify the observable behavior through an MVC-level request test, not through a direct controller call.

Controller-boundary coverage

For new tests, and for changes that explicitly migrate test boundaries, execute controller behavior, routing, request binding, validation, security, and HTTP default-semantics checks through Spring MVC. Use *HttpApi or an MVC slice test for those scenarios. Do not call controller methods directly for MVC-boundary behavior, because direct calls bypass routing, argument binding, validation, and security. When editing an existing test without explicit migration scope, preserve its current boundary by default and treat boundary migration as a separate change. If you intentionally write a pure unit test for controller-local branching, say that explicitly in the test and keep it separate from API or endpoint coverage.

Low-level technical details in test cases

Do not inline low-level technical boilerplate in test cases. This includes concurrency primitives (latches, futures, executors) and ad-hoc request/response plumbing. Extract such details into test platform code and fixture APIs (*HttpApi, *TestApi, *FixturePresets). For concurrent execution, use a shared helper (for example executeSimultaneously(requestsCount) { ... }). Keep the test case as a thin script that contains only scenario logic and assertions over typed outcomes.

JSON Schema Verification

*HttpApi methods must validate JSON request and response bodies against JSON schemas when the project provides schemas for the endpoint. Schema verification happens inside *HttpApi before decoding the response body into Kotlin objects. Tests focus on business rules and observable behavior beyond the transport contract. If schema verification fails, preserve the original validation exception in the thrown failure (as cause or suppressed). If a client validates the same body against multiple schemas (for example, “success” vs “error”), keep diagnostics for all failed validations.