# Your component tests are missing half the story
Table of Contents
Your component tests hit every HTTP endpoint. Great! But something’s still missing.
Users don’t just hit API endpoints. They click “Create Workshop” and expect the complete flow to work - workshop gets created, attendees get notified, venue gets booked, everything happens as expected.
If you’re only testing HTTP requests and responses, you’re missing half your actual boundary.
Your component boundary is bigger than you think
Remember the key principle from earlier posts: test your boundary, not your internals. But what exactly is your boundary?
In event-driven systems, your component boundary isn’t just HTTP in and JSON out. It’s everything that flows across your ownership line: HTTP requests, published events, consumed messages, scheduled jobs, …
When you test only HTTP endpoints, you’re proving “the API call succeeds.” When you test your complete boundary, you’re proving “my part of the user’s workflow actually works.”
Testing complete flows
Instead of testing isolated operations, test complete flows within your boundary. Workshop registration isn’t just “POST returns 201” - it’s a multi-step flow with multiple touchpoints:
// Before: Testing isolated piecesPOST /workshops/123/register → 201 Created ✅
// After: Testing complete flows within your boundaryUser registers for workshop → HTTP: POST /workshops/123/register → 201 Created ✅ Events: WorkshopRegistrationReceived → AttendeeAdded published ✅ State: Workshop capacity updated ✅ Triggers: If full → WaitlistActivated published ✅This tests real workshop management - commands trigger processing, events notify other services about state changes, waitlists get managed automatically.
Here’s what that looks like in practice:
private readonly ScenarioContext _scenarioContext = new ScenarioContext(serviceFixture, testOutputHelper) .Background( new SeedOrganization().WithName("TechCorp Solutions"), new UseApiKey(), new SeedVenue().WithName("Main Conference Room").WithCapacity(2), );
[Fact]public Task WhenWorkshopFillsUp_Then_WaitlistHandlesOverflow(){ return new Scenario(_scenarioContext) .Arrange(new CreateWorkshop().WithTitle("GraphQL Deep Dive").At("Main Conference Room")) .Act( new SimulateMessage<RegistrationMade>() // Fill to capacity .ForWorkshop("GraphQL Deep Dive") .ByAttendee("Alice Debugger"), new SimulateMessage<RegistrationMade>() .ForWorkshop("GraphQL Deep Dive") .ByAttendee("Bob Architect"), new SimulateMessage<RegistrationMade>() // This goes to waitlist .ForWorkshop("GraphQL Deep Dive") .ByAttendee("Carol Designer"), new SimulateMessage<RegistrationCancelled>() // Triggers waitlist promotion .ForWorkshop("GraphQL Deep Dive") .ByAttendee("Alice Debugger") ) .Assert( new WorkshopHasAttendees() .ForWorkshop("GraphQL Deep Dive") .ForAttendees("Bob Architect", "Carol Designer"), // Bob stays, Carol promoted new MessagePublished<AttendeeWaitlisted>() .ForWorkshop("GraphQL Deep Dive") .ForAttendee("Carol Designer"), new MessagePublished<AttendeePromoted>() .ForWorkshop("GraphQL Deep Dive") .ForAttendee("Carol Designer"), new MessageSent<VenueBooked>() .ForVenue("Main Conference Room") );}Multiple inputs, multiple outputs, one business flow. This tests what actually happens in production - messages flowing through your component to complete user workflows.
How to make this work in practice
Capturing what your component actually publishes
You need to see what your component publishes without complex mocking. A message spy captures messages at different stages:
- Published: Your component created the message
- Sent: The message was dispatched to messaging infrastructure
- Consumed: Your component received and processed an incoming message
Most messaging frameworks let you hook into this. MassTransit has IPublishFilter and ISendFilter. NServiceBus uses pipeline behaviors. Rebus has pipeline steps. You register your message spy as a filter to intercept at each stage.
For component tests, use in-memory transport so messages flow instantly:
// Register message spy in your test setupvar messageSpy = new MessageSpy();serviceCollection.AddSingleton<IMessageSpy>(messageSpy);
// Wait for specific messages with realistic conditionsvar workshopUpdate = messageSpy.WaitForPublished<WorkshopUpdated>( msg => msg.WorkshopTitle == "GraphQL Deep Dive" && msg.AttendeeCount == 2, TimeSpan.FromSeconds(5));
var venueBooking = messageSpy.WaitForSent<VenueBooked>( msg => msg.VenueName == "Main Conference Room", TimeSpan.FromSeconds(5));
var registration = messageSpy.WaitForConsumed<RegistrationMade>( msg => msg.WorkshopTitle == "GraphQL Deep Dive" && msg.AttendeeName == "Alice Debugger", TimeSpan.FromSeconds(5));Simulating messages from other services
Use the same message publishing infrastructure to simulate messages from other services:
await messagePublisher.Publish(new RegistrationMade{ WorkshopId = workshopId, AttendeeId = attendeeId, RegistrationTime = timeProvider.GetUtcNow()});Your component processes this exactly like in production.
Stay disciplined about your boundary
Here’s where it gets tricky: when you see your component publish a VenueBooked event, you’ll want to verify that the venue service actually processes it correctly. Resist that urge.
Stay disciplined about your boundary. Your job is proving one thing: “When my component receives X and processes Y, does it produce the expected Z?” What happens after Z is someone else’s problem.
You test that you publish VenueBooked with the right venue name and date. You don’t test that the venue service actually reserves the room - that’s their component test to write.
The result
Component tests for HTTP endpoints give you refactoring confidence. Adding the event-driven flows gives you deployment confidence for the whole user experience.
Next time you write a component test, ask: “Am I testing what my component promises to other services, or just what it promises to HTTP clients?”