Tast: Code Review Comments (go/tast-code-review-comments)

This document collects common comments made during reviews of Tast tests.

Tast code should also follow Go's established best practices as described in Effective Go and Go Code Review Comments.
Go Style (internal document similar to public style guides) is also a good read and source of style suggestions.

CombinedOutput

In general you should not parse the result of CombinedOutput. Its result interleaves stdout and stderr, so parsing it is very likely flaky.

If the message you are concerned with is written to stdout, use Output instead. In the case of stderr, capture it explicitly with Run.

// GOOD
out, err := testexec.CommandContext(...).Output()
if regexp.Match(out, "...") { ... }
// GOOD
cmd := testexec.CommandContext(...)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(...); err != nil { ... }
out := stderr.Bytes()
if regexp.Match(out, "...") { ... }
// BAD
out, err := testexec.CommandContext(...).CombinedOutput()
if regexp.Match(out, "...") { ... }

Context cancellation

After calling functions to create a new context.Context with a new deadline (e.g. ctxutil.Shorten, context.WithTimeout etc.), always call the cancel function with a defer statement. In many cases those functions start a new goroutine associated with the new context, and it is released only on cancellation or expiration of the context. Thus failing to cancel the context may lead to resource leaks.

// GOOD
ctx, cancel := ctxutil.Shorten(ctx, 5*time.Second)
defer cancel()
// BAD
ctx, _ = ctxutil.Shorten(ctx, 5*time.Second)

This article describes how a similar bug caused massive production issues at Square.

Context timeout

context.Context carries the deadline of a function call. Functions that may block should take context.Context as an argument and honor its deadline.

// GOOD
func httpGet(ctx context.Context, url string) ([]byte, error) { ... }
// BAD
func httpGet(url string) ([]byte, error) { ... }

Preconditions

Whenever possible, use preconditions rather than calling setup functions by yourself. Preconditions speeds up testing when multiple tests sharing the same precondition are run at a time (e.g. in the commit queue).

// GOOD
func init() {
	testing.AddTest(&testing.Test{
		Func: Example,
		Pre: chrome.LoggedIn(),
		...
	})
}
// BAD
func Example(ctx context.Context, s *testing.State) {
	cr, err := chrome.New(ctx)
	if err != nil {
		s.Fatal("Failed to start Chrome: ", err)
	}
	...
}

See also a section in the Writing Tests document.

Sleep

Sleeping without polling for a condition is discouraged, since it makes tests flakier (when the sleep duration isn't long enough) or slower (when the duration is too long).

The testing.Poll function makes it easier to honor timeouts while polling for a condition:

// GOOD
startServer()
if err := testing.Poll(ctx, func (ctx context.Context) error {
	return checkServerReady()
}, &testing.PollOptions{Timeout: 10 * time.Second}); err != nil {
	return errors.Wrap(err, "server failed to start")
}
// BAD
startServer()
testing.Sleep(ctx, 10*time.Second) // hope that the server starts in 10 seconds

If you really need to sleep for a fixed amount of time, use testing.Sleep to honor the context timeout.

State

testing.State implements a lot of methods allowing tests to access all the information and utilities they may use to perform testing. Since it contains many things, passing it to a function as an argument makes it difficult to tell what are inputs and outputs of the function from its signature. Also, such a function cannot be called from gRPC services.

For this reason, tast-lint forbids use of testing.State in support library packages. Other packages, such as test main functions and test subpackages, can still use testing.State, but think carefully if you really need it.

In many cases you can avoid passing testing.State to a function:

As an exception, support libraries implementing preconditions can refer testing.State because it is absolutely necessary. Please feel free to send a change to add your package to the allowlist in tast-lint.

Test dependencies

Avoid skipping tests or subtests by checking device capabilities at runtime. Instead specify software/hardware dependencies to declare software features and/or hardware capabilities your test depends on.

// GOOD
func init() {
	testing.AddTest(&testing.Test{
		Func: CheckCamera,
		SoftwareDeps: []string{"camera_720p", "chrome"},
		...
	})
}
// BAD
func CheckCamera(ctx context.Context, s *testing.State) {
	if !supports720PCamera() {
		s.Log("Skipping test; device unsupported")
		return
	}
	...
}

See also a section in the Writing Tests document.