teststyle

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 30, 2026 License: MIT Imports: 16 Imported by: 0

README

teststyle

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/analysis analyzer
  • a golangci-lint module-plugin entrypoint
  • JSON baseline support for incremental cleanup in existing repositories

Rules

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 And Good Examples

teststyle-no-if

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")
}
teststyle-no-switch

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)
}
teststyle-no-goto

Bad:

func TestCleanup(t *testing.T) {
	goto cleanup

cleanup:
	assertClean(t)
}

Good:

func TestCleanup(t *testing.T) {
	assertClean(t)
}
teststyle-whitebox-filename

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.

teststyle-whitebox-justification

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) {}

Standalone Usage

Add the tool to your module:

go get -tool github.com/stokaro/teststyle/cmd/teststyle

Check 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-switch

Keep examples idiomatic while holding tests declarative:

go tool teststyle -skip-examples

golangci-lint Module Plugin

Create 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.0

Build the custom binary:

golangci-lint custom

Enable 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/.

Baseline Format

{
  "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"
    }
  ]
}

Documentation

Overview

Package teststyle audits Go tests for declarative style and black-box-by-default package structure.

Index

Constants

View Source
const (
	RuleNoIf                  = "teststyle-no-if"
	RuleNoSwitch              = "teststyle-no-switch"
	RuleNoGoto                = "teststyle-no-goto"
	RuleWhiteBoxFileName      = "teststyle-whitebox-filename"
	RuleWhiteBoxJustification = "teststyle-whitebox-justification"

	DefaultWhiteBoxJustificationPrefix = "// White-box testing required:"
)

Variables

View Source
var Analyzer = mustAnalyzer(Config{})

Analyzer is the default go/analysis entrypoint with all rules enabled and no baseline.

View Source
var ErrBaselineMismatch = errors.New("teststyle baseline mismatch")

Functions

func Diff

func Diff(want, got Baseline) string

Diff returns a human-readable mismatch report. Empty string means equal.

func NewAnalyzer

func NewAnalyzer(config Config) (*analysis.Analyzer, error)

NewAnalyzer returns a configured go/analysis analyzer.

func WriteBaseline

func WriteBaseline(path string, baseline Baseline) error

WriteBaseline writes a deterministic baseline JSON file.

Types

type Baseline

type Baseline struct {
	TestConditionals []ConditionalBaseline `json:"test_conditionals"`
	WhiteBoxFiles    []WhiteBoxBaseline    `json:"white_box_files"`
}

Baseline records known test-style violations. Cleanup PRs should reduce this file; ordinary feature PRs should keep it unchanged.

func ReadBaseline

func ReadBaseline(path string) (Baseline, error)

ReadBaseline reads a baseline JSON file.

func Scan

func Scan(root string) (Baseline, error)

Scan scans root and returns the current repository test-style baseline.

func ScanWithConfig

func ScanWithConfig(root string, config Config) (Baseline, error)

ScanWithConfig scans root with a custom rule configuration.

func (Baseline) HasFinding

func (b Baseline) HasFinding(finding Finding) bool

HasFinding reports whether finding is recorded in the baseline.

func (*Baseline) Normalize

func (b *Baseline) Normalize()

Normalize sorts baseline entries deterministically.

type ConditionalBaseline

type ConditionalBaseline struct {
	Path     string `json:"path"`
	Function string `json:"function"`
	Kind     string `json:"kind"`
	Count    int    `json:"count"`
}

ConditionalBaseline records prohibited conditional statements in one test function, grouped by statement kind.

type Config

type Config struct {
	DisabledRules               []string `json:"disabled_rules"`
	BaselinePath                string   `json:"baseline_path"`
	Root                        string   `json:"root"`
	WhiteBoxJustificationPrefix string   `json:"white_box_justification_prefix"`
	// SkipExamples exempts parameterless Example* functions from the
	// conditional rules. 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. White-box file rules are unaffected, since they
	// judge files rather than functions.
	SkipExamples bool `json:"skip_examples"`
}

Config controls enabled rules and optional baseline filtering.

type Finding

type Finding struct {
	RuleID   string
	Path     string
	Line     int
	Column   int
	Package  string
	Function string
	Kind     string
	Reason   string
	Message  string
	// contains filtered or unexported fields
}

Finding is a machine-readable test-style violation.

type WhiteBoxBaseline

type WhiteBoxBaseline struct {
	Path    string `json:"path"`
	Package string `json:"package"`
	Reason  string `json:"reason"`
}

WhiteBoxBaseline records a same-package test file that still needs black-box conversion or an explicit white-box justification.

Directories

Path Synopsis
cmd
teststyle command
Package golangci exposes teststyle as a golangci-lint module plugin.
Package golangci exposes teststyle as a golangci-lint module plugin.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL