Introduction
Recordo is a JUnit 5 extension that takes the pain out of dealing with test data and mock interactions. Instead of hard-coding DTOs, JSON strings or HTTP stubs in your tests, Recordo moves these resources into files and generates or records them when they don’t yet exist. On the first run, if the required files don’t exist, Recordo generates them (for objects or captured HTTP interactions). You review or adjust these files, and on subsequent runs Recordo loads or replays them as fixtures, making tests deterministic.
Key benefits at a glance:
- Read Module – annotate test parameters or fields with
@Read
to automatically load JSON, CSV, or ZIP resources into objects. Missing files are generated automatically, ensuring fast deterministic tests. - Assertions Module – assert JSON or CSV responses with expressive fluent APIs. Compare against expected files, filter fields, or ignore order with clear options.
- MockMvc Module – build declarative HTTP clients for your controllers in tests. Use familiar Spring MVC annotations (
@GetMapping
,@PostMapping
, etc.) and Recordo will generate typed clients, handle requests, and record responses. - MockServer Module – capture and replay HTTP traffic from real clients like
OkHttp
orRestTemplate
. Save interactions to JSON files and replay them in tests for stable, reproducible scenarios.
Recordo combines these modules into a single coherent testing toolkit. It is especially useful when working with complex DTOs, deeply nested JSON structures, or external HTTP APIs, reducing boilerplate and making tests both readable and maintainable.
👉 Source code is available on GitHub.
Getting Started
Maven Dependencies
Recordo is modular. You can either pull in all modules for convenience, or declare only the ones you need. This gives you flexibility to keep your build lean.
All Modules
Use the recordo-all
artifact to include everything:
<dependency>
<groupId>com.cariochi.recordo</groupId>
<artifactId>recordo-all</artifactId>
<version>2.0.9</version>
<type>pom</type>
<scope>test</scope>
</dependency>
Individual Modules
If you want to be explicit and import only what you actually use:
<!-- Read Module -->
<dependency>
<groupId>com.cariochi.recordo</groupId>
<artifactId>recordo-read</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<!-- Assertions Module -->
<dependency>
<groupId>com.cariochi.recordo</groupId>
<artifactId>recordo-assertions</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<!-- MockMvc Module -->
<dependency>
<groupId>com.cariochi.recordo</groupId>
<artifactId>recordo-spring-mockmvc</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<!-- MockServer Module -->
<dependency>
<groupId>com.cariochi.recordo</groupId>
<artifactId>recordo-mockserver</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
Compatibility
- 1.x.x — Java 11+ and Spring 5.x (Spring Boot 2.x)
- 2.x.x — Java 17+ and Spring 6.x (Spring Boot 3.x)
Note: the modules recordo-read
, recordo-assertions
, and recordo-mockserver
work perfectly in projects without Spring. Only recordo-spring-mockmvc
requires a Spring context.
Initialize Extension
Register the JUnit 5 extension in tests:
@ExtendWith(RecordoExtension.class)
class MyTest {
// ...
}
ObjectMapper
Recordo uses Jackson ObjectMapper
for JSON serialization and deserialization. The resolution order is:
- If a test class has a field annotated with
@EnableRecordo
, thatObjectMapper
instance will be used. - If no such field is present and you run under Spring, Recordo will look for a single
ObjectMapper
bean in the application context. - If neither is found, a built‑in default mapper is used.
You can register multiple mappers and reference them by name in annotations (e.g., @Read(objectMapper = "customMapper")
).
Additional Properties
You can fine-tune behavior in recordo.properties
on the test classpath:
recordo.resources.root
Overrides the base directory where Recordo reads expected files and writes generated/recorded ones.
Default: src/test/resources/
Examples:
recordo.resources.root=src/it/resources
recordo.http.headers.included
Comma‑separated list of header names that must be included into recordings and used for matching during replay.
recordo.http.headers.sensitive
Comma‑separated list of sensitive headers that should be masked in stored recordings (e.g., Authorization
).
Examples:
recordo.http.headers.included=Accept,Content-Type,Authorization
recordo.http.headers.sensitive=Authorization,Set-Cookie
Read Module
The Read module lets you source test objects from external files and keep them under version control. If a referenced file is missing on the first run, Recordo generates it with a sensible structure and randomized values; on subsequent runs the same file is read back, keeping your tests deterministic.
Maven Dependency
<dependency>
<groupId>com.cariochi.recordo</groupId>
<artifactId>recordo-read</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
Annotation Parameters
@Read
supports:
value
– resource path (e.g./books/book.json
).objectMapper
– optional name of anObjectMapper
bean or test‑class field to use for (de)serialization.
Approach 1 — Interface‑based Object Factory
Declare a factory interface with @RecordoObjectFactory
. Mark factory methods with @Read
to bind them to files. Use @Modifier
methods for fluent tweaks.
@RecordoObjectFactory
public interface LogRecordFactory {
// one object
@Read("/messages/log.json")
LogRecord logRecord();
// list of objects
@Read("/messages/logs.json")
List<LogRecord> list();
// raw bytes (e.g., zip archive)
@Read("/files/out.zip")
byte[] archive();
// fluent modifications
@Modifier("id")
LogRecordFactory withId(long id);
@Modifier("responses[0].status")
LogRecordFactory withFirstStatus(Status status);
}
Usage
@ExtendWith(RecordoExtension.class)
class LogTests {
LogRecordFactory factory = Recordo.create(LogRecordFactory.class);
@Test
void creates_and_modifies() {
LogRecord base = factory.logRecord();
LogRecord modified = factory.withId(123).withFirstStatus(Status.SUCCESS).logRecord();
byte[] zip = factory.archive();
}
}
Approach 2 — Direct Object Creation
Use @Read
directly on test parameters for quick loading without factories.
@Test
void single_object(@Read("/books/book.json") Book book) { /* ... */ }
@Test
void list_of_objects(@Read("/books/books.json") List<Book> books) { /* ... */ }
@Test
void as_string(@Read("/texts/info.txt") String text) { /* ... */ }
@Test
void as_bytes(@Read("/files/archive.zip") byte[] bytes) { /* ... */ }
Supported Types
- POJOs/DTOs via Jackson
List<T>
/ arraysString
byte[]
First‑Run Behavior
- If the target file does not exist, Recordo creates it with a generated example that matches the return type.
- You may edit the file to your needs; subsequent runs read it as a fixture.
Tip: prefer small, focused files per test scenario; this keeps diffs readable and reviewable.
Assertions Module
The Assertions module compares objects and strings against JSON/CSV files using three classes:
JsonAssertion
— object ⇄ JSON file comparison.JsonCondition
— AssertJCondition
for object ⇄ JSON file comparison.CsvAssertion
— CSV string ⇄ CSV file comparison.
Maven Dependency
<dependency>
<groupId>com.cariochi.recordo</groupId>
<artifactId>recordo-assertions</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
JSON Assertions (JsonAssertion
)
Pass any object (e.g., DTOs, collections, pages). Recordo will serialize it via Jackson and compare with the expected JSON file.
@Test
void shouldMatchBooksShortView() {
Page<Book> books = bookService.findAllByAuthor(author);
assertAsJson(books)
.including("content[*].id", "content[*].title", "content[*].author.id")
.isEqualTo("/books/short_books.json");
}
Options
.including(paths...)
— compare only the listed JSON paths..excluding(paths...)
— ignore listed paths..extensible(true)
— allow extra fields..withStrictOrder(true)
— require array order to match..using(ObjectMapper)
— custom mapper.
JSON Assertions as AssertJ Condition (JsonCondition
)
JsonCondition
provides an alternative syntax for the same functionality. It supports the same options as JsonAssertion
.
@Test
void shouldMatchBooksShortView_withCondition() {
Page<Book> books = bookService.findAllByAuthor(author);
assertThat(books)
.is(equalAsJsonTo("/books/short_books.json")
.including("content[*].id", "content[*].title", "content[*].author.id")
);
}
CSV Assertions (CsvAssertion
)
Compare an actual CSV string with an expected CSV file.
@Test
void shouldMatchCsv() {
String actualCsv = "id,name\n1,John\n";
assertCsv(actualCsv)
.withHeaders(true)
.withStrictOrder(true)
.withColumnSeparator(';')
.withLineSeparator("\r\n")
.isEqualsTo("/expected/users.csv");
}
Options
.withHeaders(boolean)
— treat the first row as a header..withStrictOrder(boolean)
— require rows to appear in the same order..withColumnSeparator(char)
— set a custom column separator (default is comma)..withLineSeparator(String)
— set a custom line separator.
First Run & Debugging Failures
- First run (or if the expected file was deleted): Recordo serializes the actual data (object or CSV string) and creates the corresponding JSON/CSV file automatically.
- When a comparison fails: Recordo writes the actual data into an
ACTUAL/
subfolder next to the expected file, using the same file name as inisEqualTo
. For example,/books/short_books.json
will produce/books/ACTUAL/short_books.json
. This allows you to diff expected vs actual in your IDE. TheACTUAL/
folder is only for debugging — delete it after review and never commit it to version control.
Recommended .gitignore additions
# Recordo temporary outputs
**/ACTUAL/
MockMvc Module
This module lets you call Spring MVC controllers via type‑safe clients defined as annotated interfaces and executed with Spring MockMvc
.
⚠️ Recordo will work only if there is a single MockMvc
instance available in the Spring context.
Maven Dependency
<dependency>
<groupId>com.cariochi.recordo</groupId>
<artifactId>recordo-spring-mockmvc</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
Define the client interface
@RecordoApiClient(
objectMapper = "customMapper", // bean name (optional)
interceptors = {LocaleInterceptor.class, AuthInterceptor.class} // optional
)
@RequestMapping("/users")
interface UserApiClient {
@GetMapping("/{id}")
UserDto findById(@PathVariable("id") Long id);
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
UserDto create(@RequestBody UserDto user);
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void delete(@PathVariable("id") Long id);
@GetMapping("/{id}")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
ErrorDto getById_withErrors(@PathVariable int id,
@RequestParam("name") String name,
@RequestHeader("Authorization") String auth);
}
Notes
- In
@RecordoApiClient
interfaces you use standard Spring annotations:@RequestMapping
,@GetMapping
,@PostMapping
,@DeleteMapping
,@PathVariable
,@RequestParam
,@RequestBody
,@RequestHeader
,@ResponseStatus
. - Recordo executes requests via
MockMvc
, automatically verifies the HTTP status, and maps the response body into the declared return type. - The expected status is declared with
@ResponseStatus
. This allows you to write not only positive tests but also error handling or security tests.
Create and use the client
@WebMvcTest(UserController.class)
@ExtendWith(RecordoExtension.class)
class UserControllerTest {
private final UserApiClient api = Recordo.create(UserApiClient.class);
@Test
void shouldCreateAndFetch() {
UserDto created = api.create(new UserDto("John")); // 201 CREATED (from @ResponseStatus)
UserDto loaded = api.findById(created.getId()); // 200 OK (default)
}
}
Return types
Recordo MockMvc clients support three styles of return values:
Direct result
// Declaration
@GetMapping("/{id}")
UserDto getById(@PathVariable int id, @RequestParam("name") String name);
// Usage
UserDto user = apiClient.getById(1, "Test User");
Response wrapper
// Declaration
@GetMapping("/{id}")
Response<UserDto> getById(@PathVariable int id, @RequestParam("name") String name);
// Usage
Response<UserDto> response = apiClient.getById(1, "Test User");
UserDto userDto = response.getBody();
Map<String, String> headers = response.getHeaders();
HttpStatus status = response.getStatus();
Request object
// Declaration
@GetMapping("/{id}")
Request<UserDto> getById(@PathVariable int id, @RequestParam("name") String name);
// Usage
Request<UserDto> request = apiClient.getById(1, "Test User");
Response<UserDto> response = request.header("Authorization", "Bearer ...").perform();
Request interceptors
Declare interceptors in @RecordoApiClient(interceptors = {...})
. Resolution order:
- if a Spring bean of that type exists in the context — it is used;
- otherwise, the interceptor is instantiated via default constructor.
Interceptors can mutate outgoing requests (e.g., add headers) before they are executed by MockMvc
.
Example:
@Component
@RequiredArgsConstructor
public class AuthInterceptor implements RequestInterceptor {
private static SecurityService securityService;
@Override
public Request<?> apply(Request<?> request) {
if (request.headers().get("Authorization") == null) {
request = request.header("Authorization", "Bearer " + securityService.currentToken());
}
return request;
}
}
MockServer Module
Recordo can record real HTTP interactions on the first run and replay them on subsequent runs. Interactions are persisted as JSON and kept under version control, so your tests remain deterministic.
Maven Dependency
<dependency>
<groupId>com.cariochi.recordo</groupId>
<artifactId>recordo-mockserver</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
How it works on test runs
- First run (or if the file is missing) — real HTTP calls are executed; Recordo captures request/response pairs and writes them to the configured file/folder.
- Subsequent runs — HTTP calls are not sent to the network; responses are taken from the saved JSON.
This module keeps your integration tests fast, repeatable, and reviewable (recordings are plain JSON files).
Setup with Spring
Interceptor-first integration (primary)
Recordo integrates via a dedicated HTTP interceptor bean that you attach to your HTTP client. The main idea: create an interceptor bean and add it to the client, so that Recordo can intercept requests/responses.
Available interceptors:
ApacheRecordoInterceptor
RestClientRecordoInterceptor
RestTemplateRecordoInterceptor
OkhttpRecordoInterceptor
Make the interceptor a Spring bean and wire it into the client you use.
OkHttp (OkHttpClient)
@Bean
OkhttpRecordoInterceptor recordoOkhttpInterceptor() {
return new OkhttpRecordoInterceptor();
}
@Bean
OkHttpClient okHttpClient(OkhttpRecordoInterceptor interceptor) {
return new OkHttpClient.Builder()
.addInterceptor(interceptor)
.build();
}
Apache HttpClient (client5)
@Bean
ApacheRecordoInterceptor recordoApacheInterceptor() {
return new ApacheRecordoInterceptor();
}
@Bean
org.apache.hc.client5.http.classic.HttpClient apacheHttpClient(ApacheRecordoInterceptor interceptor) {
return HttpClients.custom()
.addRequestInterceptorFirst(interceptor)
.build();
}
Spring RestTemplate
@Bean
RestTemplateRecordoInterceptor recordoRestTemplateInterceptor() {
return new RestTemplateRecordoInterceptor();
}
@Bean
RestTemplate restTemplate(RestTemplateRecordoInterceptor interceptor) {
var restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(interceptor);
return restTemplate;
}
Spring RestClient
@Bean
RestClientRecordoInterceptor recordoRestClientInterceptor() {return new RestClientRecordoInterceptor();}
@Bean
RestClient restClient(RestClientRecordoInterceptor interceptor) {
return RestClient.builder()
.requestInterceptor(interceptor)
.build();
}
Auto-wiring client bean (alternative)
As an alternative, Recordo can search for an HTTP client bean in the Spring context and automatically attach its interceptor. This requires no explicit interceptor wiring, but works only when there is a single supported client bean of that type in the context.
Example with RestTemplate
:
@SpringBootTest
class BookServiceTest {
@Autowired
private RestTemplate restTemplate; // Recordo will detect and wrap it automatically
@Test
@MockServer("/mock_servers/books.json")
void should_retrieve_books() {
List<Book> books = bookClient.getBooks();
// interactions recorded or replayed automatically
}
}
Using Recordo without Spring
Recordo can also be used in projects without Spring. In this case you annotate the HTTP client or interceptor field with @EnableRecordo
, and Recordo will instrument it directly.
With explicit interceptor
@ExtendWith(RecordoExtension.class)
class GitHubServiceTest {
@EnableRecordo
private final OkhttpRecordoInterceptor recordoInterceptor = new OkhttpRecordoInterceptor();
private final OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(recordoInterceptor)
.build();
private final GitHubService service = new GitHubService(client);
@Test
@MockServer("/mockserver/gists.mock.json")
void test_mock_server() {
List<GistDto> gists = service.getGists();
// first run records, next runs replay
}
}
Without explicit interceptor
@ExtendWith(RecordoExtension.class)
class GitHubServiceTest {
@EnableRecordo
private final OkHttpClient client = new OkHttpClient();
private final GitHubService service = new GitHubService(client);
@Test
@MockServer("/mockserver/gists.mock.json")
void test_mock_server() {
List<GistDto> gists = service.getGists();
// first run records, next runs replay
}
}
In both cases, @EnableRecordo
tells Recordo to wrap the client or interceptor so that interactions can be recorded and replayed during tests.
OpenFeign (via supported HTTP clients)
OpenFeign can be used together with Recordo by wiring it to one of the supported HTTP clients (OkHttp or Apache HttpClient). Recordo will attach its interceptors to the underlying client and capture/replay calls.
OkHttp setup
@Bean
public okhttp3.OkHttpClient okHttpClient() {
return new okhttp3.OkHttpClient();
}
@Bean
public feign.Client feignClient(okhttp3.OkHttpClient okHttpClient) {
return new feign.okhttp.OkHttpClient(okHttpClient);
}
Apache HttpClient setup
@Bean
public org.apache.hc.client5.http.classic.HttpClient apacheHttpClient() {
return HttpClients.createDefault();
}
@Bean
public feign.Client feignClient(org.apache.hc.client5.http.classic.HttpClient httpClient) {
return new feign.httpclient.ApacheHttpClient(httpClient);
}
Note: Recordo interacts with Feign through the configured OkHttp/Apache client by inserting its interceptors. Make sure these clients are the ones used by Feign in the Spring context.
Annotation Parameters
Use @MockServer
on a test method (or class). Parameters:
value
(String) — path to the storage location:- File path ending with
.json
→ all interactions are recorded into a single file. - Folder path (no
.json
) → each interaction is recorded as a separate JSON file inside the folder.
- File path ending with
-
urlPattern
(String) — optional URL matcher; supports?
(one char),*
(zero or more chars),**
(zero or more path segments). -
beanName
(String) — bean name, which can be either the name of the interceptor bean (in interceptor-first mode) or the client bean (in auto-wiring mode). objectMapper
(String) — name ofObjectMapper
bean or test field.
Examples
Single server (file storage)
@Test
@MockServer("/mock_servers/get_gists.json")
void should_retrieve_gists() {
List<GistResponse> gists = gitHubClient.getGists();
// first run records, next runs replay
}
Single server (folder storage)
@Test
@MockServer("/mock_servers/gists/")
// folder
void should_retrieve_gists_in_folder_mode() {
List<GistResponse> gists = gitHubClient.getGists();
}
Multiple HTTP clients
@Autowired
private RestTemplate bookServerRestTemplate;
@Autowired
private RestTemplate authorServerRestTemplate;
@Test
@MockServer(httpClient = "bookServerRestTemplate", value = "/mockserver/multiservers/books-server.rest.json")
@MockServer(httpClient = "authorServerRestTemplate", value = "/mockserver/multiservers/authors-server.rest.json")
void should_retrieve_books_and_authors() {
// calls using two different clients are recorded into two files
}
Multiple servers (URL pattern)
@Test
@MockServer(beanName = "https://books.server/**", value = "/mockserver/multiservers/books-server.rest.json")
@MockServer(beanName = "https://authors.server/**", value = "/mockserver/multiservers/authors-server.rest.json")
void should_retrieve_from_multiple_servers() {
// calls to matching hosts go into the respective files
}
License
Recordo is distributed under the Apache License 2.0.