Tailscale — Testing#
Test metrics#
- Test files: 361
- Total Go files: 1,452
- Ratio (test files / source files): ~0.25 (roughly 1 test file per 4 source files)
- Test frameworks: stdlib
testingalmost exclusively;testify/assertin one K8s operator test only (k8s-operator/conditions_test.go);testing/synctest(Go 1.24 experimental) in 9 files
Test organization#
- Placement: Both
package foo(white-box) andpackage foo_test(black-box). The black-box pattern is preferred for package boundaries — e.g.,derp/derp_test.goispackage derp_test,ipn/ipnserver/server_test.goispackage ipnserver_test. White-box tests are used when internal access is required (e.g.,ssh/tailssh/tailssh_test.goispackage tailssh). - Helper packages: Several domain-specific test helper packages exist at named subpaths:
ipn/lapitest— full in-process LocalAPI server for black-box testing ofipnserver. Provideslapitest.NewServer(t, ...), typedClientobjects, and actor-based auth simulation. This is the most sophisticated test helper in the repo.net/stun/stuntest— runs a real STUN server bound to a loopback port; returns address and cleanup function. Pattern:addr, cleanup := stuntest.Serve(t); defer cleanup().appc/appctest— app connector test helpers.tailscale.com/tstest(not present in this clone) — referenced extensively via imports in test files (tstest.WhileTestRunningLogger,tstest.Replace,tstest/deptest,tstest/nettest,tstest/integration,tstest/integration/testcontrol). Thetstest/integrationpackage is run as root in CI and performs network-level integration tests. Its absence from the local clone does not diminish the pattern’s significance.
- Fixtures:
testdata/directories exist inclientupdate/,derp/derpserver/, andipn/ipnlocal/. No generative fixtures; testdata holds static golden files.
Test patterns#
Table-driven tests#
- Prevalence: Heavy — 1,076 occurrences of
t.Run,tests := []struct,testCases, ortc.nameacross test files. - Style: Anonymous struct slices with named fields, always iterated with
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ... }) }. Consistently usesttas the loop variable. - Example:
derp/derp_test.go:38—TestReadFrameHeaderdefines a[]struct{ name, input, wantType, wantLen }table and runs each witht.Run(tt.name, ...).ssh/tailssh/tailssh_test.go:59—TestMatchRuletests SSH rule-matching logic against ~20 cases using the same idiom.
Mocking approach#
- Strategy: Consumer-side narrow interfaces with hand-written fakes — no mock generation framework (no
gomock, nomockery). Test code satisfies a minimal interface defined in the package under test. This aligns with the patterns analysis finding that consumer packages define local interfaces (e.g.,tailssh.ipnLocalBackendwith ~10 methods rather than importing the fullLocalBackend). - envknob for configuration overrides: Tests use
envknob.Setenv("TS_DEBUG_...", value)to override feature flags without dependency injection. Theenvknobpackage exposesSetenvspecifically to enable this (guarded by the!ts_not_in_testsbuild tag). This is a controlled global-state override witht.Cleanupteardown. tstest.Replace: Referenced intailssh_test.go:549— a helper that temporarily replaces a package-level variable for a test (Go’s approach to “mocking” global state safely witht.Cleanupteardown).lapitest.Server: Foripnservertests, a realipnlocal.LocalBackendis wired in-process. Test actors carry identity via anipnauth.TestActorstruct. This is genuine black-box testing of the full LocalAPI stack without any mocking of the HTTP layer.testcontrol.Server: A fake Tailscale control server (tstest/integration/testcontrol) used in integration tests and as a standalone binary (cmd/testcontrol). Referenced incmd/sniproxy/sniproxy_test.goandcontrol/controlclient/controlclient_test.go— tests bring up a real HTTP server that implements the control protocol.
Integration tests#
- Present: Yes, multiple tiers:
ssh/tailssh/tailssh_integration_test.go— tagged//go:build integrationtest. Requires root, exercises real SSH command execution against a running Tailscale SSH server. Uses Docker (testcontainers/) for environment isolation. Run manually viasudo ./tailssh.test -test.run TestIntegration.tstest/integration/(referenced, not in clone) — network-level integration tests run in CI as root with-race. Sharded 1/4 across four parallel runners. Run viacmd/testwrapperwith-exec "sudo -E".cmd/sniproxy/sniproxy_test.go— starts a realtestcontrol.Serverand a real DERP server in-process; exercises SNI proxy logic end-to-end.
- How: All integration tests use in-process servers (
net/http/httptest,testcontrol.Server,stuntest.Serve) rather than external Docker or Testcontainers dependencies (with the exception of the SSH integration test which uses a Dockerfile for container-isolated OS testing). - Separation: Integration tests are separated by build tags (
integrationtest,glidertests) or by living in the dedicatedtstest/integration/package. Regulargo test ./...runs only the unit/functional tests.
Flaky test management#
cmd/testwrapper+flakytest.Mark: Tailscale ships a customgo testwrapper (cmd/testwrapper) that retries tests marked withflakytest.Mark(t, issueURL). Each flaky test is linked to a GitHub issue tracking the root cause. Up tomaxAttempts = 3retry rounds; only the marked-flaky tests are retried — non-flaky failures cause immediate exit. Test sharding is built in:testwrapper sharded:1/4 ./...delegates totool/listpkgsfor package partitioning. This is Tailscale’s bespoke answer to test flakiness at scale.
testing/synctest usage#
- 9 test files use Go 1.24’s experimental
testing/synctestfor deterministic time and goroutine control. Examples:health/health_test.go,control/controlbase/conn_test.go,derp/derphttp/derphttp_test.go. This is bleeding-edge — most Go projects have not yet adopted this API. Tailscale is an early adopter, consistent with its aggressive Go version tracking.
Zero-allocation and benchmark tests#
- 82 benchmark functions (
b.N) are spread across the codebase.testing.AllocsPerRunis used to assert zero-allocation hot paths (e.g.,derp_test.go:80— verifyingReadFrameHeaderallocates nothing). This is appropriate for a high-performance networking daemon.
Dependency tracking tests#
client/local/local_test.gouseststest/deptest.DepCheckerto assert that theclient/localpackage does not accidentally pull in heavy dependencies. This is a rare and valuable pattern: a test that guards import graph size.
Test quality observations#
What’s done well:
- Pervasive table-driven tests with consistent naming conventions (
tt,tc). - Rich in-process test infrastructure (
lapitest,testcontrol) that tests realistic multi-layer behavior without process boundaries. - The
flakytest+testwrappersystem is a mature, production-grade solution to the flaky-test problem that many teams handle with ad-hoc retries or disabling. envknob.Setenvprovides a clean, controlled global-override mechanism for feature flag testing without needing interface injection.- Consumer-defined narrow interfaces mean tests pass lightweight fakes rather than constructing full system objects.
testing/synctestadoption shows investment in time-deterministic tests for async subsystems.- Dependency tracking tests (
deptest) guard against silent binary bloat.
- Pervasive table-driven tests with consistent naming conventions (
What could improve:
- The
tstestpackage tree is missing from this clone, so the full extent of Tailscale’s shared test utilities cannot be assessed — but its referenced API surface suggests significant investment. - Only one test file uses
testify/assert(k8s-operator/conditions_test.go), which is inconsistent. The rest of the codebase uses rawt.Errorf/t.Fatalf— a deliberate choice but occasionally verbose for complex struct comparisons. checklocks:annotations (mutex ownership enforcement viago.uber.org/goleakor a custom vet checker) appear only 17 times — the codebase has significant concurrency but formal lock discipline annotations are sparse.
- The
Patterns worth emulating:
flakytest.Mark(t, issueURL)+testwrapper: Linking flaky tests to tracking issues and retrying them automatically is immediately applicable to any project with CI flakiness. The implementation (~400 lines) is self-contained and easy to transplant.lapitestserver pattern: Building a typed, in-process HTTP test server that acceptstesting.TBand wires real subsystem components is far superior to unit-testing with mocks for HTTP APIs. TheNewUnstartedServer/Start/Clienttrio follows thehttptestidiom but extends it with domain-specific actor management.- Consumer-side narrow interfaces for fakes: Defining a 10-method local interface rather than depending on a 200-method concrete type eliminates fragile mocks and allows tests to compile against a trivial struct.
- envknob test overrides: The
envknob.Setenv+ build-tag approach (!ts_not_in_tests) to enable feature flag overrides in tests — without changing production code paths — is a clean pattern for feature-flag-heavy codebases.