How to Write Unit Tests and Integration Tests for CoApi Client Interfaces

Unit tests for CoApi client interfaces verify Spring bean registration using ApplicationContextRunner, while integration tests exercise the generated proxy against real HTTP endpoints using tools like WireMock.

CoApi client interfaces are plain Kotlin interfaces annotated with @CoApi that the Spring Boot starter automatically converts into fully configured HTTP clients. The ahoo-wang/coapi repository provides a clean separation between configuration logic and runtime behavior, allowing you to test your client setup without starting a full application context and validate HTTP mappings against mock servers.

Understanding the CoApi Bean Registration Architecture

When the Spring Boot application starts, CoApiAutoConfiguration scans for interfaces annotated with @CoApi and registers two beans for each client:

  • <clientName>.HttpClient: The underlying HTTP client (WebClient or RestClient) used to invoke the remote service.
  • <clientName>.CoApi: The dynamic proxy implementation of the annotated interface that forwards calls to the HTTP client.

The core parsing logic lives in CoApiDefinition.kt (spring/src/main/kotlin/me/ahoo/coapi/spring/CoApiDefinition.kt), which resolves base URLs (including lb:// load-balanced URLs) from the Spring Environment and determines load-balancing requirements. The auto-configuration trigger resides in CoApiAutoConfiguration.kt (spring-boot-starter/src/main/kotlin/me/ahoo/coapi/spring/boot/starter/CoApiAutoConfiguration.kt).

Because Spring manages these beans entirely through the application context, you can validate your configuration layer independently from HTTP runtime behavior.

Writing Unit Tests for CoApi Auto-Configuration

Unit tests for CoApi client interfaces target the configuration layer. They verify that given specific property values and optional dependency beans, the CoApiAutoConfiguration correctly registers the expected client beans without requiring network I/O.

Use ApplicationContextRunner from spring-boot-test to isolate the configuration classes and inject mock dependencies such as LoadBalancedExchangeFilterFunction.

// src/test/kotlin/me/ahoo/coapi/example/ClientBeanTest.kt
package me.ahoo.coapi.example

import io.mockk.mockk
import me.ahoo.coapi.example.consumer.client.GitHubApiClient
import me.ahoo.coapi.spring.EnableCoApi
import me.ahoo.coapi.spring.boot.starter.CoApiAutoConfiguration
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.runner.ApplicationContextRunner
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancedExchangeFilterFunction

class ClientBeanTest {

    private val contextRunner = ApplicationContextRunner()
        .withUserConfiguration(EnableCoApiConfiguration::class.java)
        .withUserConfiguration(CoApiAutoConfiguration::class.java)
        .withBean(
            "loadBalancerExchangeFilterFunction",
            LoadBalancedExchangeFilterFunction::class.java
        ) { mockk() }

    @Test
    fun `auto-configuration creates reactive CoApi bean`() {
        contextRunner
            .withPropertyValues("github.url=https://api.github.com")
            .run { ctx ->
                // Verify the HTTP exchange adapter factory is registered
                ctx.assertThat { 
                    hasSingleBean(me.ahoo.coapi.spring.client.reactive.ReactiveHttpExchangeAdapterFactory::class.java) 
                }
                // Verify the generated client proxy is registered
                ctx.assertThat { hasSingleBean(GitHubApiClient::class.java) }
            }
    }
}

Key testing strategies:

  • Isolate the context: ApplicationContextRunner loads only the configuration classes under test, avoiding the overhead of @SpringBootTest.
  • Mock external dependencies: Provide mocked LoadBalancedExchangeFilterFunction beans to satisfy load-balancer requirements without starting a service registry.
  • Assert bean presence: Verify that ReactiveHttpExchangeAdapterFactory and the specific client interface (e.g., GitHubApiClient) are registered as singleton beans.

The official repository provides a comprehensive example in CoApiAutoConfigurationTest.kt (spring-boot-starter/src/test/kotlin/me/ahoo/coapi/spring/boot/starter/CoApiAutoConfigurationTest.kt), which demonstrates asserting bean types and configuration properties.

Writing Integration Tests for CoApi Clients

Integration tests validate that the generated CoApi proxy correctly serializes requests and deserializes responses. These tests require a running Spring context and an HTTP server to receive the actual network calls.

Use @SpringBootTest combined with an in-memory mock server like WireMock to stub remote service responses.

// src/test/kotlin/me/ahoo/coapi/example/ClientIntegrationTest.kt
package me.ahoo.coapi.example

import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.client.WireMock.*
import me.ahoo.coapi.example.consumer.client.GitHubApiClient
import me.ahoo.coapi.example.consumer.client.Issue
import me.ahoo.coapi.spring.EnableCoApi
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import reactor.test.StepVerifier

@SpringBootTest(
    classes = [EnableCoApiConfiguration::class],
    properties = [
        // Inject the dynamic WireMock port into the client configuration
        "github.url=http://localhost:\${wiremock.port}"
    ]
)
class ClientIntegrationTest {

    companion object {
        val wireMock = WireMockServer(0)   // 0 selects a random free port

        @BeforeAll @JvmStatic fun startWireMock() = wireMock.start()
        @AfterAll @JvmStatic fun stopWireMock() = wireMock.stop()
    }

    @Autowired lateinit var githubApiClient: GitHubApiClient

    @Test
    fun `GitHubApiClient fetches issues`() {
        // Configure the mock server response
        wireMock.stubFor(
            get(urlPathEqualTo("/repos/owner/repo/issues"))
                .willReturn(
                    aResponse()
                        .withHeader("Content-Type", "application/json")
                        .withBody("[{\"id\":1,\"title\":\"Issue one\"}]")
                )
        )

        // Invoke the generated CoApi client proxy
        val flux = githubApiClient.getIssue("owner", "repo")

        // Verify the reactive stream completes with expected data
        StepVerifier.create(flux)
            .expectNextMatches { it.title == "Issue one" }
            .verifyComplete()
    }
}

Integration testing best practices:

  • Dynamic port allocation: Start WireMock on port 0 to avoid conflicts, then inject the actual port into the baseUrl property (e.g., github.url).
  • Test the proxy layer: Autowire the client interface directly (e.g., GitHubApiClient) to ensure the CoApi-generated proxy correctly maps method arguments to HTTP parameters and response bodies to domain objects.
  • Reactive verification: Use StepVerifier from Project Reactor to assert on Flux or Mono return types without blocking threads.

Summary

  • CoApi client interfaces are Spring-managed beans created by CoApiAutoConfiguration based on @CoApi annotations parsed by CoApiDefinition.
  • Unit tests use ApplicationContextRunner to verify bean registration and configuration parsing without network calls, as demonstrated in CoApiAutoConfigurationTest.kt.
  • Integration tests use @SpringBootTest with WireMock to validate the full request/response cycle through the generated client proxy.
  • Key source files for testing logic include CoApiDefinition.kt for URL resolution logic and CoApiAutoConfiguration.kt for bean registration behavior.

Frequently Asked Questions

How do I test a CoApi client that uses load-balanced URLs?

Provide a mocked LoadBalancedExchangeFilterFunction bean in your ApplicationContextRunner for unit tests, or include a LoadBalancerClient configuration in your integration test context. The CoApiAutoConfiguration checks for the presence of load-balancer filters when resolving lb:// URLs in CoApiDefinition.kt.

Can I write integration tests without starting a full Spring Boot context?

Yes, use @WebFluxTest or @RestClientTest if you only need to test the web layer. However, you must explicitly import CoApiAutoConfiguration and your client interfaces, as these sliced tests do not auto-scan for @CoApi annotated beans.

What is the difference between the HttpClient and CoApi beans?

The HttpClient bean (e.g., github.HttpClient) is the underlying WebClient or RestClient instance configured with base URLs and filters. The CoApi bean (e.g., github.CoApi) is the JDK dynamic proxy implementing your annotated interface, which delegates method calls to the HttpClient. Unit tests typically verify the CoApi proxy bean is registered, while integration tests exercise its method implementations.

Where can I find official examples of CoApi tests?

The ahoo-wang/coapi repository contains CoApiAutoConfigurationTest.kt in the spring-boot-starter module, demonstrating ApplicationContextRunner usage, and CoApiDefinitionTest.kt in the spring module, showing how annotation attributes convert to definition objects. The example client GitHubApiClient.kt in the test sources illustrates a typical interface for integration testing.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →