Dapr — Testing#

Test metrics#

  • Test files: 301 *_test.go files
  • Source files: 2038 non-test .go files
  • Ratio (test files / source files): ~15% (1:6.8)
  • Test frameworks: testify (assert, require, mock) universally; no gomock, ginkgo, gocheck, or goconvey

Test organization#

  • Placement: Both same-package (white-box) and _test package (black-box) styles coexist. Older subsystems use white-box; newer packages (e.g., pkg/resiliency/breaker, pkg/components/state) use _test package convention.
  • Helper packages:
    • pkg/testing/ — Legacy mock hub. Contains 16 *_mock.go files generated with testify/mock (MockStateStore, MockPubSub, MockDirectMessaging, etc.). These are hand-maintained auto-generated mocks for the original monolithic actor/runtime API.
    • pkg/xxx/fake/ — Modern fake pattern. Every major subsystem under pkg/actors/, pkg/channel/, pkg/healthz/, pkg/security/, pkg/resiliency/, pkg/scheduler/, pkg/sentry/, pkg/runtime/ has a collocated fake/fake.go. Fakes are manually written (not generated), implementing the subsystem’s interface with configurable function fields.
    • tests/integration/framework/ — Custom integration test framework for spawning real binaries. Contains process managers for daprd, placement, scheduler, sentry, injector, operator, and mock services (grpc, http, pubsub, statestore, binding, sqlite).
    • tests/apps/ — Standalone Go HTTP/gRPC apps (30+ apps) that serve as the counterpart sidecar in E2E tests. Each app is a self-contained binary deployed to Kubernetes.
  • Fixtures: testdata/ directories in pkg/config/, pkg/resiliency/, .build-tools/, and pkg/channel/testing/. Used for YAML/JSON config fixtures and certificate materials.

Test patterns#

Table-driven tests#

  • Prevalence: Heavy — 2486 instances of t.Run(, tests :=, testCases :=, tc.name across test files.
  • Style: Anonymous struct slices with descriptive field names. Named struct slices for complex scenarios. Example:
    // pkg/resiliency/policy_test.go:177
    tests := []struct {
        name     string
        retries  int
        timeout  time.Duration
        wantErr  bool
    }{...}
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) { ... })
    }
  • Example: pkg/resiliency/retry_test.go:29testCases with name, cfg, expect, expectErr fields.

Mocking approach#

  • Strategy: Two generations of mocking coexist.
    1. pkg/testing/*_mock.go — Older testify/mock generated mocks for the runtime’s major component interfaces (state.Store, pubsub.PubSub, DirectMessaging, etc.). Used in runtime-level tests where full interface control is needed with call assertions.
    2. pkg/xxx/fake/fake.go — Modern manual fakes. Each fake stores configurable fn* fields (function types) and a New() constructor with sane defaults. Behavior is overridden via WithXxxFn(fn) methods. This is a functional options pattern applied to test doubles:
      // pkg/actors/timers/fake/fake.go
      type Fake struct {
          createFn func(context.Context, *api.CreateTimerRequest) error
          deleteFn func(context.Context, *api.DeleteTimerRequest)
      }
      func (f *Fake) WithCreateFn(fn func(context.Context, *api.CreateTimerRequest) error) *Fake {
          f.createFn = fn
          return f
      }
    The modern fake pattern has no testify/mock dependency — fakes simply implement the interface and are zero-boilerplate to wire.
  • Fake coverage: Actors subsystem alone has 9 fake/ packages: actors/fake, actors/internal/placement/fake, actors/reminders/fake, actors/router/fake, actors/state/fake, actors/table/fake, actors/targets/fake, actors/timers/fake. Shows a systematic approach to decoupled subsystem testing.

Integration tests#

  • Present: Yes — tests/integration/ with //go:build integration build tag.
  • How: Dapr’s integration tests build and run the real binaries (daprd, placement, scheduler, sentry, operator, injector) as subprocesses within go test. The custom framework (tests/integration/framework/) handles:
    • Binary compilation: framework/binary.BuildAll(t) compiles all binaries before any tests run.
    • Port reservation: framework/process/ports.Reserve(t, n) allocates free ephemeral ports.
    • Process lifecycle: Each process type (e.g., process/daprd.Daprd) starts the binary with flags, waits for readiness, and registers t.Cleanup for teardown.
    • Mock services: process/http, process/grpc, process/pubsub, process/statestore spin up in-process Go servers as sidecar counterparts.
  • Test case interface:
    type Case interface {
        Setup(*testing.T) []framework.Option  // declare processes to start
        Run(*testing.T, context.Context)       // execute assertions
    }
    Cases register themselves via init() + suite.Register(new(myCase)) — the same self-registration pattern used for component plugins.
  • Parallelism: Integration tests run in parallel by default (-integration-parallel flag). Each test has a 45-second timeout.
  • Separation: //go:build integration tag gates all integration tests. E2E tests use //go:build e2e. A single _integration_test.go file exists for DNS lookup verification (pkg/actors/internal/placement/connector/dnslookup/).
  • Suite organization: tests/integration/suite/ contains subdirectories per binary: daprd/, placement/, scheduler/, sentry/, actors/, healthz/, helm/, injector/, operator/, ports/.

E2E tests#

  • Located in tests/e2e/ (38 Go files), each test gated with //go:build e2e.
  • E2E tests deploy multi-language apps to a real Kubernetes cluster via tests/runner/ + tests/platforms/kubernetes/. Apps are pre-built Docker images in tests/apps/ (30+ sidecar companion apps in Go, with Java/Python/PHP/Dotnet variants for actor SDK testing).
  • E2E tests exercise cross-cutting features: resiliency, pubsub, actor reminders, hot-reloading, jobs, workflows, metrics, crypto.

Test quality observations#

What’s done well#

  • Binary-level integration tests without containers — The tests/integration/framework/ is one of the most sophisticated integration testing frameworks in the Go ecosystem. It runs real Dapr binaries inside go test without Docker or Kubernetes, making tests fast, deterministic, and local. This is a direct response to the flakiness of container-based integration tests.
  • Systematic fake architecture — The pkg/xxx/fake/ pattern ensures every interface has a canonical test double collocated with the interface. The WithXxxFn override pattern makes fakes maximally flexible without inheritance or fragile reflection.
  • Three-tier test pyramid — Unit (pkg/, go test), Integration (binaries-in-process, go test -tags=integration), E2E (Kubernetes, CI only). Each tier has a clear scope and gating mechanism.
  • Table-driven tests at scale — 2486 parameterized test cases signal genuine test coverage discipline, not just happy-path coverage.
  • Example functionspkg/resiliency/policy_test.go includes Example* functions that serve as both documentation and runnable tests, demonstrating the generic resiliency API.
  • t.Parallel() in integration tests — Integration tests run in parallel by default, dramatically reducing CI time for a suite that spawns multiple processes per test.

What could improve#

  • Two mock generations — The coexistence of pkg/testing/*_mock.go (testify/mock generated) and pkg/xxx/fake/fake.go (manual) creates inconsistency. New contributors must learn both styles. The older mocks could be migrated to the cleaner fake pattern over time.
  • Unit test ratio — 301 test files for 2038 source files (15%) is lower than ideal for a project of this complexity. Some packages under pkg/runtime/ and pkg/actors/ are tested primarily via integration tests, which is appropriate for subsystem contracts but leaves individual unit logic underspecified.
  • No testing.Short() support — No evidence of testing.Short() guards. All unit tests run in full even when a quick smoke test is needed.
  • E2E test dependency on Kubernetes — E2E tests require an external Kubernetes cluster and Azure credentials, making them CI-only. The new integration test framework partially compensates (covers more scenarios locally), but some E2E scenarios remain unreachable without infra.

Patterns worth emulating#

  • The tests/integration/framework/process/ pattern — Building and running real binaries as subprocesses in go test instead of relying on containers or mocking. Gives confidence that the full initialization and shutdown sequence works while remaining hermetic.
  • The fake/fake.go with WithXxxFn override pattern — Collocated, manually written fakes with functional configuration are more readable, more maintainable, and faster than gomock-generated mocks for interfaces with stable method sets.
  • suite.Register + init() for integration test case discovery — The same self-registration pattern used for component plugins applied to test suites. New test cases are added in isolation without modifying a central registry file.