Skip to content

Commit 7d9d4f0

Browse files
committed
Expand README and clarify motivation
1 parent a380c29 commit 7d9d4f0

1 file changed

Lines changed: 94 additions & 15 deletions

File tree

README.md

Lines changed: 94 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,82 @@
11
# testdetect
22

3-
Despite [many](https://github.com/golang/go/issues/12120),
4-
[many](https://github.com/golang/go/issues/14668),
5-
[many](https://github.com/golang/go/issues/21360),
6-
[many](https://github.com/golang/go/issues/60737),
7-
[many](https://github.com/golang/go/issues/60772),
8-
[many](https://github.com/golang/go/issues/64356)
9-
requests and proposals for a compile-time constraint to strip out test-time
10-
specific code, Go still does not have a `test` build tag.
11-
[`testing.Testing()`](https://pkg.go.dev/testing#Testing) was added in Go 1.21
12-
but is sadly not constant, meaning any code gated behind it will still be
13-
present in a release binary.
3+
Sometimes it is useful in Go code to provide test hooks so that methods can
4+
behave differently at test time. Here is a trivial example.
5+
6+
```go
7+
package main
8+
9+
import "fmt"
10+
11+
var testHookGreet func(string) string
12+
13+
func Greet(s string) string {
14+
if h := testHookGreet; h != nil {
15+
return h(s)
16+
}
17+
return fmt.Sprintf("Hello, %s!", s)
18+
}
19+
20+
func main() { println(Greet("world")) }
21+
```
22+
23+
Now `testHookGreet` can be set during testing to override the behavior of the
24+
real `Greet()`. This might be useful for tests that are not testing the
25+
implementation of `Greet()` itself, but in which `Greet()` is still being
26+
called somewhere.
27+
28+
Test hooks are found in several places in the [Go standard library][stdlib]
29+
as well as elsewhere [in the wild][search].
30+
31+
The Go compiler is not currently smart enough to recognize that, in the above
32+
example, `testHookGreet` is always `nil` when the real program is running. As a
33+
result, in the finished program, `Greet()` performs a check to see if
34+
`testHookGreet` is `nil` every time it is called ([godbolt][godbolt]).
35+
36+
This has very little impact in the grand scheme of things, especially if the
37+
program is running with [profile-guided optimization][pgo], but it would be
38+
nice for test instrumentation like this to be brought down to zero impact.
1439

1540
`testdetect` generates a package-local `testingDetector` type with a single
1641
method, `Testing()`. While not a true constant, this method is "constant
1742
enough" that most Go compilers will optimize test related branches out of the
18-
finished binary. In this way, it's the closest thing to an `#ifdef TEST` as we
19-
can get.
43+
finished binary. It's the closest thing to an `#ifdef TEST` as can be achieved
44+
today.
45+
46+
Here it is wired into the previous example.
47+
48+
```go
49+
package main
50+
51+
import "fmt"
52+
53+
var t testingDetector
54+
var testHookGreet func(string) string
55+
56+
func Greet(s string) string {
57+
if h := testHookGreet; t.Testing() && h != nil {
58+
return h(s)
59+
}
60+
return fmt.Sprintf("Hello, %s!", s)
61+
}
62+
63+
func main() { println(Greet("world")) }
64+
```
65+
66+
Now the entire test hook branch is optimized away, leaving the compiled code
67+
effectively the same as if the program were written like this.
68+
69+
```go
70+
package main
71+
72+
import "fmt"
73+
74+
func Greet(s string) string {
75+
return fmt.Sprintf("Hello, %s!", s)
76+
}
77+
78+
func main() { println(Greet("world")) }
79+
```
2080

2181
## Usage
2282

@@ -90,8 +150,21 @@ this code is perfectly valid by Go spec rules and will never fail to run.
90150

91151
I've built this in the hopes that it will someday be retired.
92152

93-
All of this package's complexity could be reduced by treating `_test.go` files
94-
the same as any other build constraint and exposing a `test` build tag.
153+
Despite [many](https://github.com/golang/go/issues/12120),
154+
[many](https://github.com/golang/go/issues/14668),
155+
[many](https://github.com/golang/go/issues/21360),
156+
[many](https://github.com/golang/go/issues/60737),
157+
[many](https://github.com/golang/go/issues/60772),
158+
[many](https://github.com/golang/go/issues/64356)
159+
requests and proposals for a compile-time constraint to strip out test-time
160+
specific code, Go still does not have a `test` build tag.
161+
[`testing.Testing()`](https://pkg.go.dev/testing#Testing) was added in Go 1.21
162+
but is sadly not constant, meaning any code gated behind it will still be
163+
present in a release binary.
164+
165+
Almost all of this utility's work could be could be made redundant if Go
166+
treated `_test.go` files the same as any other build constraint by exposing a
167+
`test` build tag.
95168

96169
```go filename=undertest_false.go
97170
//go:build !test
@@ -116,3 +189,9 @@ it a second time elsewhere in the code would be a compile error.
116189
I am personally of the opinion that this is simpler, more understandable, less
117190
surprising, and a simple `if false == true` check is highly likely to be
118191
optimized out by any current or future Go compiler.
192+
193+
[stdlib]: https://github.com/search?q=repo%3Agolang%2Fgo%20testhook&type=code
194+
[search]:
195+
https://github.com/search?q=NOT+repo%3Agolang%2Fgo+%22var+testhook%22+language%3AGo&type=code
196+
[pgo]: https://go.dev/doc/pgo
197+
[godbolt]: https://godbolt.org/z/fYj1rrEEx

0 commit comments

Comments
 (0)