Testing Patterns That Catch Security Regressions
During code review engagements, one of the most reliable indicators of a codebase's security posture is the quality of its test suite — specifically, whether tests exercise error handling paths with the same rigour as happy paths. In our experience reviewing Go codebases for security-sensitive applications, we have found that hand-written mock structs with function fields provide the most effective balance between test flexibility and maintenance cost. This approach, which avoids code generation tools and interface-heavy abstractions, gives reviewers full visibility into what each test asserts and, critically, what it does not.
The pattern is simple. A mock struct mirrors the interface of a dependency, with each method implemented as a call to a corresponding function field. For example, a MockCache struct would have fields GetFunc func(key string) ([]byte, error) and SetFunc func(key string, value []byte) error. Each test configures these function fields to simulate the specific condition under test: a cache hit, a cache miss, a network error on get, a write failure on set. Closures capture the arguments passed to each call, enabling assertions on both the return value and the parameters that the system under test sent to the dependency.
Testing Every Error Path
The security value of this pattern lies in its ability to systematically exercise every error path at a system boundary. Consider a handler that retrieves a user session from cache, falls back to a database lookup on cache miss, and returns an error response on database failure. A minimal test suite would cover the cache hit and cache miss paths. A security-oriented test suite must additionally cover: cache returning a corrupted value, cache returning an error (distinct from a miss), database returning an error after a cache miss, and the cache set operation failing after a successful database lookup. Each of these paths is a location where the application might leak information, bypass an access control check, or enter an inconsistent state.
We have found that the closure-based mock pattern naturally encourages this exhaustive approach because each test case is explicit about the behavior it configures. There is no hidden default behavior, no "auto-mock" that returns zero values for unconfigured methods. If a test does not configure GetFunc, the mock panics — forcing the developer to make a deliberate decision about what each dependency does in each scenario. This explicitness is valuable during security reviews because it makes omissions visible. A test file where SetFunc is never configured to return an error is a clear signal that the error handling path for cache writes has not been verified.
Assertions at System Boundaries
Beyond exercising error paths, the closure pattern enables assertions on the data that crosses system boundaries. When a function calls cache.Set(key, value), the test can inspect both the key and the value to verify that sensitive data is not being cached in plaintext, that cache keys do not contain user-controlled input without sanitisation, and that TTL values are within expected bounds. These are assertions that interface-level mocking frameworks typically do not encourage, because they focus on return values rather than input validation. In security-sensitive systems, the arguments passed to a dependency are often more important than the values returned.
We routinely recommend this testing methodology during code review engagements, particularly for systems that handle authentication tokens, session state, or personally identifiable information. The investment required is modest: a mock struct with function fields can be written in under a minute, and each test case adds perhaps ten lines of setup. The return — comprehensive coverage of error handling paths at every system boundary — is one of the most cost-effective security controls available at the code level. When we encounter a codebase where every error path is tested and every boundary interaction is asserted, the density of security findings drops dramatically.
A final note on maintenance: unlike generated mocks that must be regenerated when interfaces change, hand-written mock structs break at compile time when the interface they implement is modified. This is a feature, not a limitation. A compile error that forces a developer to update every test that touches a changed interface is an opportunity to verify that the new interface contract is tested under adversarial conditions. Generated mocks, by contrast, can be silently regenerated without anyone reviewing whether the test assertions are still meaningful.
← Back to Insights