Testing

Running Tests

mvn test

Surefire runs all classes matching **/*Test.* and **/*Spec.*.

Test Framework

  • JUnit 5 — Test runner
  • Micronaut Test — Application context injection
  • Micronaut HTTP Client — Controller testing

Test Class Pattern

@MicronautTest(transactional = false)
class MyControllerTest {

    @Property(name = "h2.path", value = "mem:my_test_db")
    @Property(name = "datasources.default.url", value = "jdbc:h2:mem:my_test_db;DB_CLOSE_DELAY=-1")
    static class Config {}

    @Inject
    @Client("/")
    HttpClient httpClient;

    @Inject
    MyService myService;

    @Test
    void testEndpoint() {
        // Use retrieve() for cleaner assertions
        Map response = httpClient.toBlocking().retrieve(
            HttpRequest.GET("/endpoint").bearerAuth(token),
            Map.class
        );
        assertNotNull(response);
    }
}

Test Isolation

Each test class uses an in-memory H2 database to avoid collisions:

@Property(name = "h2.path", value = "mem:unique_test_db")
@Property(name = "datasources.default.url", value = "jdbc:h2:mem:unique_test_db;DB_CLOSE_DELAY=-1")

The DB_CLOSE_DELAY=-1 parameter keeps the database alive for the test duration.

HTTP Client Patterns

Use retrieve() over exchange().getBody()

// Good — throws on error, returns body directly
Map response = httpClient.toBlocking().retrieve(
    HttpRequest.GET("/path").bearerAuth(token),
    Map.class
);

// Avoid — Optional.empty() for some valid responses
Optional<Map> body = httpClient.toBlocking().exchange(
    HttpRequest.GET("/path").bearerAuth(token)
).getBody(Map.class);

Error Response Testing

try {
    httpClient.toBlocking().retrieve(
        HttpRequest.POST("/path", body).bearerAuth(token),
        Map.class
    );
    fail("Expected exception");
} catch (HttpClientResponseException e) {
    assertEquals(HttpStatus.FORBIDDEN, e.getStatus());
    Map error = e.getResponse().getBody(Map.class).orElse(null);
    assertEquals("M_FORBIDDEN", error.get("errcode"));
}

Test Categories

AreaTest ClassCoverage
Auth flowAuthFlowTestRegistration, login, token lifecycle
Room eventsRoomEventTestSend, state, membership
SyncV3SyncControllerTestLong-polling, filters, pagination
E2EEV3KeyControllerTestKey upload/query/claim
MediaMediaControllerTestUpload, download, thumbnails
FederationP5FederationTestFederation endpoints
AdminAdminControllerTestAdmin API
P0 flowP0ClientFlowTestFull end-to-end integration
RoutesRouteAuditTestAll routes respond correctly

Diagnostic Tests

When a test fails with an opaque error, create a minimal diagnostic test:

@Test
void diagnostic() {
    // Minimal reproduction of the failing scenario
    // Capture the full response body
    String body = httpClient.toBlocking().retrieve(
        HttpRequest.GET("/problematic/endpoint").bearerAuth(token),
        String.class
    );
    System.out.println("Response: " + body);
}

Clean up diagnostic test files after use.