Viper — Testing#
Test metrics#
- Test files: 12
- Source files: ~21 (non-test .go files; 33 total including test files)
- Ratio (test files / source files): ~0.57 — slightly over half, reasonable for a library
- Test frameworks:
github.com/stretchr/testify/assert+github.com/stretchr/testify/require(universal); stdlibtestingthroughout; no gomock, ginkgo, goconvey, or gocheck
Test organization#
- Placement: Mixed — majority of tests use the
package viperwhite-box package (same package), giving access to unexported fields and internal helpers. The codec sub-packages (internal/encoding/*/) use their own package (black-box).finder_example_test.gousespackage viper_testfor a public API example. - Helper packages:
internal/testutil— single file, single function:AbsFilePath(t, path)wrapsfilepath.Abswitht.Fatalon error. Minimal, deliberately scoped. Follows the “testing helper as package” pattern cleanly witht.Helper().
- Fixtures: No
testdata/directories. All config fixtures are inline byte literals at the top ofviper_test.go(e.g.,yamlExample,tomlExample,jsonExample,dotenvExample,remoteExample). Config content stays co-located with the tests that use it.
Test patterns#
Table-driven tests#
- Prevalence: Moderate — used where input/output variation is the primary concern; not used for every test
- Style: Anonymous struct slice, iterated with
t.Runfor subtests - Example:
util_test.go:63—TestAbsPathifyuses a[]struct{ input, output string }slice and iterates without subtesting (simpler loop, assertions inline) - Subtests via
t.Run: Heavy use for grouping related scenarios under a single test function — e.g.,TestGetConfigFileinviper_test.gohas 8 subtests ("config file set","find file","precedence","without extension","experimental finder","finder", etc.), each constructing its ownafero.MemMapFs
In-memory filesystem for isolation#
- Strategy:
afero.NewMemMapFs()is the dominant isolation technique for file-system-dependent tests. Rather than creating real temp directories, tests construct virtual file systems and inject them viav.SetFs(fs). This produces hermetic, fast tests with no cleanup burden. - Example:
viper_test.go:175—TestGetConfigFilecreates aMemMapFs, adds dirs and files, then callsv.getConfigFile()to verify resolution logic. Not.TempDir()needed. - Real FS tests:
t.TempDir()is used ininitDirs(viper_test.go:126) for tests that specifically validate directory-scanning behavior on real paths (e.g.,TestDirsSearch). The choice between in-memory and real FS is deliberate.
Mocking approach#
- Strategy: Interface-based fakes and stubs — no mock generation framework. Dependencies such as
Finderare small interfaces that are trivially satisfied by test stubs. - Example:
finder_test.go:11—finderStubstruct implements theFinderinterface with a hardcodedresults []stringfield. Constructed inline in the test. Zero dependency on gomock or mockery. - Codec fakes:
encoding_test.go:10— a localcodecstruct implementsCodec(encode returns nil, decode is a no-op), sufficient to testDefaultCodecRegistryregistration and lookup in isolation. - pflag stubs:
viper_test.go:153—stringValuetype implementspflag.Valuedirectly in the test file to exercise flag binding without a full pflag flag.
Environment variable testing#
- Approach:
t.Setenv(key, value)(stdlib, cleans up automatically on test exit). Used throughoutviper_test.gofor testing env binding, prefix stripping, and key replacer behavior. No global state leaks.
Error assertion#
- Style:
assert.ErrorAsused to verify the custom error type hierarchy (viper_test.go:1681):This validates both the deprecated and current error types simultaneously — a test specifically designed to protect theassert.ErrorAs(t, err, &ConfigFileNotFoundError{}) assert.ErrorAs(t, err, &FileNotFoundFromSearchError{}) assert.ErrorAs(t, err, &fileLookupError)Unwrap()-based deprecation migration.
Build-tag variant testing#
- CI matrix: The GitHub Actions workflow (
ci.yaml:51) runsgo testwith three tag variants:"","viper_finder", and"viper_bind_struct". This ensures the experimental feature code paths are covered under their respective build tags. - Example:
viper_test.go:309—"experimental finder"subtest callsNewWithOptions(ExperimentalFinder()), testing the finder code path that only compiles withviper_finderbuild tag.
Benchmarks#
- Present: Yes — 3 benchmarks in
viper_test.go(lines 2678–2713):BenchmarkGetBool,BenchmarkGet,BenchmarkGetBoolFromMap - Purpose: The third benchmark (
BenchmarkGetBoolFromMap) explicitly comments “the perfect result for the above” — it measures a raw map lookup as a performance baseline, allowing comparison against Viper’s multi-layer lookup overhead.
Integration tests#
- Present: No. No
*_integration_test.goor*_e2e_test.gofiles. No Docker, testcontainers, or external service dependency. Tests are fully hermetic. - Separation: Not applicable — there is no integration test tier.
Cross-platform awareness#
- Windows skipping:
skipWindows(t)helper (viper_test.go:2716) is called in tests that rely on POSIX path behavior. CI runs on ubuntu-latest, macos-latest, and windows-latest withfail-fast: falseto let platform-specific failures surface independently.
Test quality observations#
What’s done well:
aferoin-memory filesystem injection is the gold standard for testing a config library’s file-loading logic. Tests are fast, hermetic, and exercise the full resolution path without touching the real filesystem.- The error type deprecation test at
viper_test.go:1681is exemplary: it simultaneously asserts that both the old and new error types satisfyerrors.As, making regression on theUnwrap()chain impossible to miss. - Stub-based mocking via small interfaces keeps test files lean —
finderStub(5 lines) replaces a full gomock setup. - CI matrix covering 3 Go versions × 3 platforms × 3 build tag variants (27 combinations) is thorough for a library of this breadth.
t.Setenvandt.TempDirensure environment and filesystem cleanup without explicitdefercleanup code.
What could improve:
viper_test.goat 2720 lines is too long. It mixes unit tests for internal helpers, integration-style tests using real temp dirs, flag binding tests, and YAML edge cases. Splitting by concern (config loading, env binding, flag binding, type coercion) would improve navigation.- Table-driven tests are used in
util_test.gobut not consistently acrossviper_test.go. Many tests for similar behavior (e.g.,TestGetString,TestGetBool) repeat boilerplate instead of parameterizing. Consistent table-driven style would reduce volume. - The codec sub-package tests (
internal/encoding/*/codec_test.go) each have only 2-3 test functions. Round-trip property testing (encode → decode → compare) would be a natural addition and is absent. - No coverage enforcement in CI — the test command (
go test -race -v -shuffle=on ./...) does not include-coverprofile. Coverage gating or reporting would catch gaps in the multi-source priority logic.
Patterns worth emulating:
afero.MemMapFsfor filesystem tests — swapping the filesystem via interface injection, then using an in-memory implementation in tests, is directly applicable to any code that touches the filesystem.- Benchmark-as-baseline pattern — including a raw-operation benchmark alongside the real implementation benchmark lets readers immediately see the overhead of the abstraction layer.
- Error deprecation test — asserting that
errors.Asworks for both the old and new error types simultaneously when implementingUnwrap()for deprecation is a pattern worth copying whenever error types are migrated. - Build-tag matrix in CI — running the full test suite under each feature flag combination via a CI matrix is cheap and catches flag-conditional bugs that a single test run would miss.