Skip to content

Integration testing

If you write the service as well as the client, test the two together. This catches the mistakes neither endpoint tests nor mocks can: a route that differs between client and service, a status code the client does not expect, or a property the two serialize differently.

Run the service inside the test process and point the client at it. Replace the service's own dependencies, such as its database, with mocks, so the test only covers the HTTP layer.

Use WebApplicationFactory from Microsoft.AspNetCore.Mvc.Testing, which hosts the service in memory and provides an HttpClient connected to it. Give your client a constructor that passes an HttpClient on to the entry endpoint:

public class MyClient : EntryEndpoint
{
    public MyClient(Uri uri) : base(uri) {}

    public MyClient(HttpClient httpClient) : base(httpClient) {}

    // Child endpoints
}

The entry endpoint then uses the HttpClient's BaseAddress as its URI:

public class ContactsApiFacts(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly Mock<IContactsService> _service = new();

    private MyClient CreateClient()
        => new(factory
            .WithWebHostBuilder(x => x.ConfigureTestServices(services => services.AddSingleton(_service.Object)))
            .CreateClient());

    [Fact]
    public async Task ReadsContact()
    {
        _service.Setup(x => x.ReadAsync("1337")).ReturnsAsync(new Contact { Id = "1337", Name = "Smith" });

        var contact = await CreateClient().Contacts["1337"].ReadAsync();

        Assert.Equal("Smith", contact.Name);
    }
}

IContactsService stands for whatever the service's controllers depend on.

Sample-DotNet has a complete set of tests like this, using TestServer directly.

Use @SpringBootTest to start the service on a random port, and @MockitoBean to replace its dependencies:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ContactsApiTest {
    @LocalServerPort
    private int port;

    @MockitoBean
    private ContactsService service;

    @Test
    void readsContact() {
        Contact smith = new Contact("Smith");
        smith.setId("1337");
        when(service.read("1337")).thenReturn(smith);
        MyClient client = new MyClient(URI.create("http://localhost:" + port + "/"));

        Contact contact = client.getContacts().get("1337").read();

        assertEquals("Smith", contact.getName());
    }
}

ContactsService stands for whatever the service's controllers depend on.

Use @SpringBootTest to start the service on a random port, and @MockitoBean to replace its dependencies:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ContactsApiTest {
    @LocalServerPort
    private var port: Int = 0

    @MockitoBean
    private lateinit var service: ContactsService

    @Test
    fun readsContact() {
        whenever(service.read("1337")).thenReturn(Contact(id = "1337", name = "Smith"))
        val client = MyClient(URI("http://localhost:$port/"))

        val contact = client.contacts["1337"].read()

        assertEquals("Smith", contact.name)
    }
}

ContactsService stands for whatever the service's controllers depend on. Sample-Kotlin has a complete set of tests like this.

If the service runs on Node.js, start it on port 0 (a random free port) with mocked dependencies. A Vitest fixture does this for every test:

import { once } from "node:events";
import type { AddressInfo } from "node:net";
import { test as base, expect, vi, type Mocked } from "vitest";

const test = base.extend<{ service: Mocked<ContactsService>; client: MyClient }>({
    service: async ({}, use) => {
        await use({ read: vi.fn() /* ... */ });
    },
    client: async ({ service }, use) => {
        const server = createApp(service).listen(0, "localhost");
        await once(server, "listening");
        const { port } = server.address() as AddressInfo;
        await use(new MyClient(new URL(`http://localhost:${port}/`)));
        server.closeAllConnections();
        server.close();
    }
});

test("reads contact", async ({ service, client }) => {
    service.read.mockReturnValue({ id: "1337", name: "Smith" });

    const contact = await client.contacts.get("1337").read();

    expect(contact.name).toBe("Smith");
});

createApp() stands for a function that builds your Express app from whatever its routes depend on. Sample-TypeScript has a complete set of tests like this.

Sharing the entity classes between client and service, as the sample projects do with a separate DTO project, rules out a whole class of mismatches before any test runs.