Dapr — Testing#
Test metrics#
- Test files: 301
*_test.gofiles - Source files: 2038 non-test
.gofiles - 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
_testpackage (black-box) styles coexist. Older subsystems use white-box; newer packages (e.g.,pkg/resiliency/breaker,pkg/components/state) use_testpackage convention. - Helper packages:
pkg/testing/— Legacy mock hub. Contains 16*_mock.gofiles generated withtestify/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 underpkg/actors/,pkg/channel/,pkg/healthz/,pkg/security/,pkg/resiliency/,pkg/scheduler/,pkg/sentry/,pkg/runtime/has a collocatedfake/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 fordaprd,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 inpkg/config/,pkg/resiliency/,.build-tools/, andpkg/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.nameacross 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:29—testCaseswithname,cfg,expect,expectErrfields.
Mocking approach#
- Strategy: Two generations of mocking coexist.
pkg/testing/*_mock.go— Oldertestify/mockgenerated 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.pkg/xxx/fake/fake.go— Modern manual fakes. Each fake stores configurablefn*fields (function types) and aNew()constructor with sane defaults. Behavior is overridden viaWithXxxFn(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 }
- 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 integrationbuild tag. - How: Dapr’s integration tests build and run the real binaries (
daprd,placement,scheduler,sentry,operator,injector) as subprocesses withingo 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 registerst.Cleanupfor teardown. - Mock services:
process/http,process/grpc,process/pubsub,process/statestorespin up in-process Go servers as sidecar counterparts.
- Binary compilation:
- Test case interface:Cases register themselves via
type Case interface { Setup(*testing.T) []framework.Option // declare processes to start Run(*testing.T, context.Context) // execute assertions }init()+suite.Register(new(myCase))— the same self-registration pattern used for component plugins. - Parallelism: Integration tests run in parallel by default (
-integration-parallelflag). Each test has a 45-second timeout. - Separation:
//go:build integrationtag gates all integration tests. E2E tests use//go:build e2e. A single_integration_test.gofile 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 intests/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 insidego testwithout 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. TheWithXxxFnoverride 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 functions —
pkg/resiliency/policy_test.goincludesExample*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) andpkg/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/andpkg/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 oftesting.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 ingo testinstead of relying on containers or mocking. Gives confidence that the full initialization and shutdown sequence works while remaining hermetic. - The
fake/fake.gowithWithXxxFnoverride 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.