| # Proposal: testing: better support test helper functions with TB.Helper |
| |
| Author: Caleb Spare, based on previous work by Josh Bleecher Snyder |
| |
| Last updated: 2016-12-27 |
| |
| Discussion at https://golang.org/issue/4899. |
| |
| ## Abstract |
| |
| This proposal is about fixing the long-standing issue |
| [#4899](https://golang.org/issue/4899). |
| |
| When a test calls a helper function that invokes, for instance, |
| `"*testing.T".Error`, the line number that is printed for the test failure |
| indicates the `Error` call site as inside the helper method. |
| This is almost always unhelpful for pinpointing the actual failure. |
| |
| We propose to add a new `testing.TB` method, `Helper`, |
| which marks the calling function as a test helper. |
| When logging test messages, package testing ignores frames |
| that are inside marked helper functions. |
| It prints the first stack position inside a non-helper function. |
| |
| ## Background |
| |
| In Go tests, it is common to use a helper function to perform some repeated |
| non-trivial check. |
| These are often of the form |
| |
| func helper(t *testing.T, other, args) |
| |
| though other variants exist. |
| Such helper functions may be local to the test package or may come from external packages. |
| There are many examples of such helper functions in the standard library tests. |
| Some are listed below. |
| |
| When a helper function calls `t.Error`, `t.Fatal`, or a related method, |
| the error message includes file:lineno output that indicates the location of the failure. |
| The failure location is currently considered inside the helper method, which is unhelpful. |
| |
| The misplaced failure location also inhibits useful IDE features like |
| automatically jumping to the failure position. |
| |
| There are a variety of workarounds to which people have resorted. |
| |
| ### 1. Ignore the problem, making it harder to debug test failures |
| |
| This is a common approach. |
| |
| If the helper is only called once from the `Test*` function, |
| then the problem is less severe: |
| the test failure prints the name of the `Test*` function that failed, |
| and by locating the only call to the helper within that function, |
| the user knows the failure site. |
| This is just an annoyance. |
| |
| When the helper is called more than once, it can be impossible to locate the |
| source of the failure without further debugging. |
| |
| A few examples of this pattern in the standard library: |
| |
| - cmd/cover: `func run(c *exec.Cmd, t *testing.T)` |
| - cmd/go: the methods of `testgo` |
| - compress/flate: `writeToType(t *testing.T, ttype string, bw *huffmanBitWriter, tok []token, input []byte)` |
| - crypto/aes: `func mustPanic(t *testing.T, msg string, f func())` |
| - database/sql: `func numPrepares(t *testing.T, db *DB) int` |
| - encoding/json: `func diff(t *testing.T, a, b []byte)` |
| - fmt: `func presentInMap(s string, a []string, t *testing.T)`, `func check(t *testing.T, got, want string)` |
| - html/template: the methods of `testCase` |
| - image/gif: `func try(t *testing.T, b []byte, want string)` |
| - net/http: `func checker(t *testing.T) func(string, error)` |
| - os: `func touch(t *testing.T, name string)` |
| - reflect: `func assert(t *testing.T, s, want string)` |
| - sync/atomic: `func shouldPanic(t *testing.T, name string, f func())` |
| - text/scanner: `func checkPos(t *testing.T, got, want Position)` and some of its callers |
| |
| ### 2. Pass around more context to be printed as part of the error message |
| |
| This approach adds enough information to the failure message to pinpoint the |
| source of failure, at the cost of greater burden on the test writer. |
| The result still isn't entirely satisfactory for the test invoker: |
| if the user only looks at the file:lineno in the failure message, |
| they are still led astray until they examine the full message. |
| |
| Some standard library examples: |
| |
| - bytes: `func check(t *testing.T, testname string, buf *Buffer, s string)` |
| - context: `func testDeadline(c Context, name string, failAfter time.Duration, t testingT)` |
| - debug/gosym: `func testDeadline(c Context, name string, failAfter time.Duration, t testingT)` |
| - mime/multipart: `func expectEq(t *testing.T, expected, actual, what string)` |
| - strings: `func equal(m string, s1, s2 string, t *testing.T) bool` |
| - text/scanner: `func checkTok(t *testing.T, s *Scanner, line int, got, want rune, text string)` |
| |
| ### 3. Use the \r workaround |
| |
| This technique is used by test helper packages in the wild. |
| The idea is to print a carriage return from inside the test helper |
| in order to hide the file:lineno printed by the testing package. |
| Then the helper can print its own file:lineno and message. |
| |
| One example is |
| [github.com/stretchr/testify](https://github.com/stretchr/testify/blob/2402e8e7a02fc811447d11f881aa9746cdc57983/assert/assertions.go#L226). |
| |
| ## Proposal |
| |
| We propose to add two methods in package testing: |
| |
| // Helper marks the current function as a test helper function. |
| // When printing file and line information |
| // from methods such as t.Error, t.Fatal, and t.Log, |
| // the current function will be skipped. |
| func (t *T) Helper() |
| |
| // same doc comment |
| func (b *B) Helper() |
| |
| When package testing prints file:lineno, it walks up the stack, |
| skipping helper functions, and chooses the first entry in a non-helper function. |
| |
| We also propose to add `Helper()` to the `testing.TB` interface. |
| |
| ## Rationale |
| |
| ### Alternative 1: allow the user to specify how many stack frames to skip |
| |
| An alternative fix is to give the user control over the number of stack frames to skip. |
| This is similar to what package log already provides: |
| |
| func Output(calldepth int, s string) error |
| func (l *Logger) Output(calldepth int, s string) error |
| |
| For instance, in https://golang.org/cl/12405043 @robpike writes |
| |
| > // Up returns a *T object whose error reports identify the line n callers |
| > // up the frame. |
| > func (t *T) Up(n) *t { .... } |
| > |
| > Then you could write |
| > |
| > t.Up(1).Error("this would be tagged with the caller's line number") |
| |
| @bradfitz mentions similar APIs in [#4899](https://golang.org/issue/4899) and |
| [#14128](https://golang.org/issue/14128). |
| |
| `Helper` is easier to use, because the user doesn't have to think about stack frames, |
| and it does not break when refactoring. |
| |
| Also, it is not always easy to decide how many frames to skip. |
| A helper may be called through multiple paths, so it may be a variable depth |
| from the desired logging site. |
| For example, in the cmd/go tests, the `"*testgoData".must` helper is called directly |
| by some tests, but is also called by other helpers such as `"*testgoData".cd`. |
| Manual stack control would require the user to pass state into this method |
| in order to know whether to skip one or two frames. |
| Using the `Helper` API, the user would simply mark both `must` and `cd` as helpers. |
| |
| ### Alternative 2: use a special Logf/Errorf/Fatalf sentinel |
| |
| Another approach given by @bradfitz in |
| [#14128](https://github.com/golang/go/issues/14128#issuecomment-176254702) |
| is to provide a magic format value: |
| |
| t.Logf("some value = %v", val, testing.NoDecorate) |
| |
| This seems roughly equivalent in power to our proposal, but it has downsides: |
| |
| * It breaks usual `printf` conventions (@adg [points out that vet would have to |
| be aware of it](https://github.com/golang/go/issues/14128#issuecomment-176456878)). |
| * The mechanism is unusual -- it lacks precedent in the standard library. |
| * `NoDecorate` is less obvious in godoc than a TB method. |
| * Every `testing.T` method in a helper method must be decorated. |
| * It is not clear how to handle nested helpers other than by manually specifying |
| the number of stack frames to skip, inheriting the problems of alternative 1. |
| |
| ## Compatibility |
| |
| Adding a method to `*testing.T` and `*testing.B` raises no compatibility issues. |
| |
| We will also add the method to the `testing.TB` interface. |
| Normally changing interface method sets is verboten, |
| but in this case it is be fine because `TB` has a private method |
| specifically to prevent other implementations: |
| |
| // A private method to prevent users implementing the |
| // interface and so future additions to it will not |
| // violate Go 1 compatibility. |
| private() |
| |
| ## Implementation |
| |
| @cespare will send a CL implementing `"testing.TB".Helper` based on |
| @josharian's previous work in https://golang.org/cl/79890043. |
| |
| The CL will be sent before April 30, 2017 in order to make the 1.9 release cycle. |
| |
| ## Open issues |
| |
| This change directly solves [#4899](https://golang.org/issue/4899). |