Linter to check if your tests have been marked as parallel correctly
Go
61
128 commits
updated Oct 14, 2025
The Go linter paralleltest checks that the t.Parallel gets called for the test method and for the range of test cases within the test.
go install github.com/kunwardeep/paralleltest@latest
paralleltest ./...
A few options can be activated by flag:
-i: Ignore missing calls to t.Parallel and only report incorrect uses of it.-ignoremissingsubtests: Require that top-level tests specify t.Parallel, but don't require it in subtests (t.Run(...)).-ignoreloopVar: Ignore loop variable detection.-checkcleanup: Check that defer is not used with t.Parallel (use t.Cleanup instead to ensure cleanup runs after parallel subtests complete).git clone https://github.com/kunwardeep/paralleltest.git
cd paralleltest
make install_devtools
make ensure_deps
make test
make lint
To fix linting issues automatically:
make lint_fix
The project uses GitHub Actions for continuous integration. The workflow includes:
The workflow runs on:
t.Parallel() in the test method// bad
func TestFunctionMissingCallToParallel(t *testing.T) {
}
// good
func TestFunctionMissingCallToParallel(t *testing.T) {
t.Parallel()
// ^ call to t.Parallel()
}
// Error displayed
// Function TestFunctionMissingCallToParallel missing the call to method parallel
t.Parallel() in the range method// bad
func TestFunctionRangeMissingCallToParallel(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
}{{name: "foo"}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fmt.Println(tc.name)
})
}
}
// good
func TestFunctionRangeMissingCallToParallel(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
}{{name: "foo"}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// ^ call to t.Parallel()
fmt.Println(tc.name)
})
}
}
// Error displayed
// Range statement for test TestFunctionRangeMissingCallToParallel missing the call to method parallel in t.Run
t.Parallel() is called in the range method but testcase variable not being used// bad
func TestFunctionRangeNotUsingRangeValueInTDotRun(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
}{{name: "foo"}}
for _, tc := range testCases {
t.Run("this is a test name", func(t *testing.T) {
// ^ call to tc.name missing
t.Parallel()
fmt.Println(tc.name)
})
}
}
// good
func TestFunctionRangeNotUsingRangeValueInTDotRun(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
}{{name: "foo"}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
fmt.Println(tc.name)
})
}
}
// Error displayed
// Range statement for test TestFunctionRangeNotUsingRangeValueInTDotRun does not use range value in t.Run
t.Parallel() is called in the range method and test case variable tc being used, but is not reinitialised (More Info)// bad
func TestFunctionRangeNotReInitialisingVariable(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
}{{name: "foo"}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
fmt.Println(tc.name)
})
}
}
// good
func TestFunctionRangeNotReInitialisingVariable(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
}{{name: "foo"}}
for _, tc := range testCases {
tc:=tc
// ^ tc variable reinitialised
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
fmt.Println(tc.name)
})
}
}
// Error displayed
// Range statement for test TestFunctionRangeNotReInitialisingVariable does not reinitialise the variable tc
defer with t.Parallel() (requires -checkcleanup flag)When t.Parallel() is called, the test function returns immediately, causing defer statements to execute before subtests complete. This can lead to cleanup happening too early, causing test failures or resource leaks.
// bad - defer runs before subtests complete
func TestWithDeferAndParallel(t *testing.T) {
t.Parallel()
tempFile := "test.tmp"
f, _ := os.Create(tempFile)
defer f.Close() // runs immediately when function returns
defer os.Remove(tempFile) // file deleted before subtests finish!
t.Run("subtest1", func(t *testing.T) {
t.Parallel()
fmt.Fprintf(f, "data") // may fail - file already closed!
})
}
// good - t.Cleanup runs after all subtests complete
func TestWithCleanupAndParallel(t *testing.T) {
t.Parallel()
tempFile := "test.tmp"
f, _ := os.Create(tempFile)
t.Cleanup(func() {
f.Close()
os.Remove(tempFile)
})
t.Run("subtest1", func(t *testing.T) {
t.Parallel()
fmt.Fprintf(f, "data") // works correctly
})
}
// Error displayed (with -checkcleanup flag)
// Function TestWithDeferAndParallel uses defer with t.Parallel, use t.Cleanup instead to ensure cleanup runs after parallel subtests complete
Note: This check is disabled by default. Enable it with the -checkcleanup flag.
git checkout -b feature/amazing-feature)git commit -m 'Add some amazing feature')git push origin feature/amazing-feature)This project is licensed under the MIT License - see the LICENSE file for details.
Go
97.9%
Makefile
2.1%
Linter to check if your tests have been marked as parallel correctly
Go
61
128 commits
updated Oct 14, 2025
The Go linter paralleltest checks that the t.Parallel gets called for the test method and for the range of test cases within the test.
go install github.com/kunwardeep/paralleltest@latest
paralleltest ./...
A few options can be activated by flag:
-i: Ignore missing calls to t.Parallel and only report incorrect uses of it.-ignoremissingsubtests: Require that top-level tests specify t.Parallel, but don't require it in subtests (t.Run(...)).-ignoreloopVar: Ignore loop variable detection.-checkcleanup: Check that defer is not used with t.Parallel (use t.Cleanup instead to ensure cleanup runs after parallel subtests complete).git clone https://github.com/kunwardeep/paralleltest.git
cd paralleltest
make install_devtools
make ensure_deps
make test
make lint
To fix linting issues automatically:
make lint_fix
The project uses GitHub Actions for continuous integration. The workflow includes:
The workflow runs on:
t.Parallel() in the test method// bad
func TestFunctionMissingCallToParallel(t *testing.T) {
}
// good
func TestFunctionMissingCallToParallel(t *testing.T) {
t.Parallel()
// ^ call to t.Parallel()
}
// Error displayed
// Function TestFunctionMissingCallToParallel missing the call to method parallel
t.Parallel() in the range method// bad
func TestFunctionRangeMissingCallToParallel(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
}{{name: "foo"}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fmt.Println(tc.name)
})
}
}
// good
func TestFunctionRangeMissingCallToParallel(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
}{{name: "foo"}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// ^ call to t.Parallel()
fmt.Println(tc.name)
})
}
}
// Error displayed
// Range statement for test TestFunctionRangeMissingCallToParallel missing the call to method parallel in t.Run
t.Parallel() is called in the range method but testcase variable not being used// bad
func TestFunctionRangeNotUsingRangeValueInTDotRun(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
}{{name: "foo"}}
for _, tc := range testCases {
t.Run("this is a test name", func(t *testing.T) {
// ^ call to tc.name missing
t.Parallel()
fmt.Println(tc.name)
})
}
}
// good
func TestFunctionRangeNotUsingRangeValueInTDotRun(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
}{{name: "foo"}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
fmt.Println(tc.name)
})
}
}
// Error displayed
// Range statement for test TestFunctionRangeNotUsingRangeValueInTDotRun does not use range value in t.Run
t.Parallel() is called in the range method and test case variable tc being used, but is not reinitialised (More Info)// bad
func TestFunctionRangeNotReInitialisingVariable(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
}{{name: "foo"}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
fmt.Println(tc.name)
})
}
}
// good
func TestFunctionRangeNotReInitialisingVariable(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
}{{name: "foo"}}
for _, tc := range testCases {
tc:=tc
// ^ tc variable reinitialised
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
fmt.Println(tc.name)
})
}
}
// Error displayed
// Range statement for test TestFunctionRangeNotReInitialisingVariable does not reinitialise the variable tc
defer with t.Parallel() (requires -checkcleanup flag)When t.Parallel() is called, the test function returns immediately, causing defer statements to execute before subtests complete. This can lead to cleanup happening too early, causing test failures or resource leaks.
// bad - defer runs before subtests complete
func TestWithDeferAndParallel(t *testing.T) {
t.Parallel()
tempFile := "test.tmp"
f, _ := os.Create(tempFile)
defer f.Close() // runs immediately when function returns
defer os.Remove(tempFile) // file deleted before subtests finish!
t.Run("subtest1", func(t *testing.T) {
t.Parallel()
fmt.Fprintf(f, "data") // may fail - file already closed!
})
}
// good - t.Cleanup runs after all subtests complete
func TestWithCleanupAndParallel(t *testing.T) {
t.Parallel()
tempFile := "test.tmp"
f, _ := os.Create(tempFile)
t.Cleanup(func() {
f.Close()
os.Remove(tempFile)
})
t.Run("subtest1", func(t *testing.T) {
t.Parallel()
fmt.Fprintf(f, "data") // works correctly
})
}
// Error displayed (with -checkcleanup flag)
// Function TestWithDeferAndParallel uses defer with t.Parallel, use t.Cleanup instead to ensure cleanup runs after parallel subtests complete
Note: This check is disabled by default. Enable it with the -checkcleanup flag.
git checkout -b feature/amazing-feature)git commit -m 'Add some amazing feature')git push origin feature/amazing-feature)This project is licensed under the MIT License - see the LICENSE file for details.
Go
97.9%
Makefile
2.1%