teststyle is a Go linter for declarative tests and black-box-by-default test
packages.
It provides:
- a standalone CLI:
go tool teststyle - a
go/analysisanalyzer - a
golangci-lintmodule-plugin entrypoint - JSON baseline support for incremental cleanup in existing repositories
| Rule ID | Behavior |
|---|---|
teststyle-no-if |
Disallows if statements in Test*, Fuzz*, and Example* functions. |
teststyle-no-switch |
Disallows expression switches and type switches in test functions. |
teststyle-no-goto |
Disallows goto statements in test functions. |
teststyle-whitebox-filename |
Requires same-package test files to be named *_internal_test.go. |
teststyle-whitebox-justification |
Requires a white-box justification comment immediately after the package clause. |
for loops are allowed so table-driven tests can stay compact. Helper functions
may contain conditionals, but helpers should not hide assertion-selection logic.
Parameterless Example* functions can be exempted from the conditional rules
with the -skip-examples flag (skip_examples in plugin settings). An example
is documentation first: the if err != nil it shows is often exactly what a
reader should copy, so a repository can keep examples idiomatic while holding
Test* and Fuzz* functions declarative. The white-box file rules still apply
to example files.
Bad:
func TestParseConfig(t *testing.T) {
got, err := ParseConfig("missing.yaml")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.Name != "app" {
t.Fatalf("got %q", got.Name)
}
}Good:
func TestParseConfig(t *testing.T) {
got, err := ParseConfig("app.yaml")
assertNoError(t, err)
assertEqual(t, got.Name, "app")
}Bad:
func TestRenderDialect(t *testing.T) {
switch dialect {
case "postgres":
assertPostgres(t)
default:
assertGeneric(t)
}
}Good:
func TestRenderDialect_Postgres(t *testing.T) {
assertPostgres(t)
}
func TestRenderDialect_Generic(t *testing.T) {
assertGeneric(t)
}Bad:
func TestCleanup(t *testing.T) {
goto cleanup
cleanup:
assertClean(t)
}Good:
func TestCleanup(t *testing.T) {
assertClean(t)
}Bad:
package config
func TestParseDefaults(t *testing.T) {}Good:
package config_test
func TestParseDefaults(t *testing.T) {}White-box exception:
package config
// White-box testing required: parseDefaults is an unexported state-machine
// helper whose edge cases cannot be isolated through the exported API.
func TestParseDefaults(t *testing.T) {}The file must be named *_internal_test.go.
Bad:
package config
import "testing"
func TestParseDefaults(t *testing.T) {}Good:
package config
// White-box testing required: parseDefaults is an unexported state-machine
// helper whose edge cases cannot be isolated through the exported API.
import "testing"
func TestParseDefaults(t *testing.T) {}Add the tool to your module:
go get -tool github.com/stokaro/teststyle/cmd/teststyleCheck a repository against an existing baseline:
go tool teststyle -baseline .teststyle-baseline.json -root .If the baseline file does not exist, teststyle treats it as an empty baseline.
That makes clean repositories runnable without an adoption file while still
failing on any current violation.
Write a baseline during initial adoption:
go tool teststyle -write-baseline -baseline .teststyle-baseline.json -root .Disable individual rules:
go tool teststyle -disable teststyle-no-if,teststyle-no-switchKeep examples idiomatic while holding tests declarative:
go tool teststyle -skip-examplesCreate a custom golangci-lint build config:
version: v2.3.0
plugins:
- module: github.com/stokaro/teststyle
import: github.com/stokaro/teststyle/golangci
version: v0.1.0Build the custom binary:
golangci-lint customEnable the plugin in .golangci.yml:
version: "2"
linters:
default: none
enable:
- teststyle
settings:
custom:
teststyle:
type: module
description: Declarative Go test style linter.
settings:
baseline_path: .teststyle-baseline.json
root: .The module-plugin path uses the same analyzer and rule IDs as the standalone
CLI. Baseline matching is count-aware, so a baseline entry for one if does not
hide a second newly introduced if.
Complete example configs are available in examples/golangci/.
{
"test_conditionals": [
{
"path": "parser/parser_test.go",
"function": "TestParse",
"kind": "if",
"count": 1
}
],
"white_box_files": [
{
"path": "parser/parser_test.go",
"package": "parser",
"reason": "same-package test file is not named *_internal_test.go"
}
]
}