DEV Community

Cover image for I got tired of writing the same API test 200 times, so I built a library
RAHUL GARG
RAHUL GARG

Posted on

I got tired of writing the same API test 200 times, so I built a library

A couple of years ago I was working on a project with a decent-sized REST API. We had integration tests, but maintaining them was painful. Every new endpoint meant a new test class, another fixture, another round of HttpClient setup, response deserialization, custom matchers for dynamic fields like IDs and timestamps. The tests themselves were fine. The ceremony around them was not.

I kept thinking: the actual information in a test is tiny. It's a method, a path, some input, an expected output. Everything else is just the same scaffolding rewritten. What if the test was just... that information, and nothing else?

That's what I built. ConfIT is a .NET library where you write your API tests in YAML (or JSON if you prefer), and the library handles execution, mock setup, response matching, and variable passing between tests.

A test looks like this:

CreateUser:
  tags: [smoke]
  api:
    request:
      method: POST
      path: /api/users
      body:
        name: Alice
        email: alice@example.com
    response:
      statusCode: 201
      matcher:
        semantic:
          id: isUuid
      extract:
        userId: $.body.id

GetUser:
  api:
    request:
      method: GET
      path: /api/users/{{userId}}
    response:
      statusCode: 200
      body:
        name: Alice
        email: alice@example.com
      matcher:
        ignore: [createdAt]
Enter fullscreen mode Exit fullscreen mode

The userId extracted from the first response flows automatically into the second via {{userId}}. No custom processor code for the common case.

The matcher block is what replaced most of the assertion code for me. ignore drops fields you don't care about from the diff. semantic gives you named type assertions: isUuid, isIsoDate, isEmail, greaterThan(n), hasLength(n). Things I was writing by hand in every project.

The thing I use most day-to-day is that the same YAML file works at two levels. In a component test the service boots in-process and dependencies are mocked via WireMock. In an integration test everything is real. I don't maintain two test suites. I maintain one set of test files and switch the fixture config.

Setting up a component suite used to take a fair bit of boilerplate. Now it's:

public TestSuiteFixture() =>
    _suite = SuiteBootstrapper.ForComponent<Startup>("suite.config.yaml",
        onStarted: svc =>
            new DbInitializer(svc.GetRequiredService<AppDbContext>()).Seed());

public TestSuiteContext Context => _suite.Context;
Enter fullscreen mode Exit fullscreen mode

Auth, mock server URL, folder paths, test filters - all in suite.config.yaml. The only thing left in C# is the DB seeding because that genuinely is project-specific.

It recently got picked up on the ThoughtWorks Tech Radar in the Assess ring. The writeup mentioned reducing duplication between component and integration tests, which is exactly what it was built for.

One thing I wasn't expecting: the YAML files became useful beyond just running tests. New engineers on a team can read them to understand what the API actually does. QA can add test cases without touching C#. During code review, you're looking at data, not logic.

There are cases where it doesn't fit. Tests with complex stateful flows or lots of conditional logic are better off as regular code. It's not trying to replace everything. But for the bulk of CRUD-style API tests, I've stopped writing test classes almost entirely.

The library targets .NET 9 and .NET 10. There's an AppLauncher mode if you want to test a non-.NET service (Go, Node, Python). It boots the process via a shell command and speaks HTTP, so the same test files work there too.

Code and docs: github.com/techygarg/ConfIT
NuGet: nuget.org/packages/ConfIT

Happy to answer questions or hear where you've hit similar friction in test suites.

Top comments (0)