Docs Testing Strategy

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:

  • TestOrderCannotBeCancelledAfterShipment
  • TestUnauthorizedUserCannotCreateOrder

Avoid:

  • TestCancelOrderError
  • TestCreateOrderFail

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.