// code.go
var now = time.Now
// code_test.go
func TestCode(t *testing.T) {
nowSwap := now
t.Cleanup (func() {
now = nowSwap
}
now = func() time.Time {
return time.Date(...)
}
}
Examples
Code: https://github.com/open-telemetry/opentelemetry-go/blob/main...
Test: https://github.com/open-telemetry/opentelemetry-go/blob/490f...> Parallel subtestsWith t.Run(..., func(t testing.T) { t.Parallel(); ... }), the parent test function can return (and thus run its defers) before parallel subtests actually finish.*
If you are going to get into the business of introducing order dependence to test cases through global state (see my other reply on the parent), you will always want the cleanup to work correctly.
1. Using (testing.TB).Cleanup is a good defensive habit to have if you author test helpers, especially if the test helpers (see: (testing.TB).Helper) themselves do something (e.g., resource provisioning) that requires ordered teardown. Using (testing.TB).Cleanup is better than returning a cancellation or cleanup function from them.
2. (testing.TB).Cleanup has stronger guarantees about when it is called, especially when the test case itself crashes. Example: https://go.dev/play/p/a3j6O9RK_OK.
I am certain that I am forgetting another edge case or two here.
Generally nobody should be designing their APIs to be testable through mutable global state. That solves half the problem here.
Others have linked to the much more "fun" https://github.com/bouk/monkey which is an actual monkey patch, in that it changes the code that is called from anywhere in the runtime
And stuff like `func SetTime(...)` in a _test.go file only works for tests in that same package, because other packages don't compile that _test.go and won't have that function defined.
The way it works is that at the start of every function it adds an if statement that atomically checks whether or not the function has been intercepted, and if it did, then executes the replacement function instead. This also addresses the inlining issue.
My tool no longer works since it was rewriting GOPATH, and Go since effectively switched to Go Modules, but if you're persistent enough you can make it work with Go modules too — all you need to do is rewrite the Go module cache instead of GOPATH and you're good to go.
That reminded me of the Go Playground, where it is always 2009
(I suppose Rust is arguably an exception to this; thin runtime, but there's a lot of things the replaced function could do that would still blow Rust up if the rest of the code isn't compiled and correctly optimized to account for whatever the new code does.)