# Guide: Add a Controller Test ## Purpose This guide provides step-by-step instructions for writing integration tests for an **avaje-nima** controller using `avaje-nima-test` and `@InjectTest`. Tests run against a real embedded Helidon server — no mocking framework needed. When asked to *"add a test"*, *"test this endpoint"*, *"write a test for my controller"*, or *"add a controller test"* to an avaje-nima project, follow these steps exactly. --- ## How it works `avaje-nima-test` (via `@InjectTest`) starts a real Helidon SE server on a random port and wires the full `BeanScope` before any test in the class runs. The server is stopped after the last test. There are two ways to call endpoints from tests: | | Approach 1: raw `HttpClient` | Approach 2: generated typed API | |---|---|---| | Inject | `HttpClient` | `HelloControllerTestAPI` | | Response type | `HttpResponse` | `HttpResponse` (typed) | | Path construction | manual string | generated method per endpoint | | Best for | raw/non-JSON, custom headers | standard CRUD/JSON endpoints | **Prefer Approach 2** (generated typed API) for standard JSON endpoints; use Approach 1 when you need direct control over headers, raw body inspection, or are testing non-JSON endpoints. --- ## Prerequisites The following dependencies must be present in `pom.xml` (included in all archetype-generated projects — verify before proceeding): ```xml io.avaje avaje-nima-test ${avaje.nima.version} test io.avaje junit 1.8 test ``` If either dependency is missing, add it inside ``. --- ## Step 1 — Confirm the test dependencies are present Check `pom.xml` for `avaje-nima-test` (test scope). If missing, add it as shown above. --- ## Step 2 — Create or locate the test class Test classes live in `src/test/java` and mirror the main source package. If a test class already exists for the controller, open it. Otherwise create one at: ``` src/test/java//Test.java ``` The class must carry `@InjectTest`. Declare the injections you need: ```java package ; import static org.assertj.core.api.Assertions.assertThat; import java.net.http.HttpResponse; import org.junit.jupiter.api.Test; import io.avaje.http.client.HttpClient; import io.avaje.inject.test.InjectTest; import jakarta.inject.Inject; import .HelloControllerTestAPI; @InjectTest class HelloControllerTest { @Inject HttpClient client; // Approach 1 — raw HTTP @Inject HelloControllerTestAPI helloApi; // Approach 2 — typed API ``` - **`@InjectTest`** — JUnit 5 extension that starts the embedded server and builds the full `BeanScope` before the first test in the class. - **`HttpClient`** — pre-configured with `http://localhost:` as the base URL. - **`HelloControllerTestAPI`** — auto-generated by `avaje-nima-generator` during test compilation; one interface per `@Controller`, named `TestAPI`, located in the same package as the controller. --- ## Step 3 — Write the test method ### Approach 1 — `HttpClient` (untyped) ```java @Test void data_returnsJson() { HttpResponse res = client.request() .path("hi/data") .GET().asString(); assertThat(res.statusCode()).isEqualTo(200); assertThat(res.body()).contains("message"); assertThat(res.body()).contains("timestamp"); } ``` **Request builder reference:** | Scenario | Snippet | |---|---| | Path (relative to base URL) | `.path("hi/data/Alice")` | | Query parameter | `.queryParam("q", "foo")` | | Request header | `.header("Authorization", "Bearer ...")` | | GET, return as String | `.GET().asString()` | | POST with JSON body | `.body(myDto).POST().asString()` | | Assert status | `assertThat(res.statusCode()).isEqualTo(200)` | | Assert JSON field present | `assertThat(res.body()).contains("fieldName")` | | Assert exact body | `assertThat(res.body()).isEqualTo("exact text")` | ### Approach 2 — Generated typed API (preferred for JSON) `avaje-nima-generator` generates a `HelloControllerTestAPI` interface during `mvn test-compile`. It mirrors the controller method-for-method, with return types resolved to the actual response DTOs: ```java // Generated: .HelloControllerTestAPI @Client("/hi") public interface HelloControllerTestAPI { @Get HttpResponse hi(); @Get("/data") HttpResponse data(); @Get("/data/{name}") HttpResponse dataByName(String name); } ``` Using the API in a test: ```java @Test void dataByName_returnsTypedResponse() { HttpResponse res = helloApi.dataByName("World"); assertThat(res.statusCode()).isEqualTo(200); assertThat(res.body().message()).isEqualTo("World"); assertThat(res.body().timestamp()).isGreaterThan(0); } ``` Key advantages over Approach 1: - **Typed responses** — no JSON string parsing or `.contains()` hacks. - **Compile-time safety** — method signatures are checked by `javac`. - **Path parameters are method parameters** — `dataByName("World")` vs. `.path("hi/data/World")`. --- ## Step 4 — Avoid controller method name collisions `avaje-nima-generator` derives the internal route-handler name from the **Java method name**. Two controller methods in the same class that share a Java name — even with different parameter lists — produce a naming collision in the generated `$Route` class and fail to compile. **What goes wrong:** ```java // Both generate a private method named _data — compile error. @Get("/data") GreetingResponse data() { ... } @Get("/data/{name}") GreetingResponse data(String name) { ... } ``` **The fix:** give each overload a **unique Java method name**. The HTTP path (set by `@Get`) is independent: ```java @Get("/data") GreetingResponse data() { ... } // -> _data @Get("/data/{name}") GreetingResponse dataByName(String name) { ... } // -> _dataByName ``` --- ## Step 5 — Verify ```bash mvn test ``` Expected output: ``` Tests run: N, Failures: 0, Errors: 0, Skipped: 0 ``` --- ## Notes - `@InjectTest` starts an embedded server on a **random port** — never hard-code a port number in tests. - `HttpClient` and `HelloControllerTestAPI` can be injected as `static` if desired for class-level lifecycle (one server instance shared across all `@Test` methods). - The `TestAPI` interface is generated into `target/generated-test-sources` during `mvn test-compile`. It will not exist on a clean checkout until at least one compilation has run. - The `HttpResponse` type used by avaje HttpClient is JDK `java.net.http.HttpResponse`. - `avaje-nima-test` and the `junit` wrapper pull in JUnit 5 and AssertJ transitively — no additional test libraries are required. --- ## Version compatibility | Component | Tested version | |---|---| | `avaje-nima-test` | 1.8 | | `avaje-inject-test` | (transitive via avaje-nima-test) | | JUnit 5 | (transitive via `io.avaje:junit`) | | AssertJ | (transitive via `io.avaje:junit`) | | Java | 25 | | Helidon SE | 4.4.0 | --- ## References - avaje-http client docs: https://avaje.io/http-client/ - avaje-inject test docs: https://avaje.io/inject/#testing