Testing Strategy
Tests are not just a safety net.
They are executable documentation of architectural intent.
When tests are misaligned with architecture, they become:
- Brittle
- Slow
- Hard to reason about
- Resistant to refactoring
Plumego encourages a testing strategy that mirrors the system’s layers, so that each test answers a clear question — and only that question.
The Core Principle
The foundational rule is simple:
Each layer is tested for its own responsibility, and nothing else.
This means:
- Domain tests do not care about HTTP
- Usecase tests do not care about JSON
- Handler tests do not care about business rules
- Integration tests do not replace unit tests
When tests cross responsibilities, clarity is lost.
The Layered Testing Model
A Plumego system naturally supports a layered testing model:
HTTP / Handlers → Handler Tests
Application → Usecase Tests
Domain → Domain Tests
Infrastructure → Adapter Tests
System → Integration Tests
Each layer answers a different question.
Domain Tests: Invariants and Rules
Question answered:
“Are the business rules always correct?”
Domain tests should:
- Be fast
- Use no mocks
- Have no external dependencies
- Test invariants and state transitions
Example:
func TestOrderCannotBeCancelledAfterShipment(t *testing.T) {
order := Order{Status: Shipped}
err := order.Cancel()
if !errors.Is(err, ErrCannotCancel) {
t.Fatalf("expected ErrCannotCancel, got %v", err)
}
}
If a domain test requires a database, the domain is not pure.
Usecase Tests: Behavior and Flow
Question answered:
“Does this operation behave correctly under different scenarios?”
Usecase tests should:
- Mock interfaces (repositories, services)
- Test authorization and workflow decisions
- Avoid HTTP and framework concerns
Example (conceptual):
func TestCreateOrderForbidden(t *testing.T) {
uc := CreateOrderUsecase{
permission: denyAllPermission{},
}
err := uc.Execute(input)
if !errors.Is(err, ErrForbidden) {
t.Fatalf("expected forbidden, got %v", err)
}
}
Usecase tests validate intent, not transport.
Handler Tests: Boundary Translation
Question answered:
“Is HTTP correctly translated to application behavior?”
Handler tests should:
- Focus on request parsing
- Verify response codes and payloads
- Stub usecases
- Avoid real domain logic
Example:
req := httptest.NewRequest("POST", "/orders", body)
resp := httptest.NewRecorder()
app.ServeHTTP(resp, req)
if resp.Code != http.StatusCreated {
t.Fatalf("unexpected status: %d", resp.Code)
}
If handler tests break when business rules change, boundaries are leaking.
Infrastructure Tests: Adapter Correctness
Question answered:
“Does this adapter correctly talk to the outside world?”
Examples include:
- Database repositories
- External API clients
- Message publishers
Infrastructure tests may:
- Use test containers
- Use local databases
- Be slower than unit tests
They should remain isolated from application logic tests.
Integration Tests: Contract Verification
Integration tests answer a broader question:
“Do these components work together as expected?”
They are appropriate for:
- Startup wiring
- Middleware chains
- Authentication flows
- End-to-end request paths
Integration tests should be few, intentional, and scoped.
They must not replace unit tests.
What Not to Test
Avoid tests that:
- Assert implementation details
- Duplicate logic across layers
- Mock everything until nothing is real
- Encode framework internals
Tests should protect behavior, not code shape.
Avoiding the “Test Pyramid” Dogma
Plumego does not mandate a specific test pyramid.
Instead, it encourages test clarity:
- Fast tests where possible
- Slow tests where necessary
- Explicit tradeoffs
The right balance depends on the system, not on diagrams.
Dependency Injection and Testing
Explicit dependency wiring makes testing straightforward:
- Inject fakes
- Inject stubs
- Avoid global state
- Avoid hidden dependencies
If testing feels hard, wiring is usually the problem.
Error Testing as First-Class Concern
Error paths are part of behavior.
Tests should explicitly cover:
- Validation failures
- Authorization failures
- Domain invariant violations
- Infrastructure failures
Ignoring error paths creates false confidence.
Test Naming and Intent
Test names should express intent, not mechanics.
Prefer:
TestOrderCannotBeCancelledAfterShipmentTestUnauthorizedUserCannotCreateOrder
Avoid:
TestCancelOrderErrorTestCreateOrderFail
Intent-focused naming improves readability and maintenance.
Common Anti-Patterns
End-to-End Tests for Everything
They are slow, brittle, and hide root causes.
Mocking the Domain
This defeats the purpose of having domain logic.
Tests That Encode Architecture Violations
If tests rely on forbidden imports or global state, they normalize bad structure.
Summary
In Plumego:
- Tests mirror architecture
- Each layer is tested independently
- Boundaries are validated, not bypassed
- Errors are tested explicitly
- Wiring determines testability
A good testing strategy does not just catch bugs —
it keeps the system honest over time.
Next
With testing strategy defined, the remaining structural pattern is:
→ Dependency Wiring
This explains how to assemble all layers explicitly, without magic, and make everything testable by construction.