# Own your boundary: component tests for confident refactoring

Table of Contents

You want to refactor your code, but you’re worried you might break something important. How do you know your changes won’t cause problems?

Your tests are fighting refactoring

Unit tests break when you refactor because they test implementation, not behavior (and they are poorly written). Move a class? Fix 15 tests. Change your domain model? Rewrite all your mocks.

E2E tests are too slow to run during development. You find out they’re broken after you push to CI (yes, I know you never run them locally 😉).

You need tests that prove your business logic still works after refactoring, without breaking every time you move code around.

Component tests: test your boundary, not your internals

Component tests solve this by testing what you own through your API boundary:

// Instead of testing internal classes with mocks...
var service = new EventService(mockRepo, mockValidator);
var result = service.CreateEvent(request);
// Test through your actual API
POST /events { "name": "Tech Conference", "date": "2025-10-15" }

Key principle: Test behavior through the boundary you control. Everything inside your component is real (database, DI, domain logic). Everything outside is stubbed (external APIs, other services).

This means you can refactor your internals without breaking tests, while still proving your business logic works.

How do you make these tests readable and maintainable?

The problem with most integration tests? They’re unreadable:

[Fact]
public async Task CreateEvent_WhenValidRequest_ReturnsEventId()
{
// 20 lines of HTTP setup
// 10 lines of request building
// 15 lines of response parsing
// 10 lines of database verification
}

Step-based tests are readable:

private readonly ScenarioContext _scenarioContext = new ScenarioContext(serviceFixture, testOutputHelper)
.Background(
new SeedVenue().WithName("Conference Room 404").WithAddress("123 Null Pointer Ave"),
new SeedUser().WithName("Alice Debugger").WithEmail("[email protected]")
);
[Fact]
public Task WhenCreateEventValid_Then_EventIsCreated()
{
return new Scenario(_scenarioContext)
.Act(new CreateEventStep())
.Assert(
new RequestSucceeded(),
new EventIsCreated());
}

The background is set up once on the ScenarioContext and applies to all tests using it. Change how you seed venues or users? Update one SeedVenue or SeedUser class, not 50 tests.

We’ve built a small framework around this pattern to make it even easier. It handles the plumbing while you focus on writing readable, composable steps (I prefer not to use the word “framework”, but OK, it’s a small framework 🙃)

Component tests become your main safety net for refactoring. They sit between unit tests (pure domain logic) and E2E tests (critical user journeys), focusing on business behaviors through your API.

Why this works:

Developer-owned - These tests live in your repo and run with your build. When they fail, it’s your code. You fix them.

Reusable - That CreateEvent step? Used across several tests. Build once, use everywhere.

Maintainable - When business rules change, update the relevant steps. When implementation changes, tests keep passing (hopefully!).

The result

Component tests become your safety net for both refactoring and deployment. They prove the behavior works, so you can refactor freely and deploy on Friday afternoon without breaking into a cold sweat.

Next: Stop fighting your integration tests: two patterns for tests that work

Component tests Series

Comments