frp — Testing#
Test metrics#
- Test files: 29 (
*_test.go) - Source files: 263 (
.go, excluding tests) - Ratio (test files / source files): ~11% — low, reflecting that most behavioral coverage lives in the E2E suite rather than unit tests
- Test frameworks:
github.com/stretchr/testify/require— unit tests (all 28 non-E2E test files)github.com/onsi/ginkgo/v2+github.com/onsi/gomega— E2E suite undertest/e2e/
Test organization#
- Placement: Both strategies are used.
- Most unit tests are same-package (e.g.,
client/config_manager_test.godeclarespackage client), giving direct access to unexported fields for white-box assertions. - Some are
_testpackage (e.g.,pkg/config/v1/proxy_test.godeclarespackage v1), treating the package as a black box.
- Most unit tests are same-package (e.g.,
- Helper packages:
test/e2e/framework/— full Ginkgo test framework:Frameworkstruct handles temp directories, port allocation, mock server lifecycle, frps/frpc process management, Go-template config rendering, and cleanup actions.test/e2e/mock/server/—Serverinterface plus implementations:httpserver,streamserver(TCP/UDP/Unix echo),oidcserver.test/e2e/pkg/— supporting utilities:port.Allocator(range-partitioned port reservation),process.Process(subprocess management with stdout/stderr capture andWaitForOutput),request.Request(fluent HTTP/TCP test-request builder),cert(self-signed TLS cert generator),rpc,ssh/client.
- Fixtures: No
testdata/directories observed. Config fixtures are inline Go string constants or built programmatically using Go text templates (e.g.,consts.DefaultServerConfig+{{ .PortName }}placeholders rendered by the Framework).
Test patterns#
Table-driven tests#
- Prevalence: Heavy in config and serialization packages; present but lighter in service/component packages.
- Style: Anonymous struct slices (
tests := []struct{ name string; content string }{...}) witht.Run(test.name, func(t *testing.T) { ... }). - Example:
pkg/config/load_test.go:61—TestLoadServerConfigruns the same parse-and-assert logic against TOML, YAML, and JSON representations of the same config.pkg/config/load_test.go:549—TestFindFieldLineInContentmaps field path strings to expected line numbers in a small table.
Mocking approach#
- Strategy: Manual interface fakes — no code-generated mocks (no mockery, gomock, etc.).
- Examples:
client/service_test.go:18-28—failingConnectorimplements theConnectorinterface with a configurable error return; injected viaServiceOptions.ConnectorCreatorto test thatRun()cleans up on login failure.server/group/base_test.go:15-48—fakeLnimplementsnet.Listenerusing buffered channels and async.Onceclose guard, used to drive the group’s worker goroutine without real TCP sockets.test/e2e/mock/server/— full in-process echo servers (streamserver.TCP/UDP/Unix,httpserver,oidcserver) implement theServerinterface and serve as real network targets for E2E tests.
Integration tests#
- Present: Yes — a sophisticated process-level E2E suite, plus some unit tests that bind real TCP ports.
- How:
- Process-based E2E (
test/e2e/): theFrameworkwrites rendered config files to temp directories, then starts actualfrpsandfrpcbinaries as child processes. Readiness is detected by either polling TCP connectivity (for frps) or watching the process’s stdout for"start proxy success"log lines per proxy (framework/process.go:125-145). After each test, processes are stopped, ports released, and temp directories cleaned. - In-process unit-level integration (
client/service_test.go:34-44):getFreeTCPPortbinds a real ephemeral TCP port, passes it as the admin server port, then asserts the port is released after a failedRun()— verifying real OS port lifecycle. - Ginkgo parallel E2E:
test/e2e/e2e.goenablesRandomizeAllSpecs = trueand supportsParallelTotal/ParallelProcesssharding; the port allocator partitions port ranges by Ginkgo node to avoid collisions.
- Process-based E2E (
- Separation: The entire E2E suite is under
test/e2e/and is invoked as a separatemake e2etarget (distinct frommake testfor unit tests). No build tags are used; separation is purely by directory and Makefile target.
E2E framework design (notable)#
The test/e2e/framework package is architecturally modeled after the Kubernetes E2E framework (Ginkgo BeforeEach/AfterEach lifecycle registration, CleanupActionHandle, SynchronizedBeforeSuite/SynchronizedAfterSuite). Key design points:
- Template-based config generation: Configs are Go text templates with
{{ .PortName }}placeholders. The Framework allocates real ports and renders the templates before writing them to disk, decoupling test logic from concrete port numbers. - Dual config lineage: Both
test/e2e/legacy/(INI-style, pre-v1 config) andtest/e2e/v1/(TOML/YAML-style) suites coexist, covering both config formats. Both are blank-imported intoe2e_test.goto auto-register their Ginkgo specs. NewRequestExpectfluent builder (framework/request.go): wraps the request package into a chainable assertion —.Protocol("tcp").PortName("Foo").ExpectError(false).Ensure()— used pervasively in E2E scenario specs.- Registered cleanup actions (
framework/cleanup.go): a global list of cleanup handles ensures teardown runs even when Ginkgo aborts, preventing port leaks across test runs.
Test quality observations#
What’s done well:
- The E2E framework is production-quality infrastructure: parallel-safe port allocation, process lifecycle management with readiness detection, and automatic cleanup. It validates real protocol behavior (TCP tunneling, QUIC, STCP, HTTP proxying) end-to-end.
- Service-level unit tests use constructor injection (
ServiceOptions.ConnectorCreator) precisely: they swap in afailingConnectorto test cleanup paths without needing a real server. This is the correct use of the DI injection point from the patterns analysis. - Error sentinel testing is thorough —
errors.Is(err, configmgmt.ErrConflict)etc. appear consistently in config manager tests, documenting and enforcing the error taxonomy. - Idempotency tests (
TestCompleteProxyConfigurers_Idempotent,TestCompleteVisitorConfigurers_Idempotent) verify that callingComplete()twice on a config struct is safe — important for hot-reload correctness. fakeLninserver/group/base_test.gois an exemplary minimal fake: channel-backed,sync.Once-closed, implementsnet.Listenercleanly. Tests cover fan-out, stop-on-close, and closed-channel panic recovery.- Config loading tests (
pkg/config/load_test.go) are exhaustively parametric: TOML/YAML/JSON, strict vs. lenient, YAML merge keys, edge cases (array at root, scalar at root).
What could improve:
- Unit test coverage ratio (~11%) is low relative to the codebase size. Core subsystems — proxy routing (
client/proxy/), server connection handling (server/control.go), NAT hole-punching (pkg/nathole/) — have zero unit tests. All behavioral confidence in these paths comes from E2E, which is slower and harder to debug. - No
-raceflag is visible in the CI configuration (make alltest→go testwithout-race). For a codebase with 45 goroutines, 55 select blocks, 8sync.Once, and atomic state, race-detector runs would be valuable. - E2E readiness still falls back to
time.Sleep(1500ms)for visitor-only clients (framework/process.go:77-80), since visitors have no deterministic “ready” log line. This is a known limitation acknowledged in comments. - No benchmark (
*_bench_test.go) or fuzz tests visible. The rate-limiting and bandwidth-shaping paths inpkg/util/limit/would be natural candidates.
- Unit test coverage ratio (~11%) is low relative to the codebase size. Core subsystems — proxy routing (
Patterns worth emulating:
- Template-rendered config files for E2E: eliminates port-number constants, enables parallel test sharding, and keeps test specs focused on behavior rather than port management.
- Process stdout
WaitForOutputfor readiness: instead of sleeping, scan the child process’s stdout for a specific log string. Used inwaitForClientProxyReady— more deterministic and faster than fixed sleeps. - Constructor injection with a creator function (
ConnectorCreator func(context.Context, *v1.ClientCommonConfig) Connector): a single field inServiceOptionsmakes the entire service testable without mocking frameworks, by substituting the transport layer. fakeLnpattern: implementingnet.Listenerwith channels andsync.Onceis the idiomatic minimal fake for any code thatAccept()s connections — reusable across many Go projects.