Rclone — Testing#

Test metrics#

  • Test files: 316
  • Source files (non-test): 770
  • Ratio (test files / source files): ~0.41 (41%)
  • Test frameworks: stdlib testing everywhere; github.com/stretchr/testify/assert and testify/require used heavily in integration tests and newer unit tests (5,257 assert. calls + 3,039 require. calls across test files); no gomock, ginkgo, or goconvey

Test organization#

  • Placement: Both styles used. Pure unit tests live in a _test package (white-box excluded). Backend-specific internal tests use the same package name with _internal_test.go suffix, granting access to unexported types. The _test.go suffix (no _internal) is used for the integration harness entry points.
  • Helper packages: Rclone has an exceptionally rich dedicated test support library rooted at fstest/:
    • fstest — core utilities: remote name flags (-remote), fstest.Item (expected object state), Initialise() (configures rclone for testing, disables password prompts), CheckItems / CheckListingWithPrecision (assertions on directory listings).
    • fstest/fstests — 2,852-line generic integration test suite (fstests.Run(t, opt)). Defines the canonical conformance test: every backend passes the same ~60 subtests (Put, Get, List, Copy, Move, Purge, Metadata, etc.).
    • fstest/mockfs — hand-written fs.Fs mock (registers itself as a backend named "mockfs").
    • fstest/mockobject — hand-written fs.Object mock, expressed as a string type with method set.
    • fstest/mockdir — hand-written fs.Directory mock.
    • fstest/testy — minimal CI detection (CI(), SkipUnreliable(t)).
    • fstest/testserver — starts/stops real server processes (FTP, SFTP, WebDAV, etc.) by executing shell scripts in fstest/testserver/init.d/ for integration testing against real protocols.
    • fstest/test_all — standalone CLI binary that orchestrates running integration tests across all configured remotes in parallel, with retries and timeout control.
    • fstest/runs — data types (Test, Backend, Run) used by test_all and configurable via config.yaml.
  • Fixtures: testdata/ directories scattered at lib/http/testdata, cmd/serve/*/testdata, cmd/bisync/testdata, fs/config/testdata, fs/rc/rcserver/testdata. Used for TLS certs, sample configs, expected output files. No generated fixtures.

Test patterns#

Table-driven tests#

  • Prevalence: Heavy — 541 sites (from patterns analysis) across _test.go files.
  • Style: Anonymous struct slices with t.Run(tt.name, func(t *testing.T) {...}). Occasionally positional (no name field, just index).
  • Example: backend/s3/s3_test.go:100TestParseRetainUntilDate uses an anonymous struct with name, input, wantErr, checkFunc fields; iterates with for _, tt := range tests { t.Run(tt.name, ...) }.

Mocking approach#

  • Strategy: Hand-written fakes, no generation framework.
  • Example: fstest/mockobject/mockobject.go defines type Object string which implements the full fs.Object interface. Unimplemented methods return errNotImpl. fstest/mockfs/mockfs.go provides a minimal fs.Fs that registers itself via fs.Register so it can be addressed as "mockfs:" in tests.
  • InternalTest protocol: Backends that have backend-specific integration tests implement func (f *Fs) InternalTest(t *testing.T) (in their _internal_test.go). fstests.Run detects this method via type assertion and calls it automatically, integrating bespoke tests into the standard suite. Examples: backend/s3/s3_internal_test.go:611, backend/drive/drive_internal_test.go:684, backend/crypt/crypt_internal_test.go:122.
  • Compile-time assertions: Backends export test-helper methods by promoting unexported methods, then assert the interface is satisfied: var _ fstests.SetUploadChunkSizer = (*Fs)(nil) (backend/s3/s3_test.go:93). This ensures the test harness can call f.SetUploadChunkSize(...) without reflection.

Integration tests#

  • Present: Yes — the defining testing characteristic of rclone.
  • How: fstests.Run(t, &fstests.Opt{RemoteName: "TestS3:", NilObject: (*Object)(nil), ...}) in each backend’s *_test.go. The suite connects to the named remote, creates a temporary directory (rclone-test-<12-char-random>), exercises the full Fs interface, then cleans up. testserver can start local FTP/SFTP/etc. servers via init.d scripts for testing those backends without cloud credentials.
  • Separation: No build tags. Instead, quicktest (make quicktest) sets RCLONE_CONFIG="/notfound". When rclone cannot find a config, all configured remotes skip with t.Skipf("WARN: %q not configured", remoteName) inside fstests.Run. This gracefully degrades to unit-only mode without code changes. Full integration runs use actual cloud credentials, typically run by project maintainers via test_all.
  • test_all orchestrator: fstest/test_all/test_all.go is a standalone binary accepting -remotes, -backends, -maxtries (default 5), -n (parallelism, default 20), -timeout (default 60 min). Reads fstest/runs/config.yaml which lists all backends with their test options (FastList, OneOnly, ExtraTime, etc.). Produces a test report with pass/fail counts per remote.

Test quality observations#

What’s done well#

  • Generic conformance suite (fstests): The single most notable testing achievement. A 2,852-line suite that every one of the 70+ backends must pass. This is architecturally enforced correctness — adding a new backend means running fstests.Run and passing every subtest. The suite covers Unicode paths, large files, chunked upload, metadata, tier changes, copy/move server-side, empty directory handling, and more.
  • RCLONE_CONFIG=/notfound trick: Elegant runtime gating that avoids build-tag fragmentation. All integration tests are compiled in the binary and skipped gracefully when the remote isn’t configured. Developers always have accurate compile-time checking.
  • InternalTest protocol: A clean extension point that lets backends add their own integration subtests without forking or duplicating the harness setup code. The backend just implements a method; fstests.Run discovers it automatically.
  • Compile-time interface assertions in tests: var _ fstests.SetUploadChunkSizer = (*Fs)(nil) prevents silent test gaps — if a backend doesn’t export the required helper method, the package fails to compile.
  • testify adoption is consistent: All integration and newer unit tests use require.NoError(t, err) / assert.Equal(t, want, got) uniformly, making test failures self-describing without custom message scaffolding.

What could improve#

  • No build-tag separation: While the /notfound trick is elegant, it means go test ./... will attempt to connect to remotes that happen to be configured on the developer’s machine. Build tags like //go:build integration would make intent explicit.
  • testify adoption is inconsistent across age layers: Older packages (parts of fs/, lib/) use bare t.Errorf / t.Fatalf while newer tests use testify. Not a defect, but inconsistent reading experience.
  • test_all not in CI: The comprehensive multi-remote integration test harness runs only when maintainers run it manually with real credentials. CI only runs quicktest and racequicktest. This means regressions against real cloud APIs are caught asynchronously.
  • Limited unit test coverage for sync core: fs/sync/sync.go is the most complex module (two-stage pipeline, 13 configurable parameters) but its test file (fs/sync/sync_test.go) exercises it almost entirely via full integration with a real (local) filesystem rather than unit-testing the pipeline mechanics in isolation.

Patterns worth emulating#

  • Generic backend conformance suite (fstests.Run): Applicable whenever a library has multiple pluggable backends. Define one canonical test suite; every implementation runs it. Catches interface drift and missing features automatically.
  • Runtime integration skip via impossible config path: Simpler than build tags for many projects. Set an env var to a file that doesn’t exist; the init code gracefully exits; unit tests still run normally.
  • InternalTest method hook: A lightweight, zero-reflection-overhead extension protocol for injecting backend-specific tests into a shared harness. The type assertion if it, ok := f.(InternalTester); ok { it.InternalTest(t) } keeps the harness agnostic without requiring registration.
  • Compile-time assertions for test helpers: var _ Interface = (*ConcreteType)(nil) in test files ensures test-scaffolding methods are correctly exported before any test runs.