Antonboom/testifylint
GitHub: Antonboom/testifylint
一款 Go 语言静态分析 linter,专门检查和规范 testify 测试框架的使用方式,帮助开发者避免常见断言错误并提升测试代码质量。
Stars: 169 | Forks: 18
# testifylint

[](https://github.com/Antonboom/testifylint/actions/workflows/ci.yml)
[](https://goreportcard.com/report/github.com/Antonboom/testifylint?dummy=unused)
[](https://coveralls.io/github/Antonboom/testifylint?branch=master&dummy=unused)
[](LICENSE)
[](https://github.com/Antonboom/testifylint/blob/master/CONTRIBUTING.md#Open-for-contribution)
检查 [github.com/stretchr/testify](https://github.com/stretchr/testify) 的使用情况。
## 目录
* [问题描述](#problem-statement)
* [安装与使用](#installation--usage)
* [配置](#configuring)
* [检查器](#checkers)
* [警告链](#chain-of-warnings)
* [testify v2](#testify-v2)
## 问题描述
测试也是程序代码,对它们的要求不应与对被测代码
的要求有太大差异 🙂
我们应该努力保持测试的一致性,提高其可读性,降低产生 bug 的几率,
并加快问题的排查速度。
[testify](https://github.com/stretchr/testify) 是近年来最受欢迎的 Golang 测试框架*。
但它在某些地方有着极其糟糕且含糊不清的 API,**此 linter 的目的就是保护你免受那些
烦人的错误**。
_* JetBrains "Go 生态系统状况" 报告
[2021](https://www.jetbrains.com/lp/devecosystem-2021/go/#Go_which-testing-frameworks-do-you-use-regularly-if-any)
和 [2022](https://www.jetbrains.com/lp/devecosystem-2022/go/#which-testing-frameworks-do-you-use-regularly-if-any-)._
## 安装与使用
```
$ go install github.com/Antonboom/testifylint@latest
$ testifylint -h
$ testifylint ./...
```
### 修复
```
$ testifylint --fix ./...
```
由于
[golangci/golangci-lint#1779](https://github.com/golangci/golangci-lint/issues/1779),
目前**无法**通过 `golangci-lint` 进行修复。
请注意,修复后可能会出现未使用的 import,请运行 `go fmt`。
## 配置
### CLI
```
# 启用所有 checker。
$ testifylint --enable-all ./...
# 仅启用特定的 checker。
$ testifylint --disable-all --enable=empty,error-is-as ./...
# 仅禁用特定的 checker。
$ testifylint --enable-all --disable=empty,error-is-as ./...
# checker 配置。
$ testifylint --bool-compare.ignore-custom-types ./...
$ testifylint --expected-actual.pattern=^wanted$ ./...
$ testifylint --formatter.check-format-string --formatter.require-f-funcs --formatter.require-string-msg ./...
$ testifylint --go-require.ignore-http-handlers ./...
$ testifylint --require-error.fn-pattern="^(Errorf?|NoErrorf?)$" ./...
$ testifylint --suite-extra-assert-call.mode=require ./...
```
### golangci-lint
https://golangci-lint.run/docs/linters/configuration/#testifylint
## 检查器
- ✅ – 是
- ❌ – 否
- 🤏 – 部分
| 名称 | 默认启用 | 自动修复 |
|-----------------------------------------------------|--------------------|---------|
| [blank-import](#blank-import) | ✅ | ❌ |
| [bool-compare](#bool-compare) | ✅ | ✅ |
| [compares](#compares) | ✅ | ✅ |
| [contains](#contains) | ✅ | 🤏 |
| [empty](#empty) | ✅ | ✅ |
| [encoded-compare](#encoded-compare) | ✅ | ✅ |
| [equal-values](#equal-values) | ✅ | ✅ |
| [error-is-as](#error-is-as) | ✅ | 🤏 |
| [error-nil](#error-nil) | ✅ | ✅ |
| [expected-actual](#expected-actual) | ✅ | ✅ |
| [float-compare](#float-compare) | ✅ | ❌ |
| [formatter](#formatter) | ✅ | 🤏 |
| [go-require](#go-require) | ✅ | ❌ |
| [len](#len) | ✅ | ✅ |
| [negative-positive](#negative-positive) | ✅ | ✅ |
| [nil-compare](#nil-compare) | ✅ | ✅ |
| [regexp](#regexp) | ✅ | ✅ |
| [require-error](#require-error) | ✅ | ❌ |
| [suite-broken-parallel](#suite-broken-parallel) | ✅ | ✅ |
| [suite-dont-use-pkg](#suite-dont-use-pkg) | ✅ | ✅ |
| [suite-extra-assert-call](#suite-extra-assert-call) | ✅ | ✅ |
| [suite-method-signature](#suite-method-signature) | ✅ | ❌ |
| [suite-subtest-run](#suite-subtest-run) | ✅ | ❌ |
| [suite-thelper](#suite-thelper) | ❌ | ✅ |
| [useless-assert](#useless-assert) | ✅ | ❌ |
### blank-import
```
❌
import (
"testing"
_ "github.com/stretchr/testify"
_ "github.com/stretchr/testify/assert"
_ "github.com/stretchr/testify/http"
_ "github.com/stretchr/testify/mock"
_ "github.com/stretchr/testify/require"
_ "github.com/stretchr/testify/suite"
)
✅
import (
"testing"
)
```
**自动修复**:否。
**默认启用**:是。
**原因**:`testify` 没有任何 `init()` 魔法,因此这些作为 `_` 的 import 毫无用处,被视为无用代码。 ### bool-compare ``` ❌ assert.Equal(t, false, result) assert.EqualValues(t, false, result) assert.Exactly(t, false, result) assert.NotEqual(t, true, result) assert.NotEqualValues(t, true, result) assert.False(t, !result) assert.True(t, result == true) // And other variations... ✅ assert.True(t, result) assert.False(t, result) ``` **自动修复**:是。
**默认启用**:是。
**原因**:简化代码。 此外,`bool-compare` 还支持用户自定义类型,例如 ``` type Bool bool ``` 并通过将变量转换为内置 `bool` 来修复断言: ``` var predicate Bool ❌ assert.Equal(t, false, predicate) ✅ assert.False(t, bool(predicate)) ``` 使用 `--bool-compare.ignore-custom-types` 标志可关闭此行为。 ### compares ``` ❌ assert.True(t, a == b) assert.True(t, a != b) assert.True(t, a > b) assert.True(t, a >= b) assert.True(t, a < b) assert.True(t, a <= b) assert.False(t, a == b) // And so on... ✅ assert.Equal(t, a, b) assert.NotEqual(t, a, b) assert.Greater(t, a, b) assert.GreaterOrEqual(t, a, b) assert.Less(t, a, b) assert.LessOrEqual(t, a, b) ``` **自动修复**:是。
**默认启用**:是。
**原因**:更合适的 `testify` API 以及更清晰的失败信息。 如果 `a` 和 `b` 是指针,则必须改用 `assert.Same`/`NotSame`, 因为 `assert.Equal` 具有不合适的递归特性(基于 [reflect.DeepEqual](https://pkg.go.dev/reflect#DeepEqual))。 ### contains ``` ❌ assert.True(t, strings.Contains(a, "abc123")) assert.False(t, !strings.Contains(a, "abc123")) assert.False(t, strings.Contains(a, "abc123")) assert.True(t, !strings.Contains(a, "abc123")) assert.Contains(t, arr, 1, 2) assert.NotContains(t, arr, 1, 2) ✅ assert.Contains(t, a, "abc123") assert.NotContains(t, a, "abc123") assert.Subset(t, arr, 1, 2) assert.NotSubset(t, arr, 1, 2) ``` **自动修复**:部分。
**默认启用**:是。
**原因**:防止 bug、简化代码,以及使用更合适的 `testify` API 和更清晰的失败信息。 #### Kubernetes 测试中的 bug 示例
#### Prometheus 测试中的 bug 示例
### empty
```
❌
assert.Len(t, arr, 0)
assert.Zero(t, str)
assert.Zero(t, len(arr))
assert.Equal(t, 0, len(arr))
assert.EqualValues(t, 0, len(arr))
assert.Exactly(t, 0, len(arr))
assert.LessOrEqual(t, len(arr), 0)
assert.GreaterOrEqual(t, 0, len(arr))
assert.Less(t, len(arr), 1)
assert.Greater(t, 1, len(arr))
assert.Equal(t, "", str)
assert.EqualValues(t, "", str)
assert.Exactly(t, "", str)
assert.Equal(t, ``, str)
assert.EqualValues(t, ``, str)
assert.Exactly(t, ``, str)
assert.Positive(t, len(arr))
assert.NotZero(t, str)
assert.NotZero(t, len(arr))
assert.NotEqual(t, 0, len(arr))
assert.NotEqualValues(t, 0, len(arr))
assert.Greater(t, len(arr), 0)
assert.Less(t, 0, len(arr))
assert.NotEqual(t, "", str)
assert.NotEqualValues(t, "", str)
assert.NotEqual(t, ``, str)
assert.NotEqualValues(t, ``, str)
✅
assert.Empty(t, arr)
assert.NotEmpty(t, arr)
```
**自动修复**:是。
**默认启用**:是。
**原因**:更合适的 `testify` API 以及更清晰的失败信息。 此外,`empty` 会在 `*Emtpy` 断言中移除多余的 `len` 调用: ``` ❌ assert.Empty(t, len(arr)) assert.NotEmpty(t, len(arr)) ✅ assert.Empty(t, arr) assert.NotEmpty(t, arr) ``` 附注:`empty` 不会移除字符串转换并保持原样: - `string(bytes)` – 保持断言失败信息的可读性; - `string(str)` – 倾向于使用 [unconvert](https://golangci-lint.run/usage/linters/#unconvert)。 ### encoded-compare ``` ❌ assert.Equal(t, `{"foo": "bar"}`, body) assert.EqualValues(t, `{"foo": "bar"}`, body) assert.Exactly(t, `{"foo": "bar"}`, body) assert.Equal(t, expectedJSON, resultJSON) assert.Equal(t, expBodyConst, w.Body.String()) assert.Equal(t, fmt.Sprintf(`{"value":"%s"}`, hexString), result) assert.Equal(t, "{}", json.RawMessage(resp)) assert.Equal(t, expJSON, strings.Trim(string(resultJSONBytes), "\n")) // + Replace, ReplaceAll, TrimSpace assert.Equal(t, expectedYML, conf) ✅ assert.JSONEq(t, `{"foo": "bar"}`, body) assert.YAMLEq(t, expectedYML, conf) ``` **自动修复**:是。
**默认启用**:是。
**原因**:防止 bug,并使用更合适的 `testify` API 和更清晰的失败信息。 `encoded-compare` 会检测 JSON 风格的字符串常量(也可在 `fmt.Sprintf` 中使用)以及 JSON 风格/YAML 风格的命名 变量。如果变量被转换为 `json.RawMessage`,则会被无条件视为 JSON。 在修复时,`encoded-compare` 会移除对 `[]byte`、`string`、`json.RawMessage` 的不必要的转换,以及 `strings.Replace`、`strings.ReplaceAll`、`strings.Trim`、`strings.TrimSpace` 的调用,并在需要时 添加向 `string` 的转换。 ### equal-values ``` ❌ assert.EqualValues(t, 42, result.IntField) assert.NotEqualValues(t, 42, result.IntField) // And other variations with similar types (strings, numerics, structs, etc.)... ✅ assert.Equal(t, 42, result.IntField) assert.NotEqual(t, 42, result.IntField) ``` **自动修复**:是。
**默认启用**:是。
**原因**:使用更合适且更不易出错的 `testify` API。 此外: 1. 溢出与下溢问题[由 testify 自身覆盖](https://github.com/stretchr/testify/pull/1531)。 2. Nil 比较由 [nil-compare](#nil-compare) 覆盖。 ### error-is-as ``` ❌ assert.Error(t, err, errSentinel) // Typo, errSentinel hits `msgAndArgs`. assert.NoError(t, err, errSentinel) assert.IsType(t, err, errSentinel) assert.IsType(t, (*http.MaxBytesError)(nil), err) assert.IsNotType(t, err, errSentinel) assert.IsNotType(t, store.NotFoundError{}, err) assert.True(t, errors.Is(err, errSentinel)) assert.False(t, errors.Is(err, errSentinel)) assert.True(t, errors.As(err, &target)) assert.False(t, errors.As(err, &target)) ✅ assert.ErrorIs(t, err, errSentinel) assert.NotErrorIs(t, err, errSentinel) assert.ErrorAs(t, err, &target) assert.NotErrorAs(t, err, &target) ``` **自动修复**:部分。
**默认启用**:是。
**原因**:在前两种情况下,这是一个常见错误,会导致隐藏了对 sentinel errors 不正确的包装。 在其他情况下——更合适的 `testify` API 以及更清晰的失败信息。 此外,`error-is-as` 重复了 `go vet` 的 [errorsas 检查](https://cs.opensource.google/go/x/tools/+/master:go/analysis/passes/errorsas/errorsas.go) 逻辑,但不支持自动修复。 `assert.ErrorAs` 备忘单: ``` assert.ErrorAs(t, err, new(*http.MaxBytesError)) assert.ErrorAs(t, err, store.NotFoundError{}) mbErr := new(http.MaxBytesError) require.ErrorAs(t, err, &mbErr) assert.Equal(t, 100, mbErr.Limit) if mbErr := new(http.MaxBytesError); assert.ErrorAs(t, err, &mbErr) { assert.Equal(t, 100, mbErr.Limit) } ``` ### error-nil ``` ❌ assert.Nil(t, err) assert.Empty(t, err) assert.Zero(t, err) assert.Equal(t, nil, err) assert.EqualValues(t, nil, err) assert.Exactly(t, nil, err) assert.ErrorIs(t, err, nil) assert.IsType(t, err, nil) assert.NotNil(t, err) assert.NotEmpty(t, err) assert.NotZero(t, err) assert.NotEqual(t, nil, err) assert.NotEqualValues(t, nil, err) assert.NotErrorIs(t, err, nil) assert.IsNotType(t, err, nil) ✅ assert.NoError(t, err) assert.Error(t, err) ``` **自动修复**:是。
**默认启用**:是。
**原因**:更合适的 `testify` API 以及更清晰的失败信息。 ### expected-actual ``` ❌ assert.Equal(t, result, expected) assert.Equal(t, result, len(expected)) assert.Equal(t, len(resultFields), len(expectedFields)) assert.EqualExportedValues(t, resultObj, User{Name: "Rob"}) assert.EqualValues(t, result, 42) assert.Exactly(t, result, int64(42)) assert.JSONEq(t, result, `{"version": 3}`) assert.InDelta(t, result, 42.42, 1.0) assert.InDeltaMapValues(t, result, map[string]float64{"score": 0.99}, 1.0) assert.InDeltaSlice(t, result, []float64{0.98, 0.99}, 1.0) assert.InEpsilon(t, result, 42.42, 0.0001) assert.InEpsilonSlice(t, result, []float64{0.9801, 0.9902}, 0.0001) assert.IsType(t, result, (*User)(nil)) assert.NotEqual(t, result, "expected") assert.NotEqualValues(t, result, "expected") assert.NotSame(t, resultPtr, &value) assert.Same(t, resultPtr, &value) assert.WithinDuration(t, resultTime, time.Date(2023, 01, 12, 11, 46, 33, 0, nil), time.Second) assert.YAMLEq(t, result, "version: '3'") ✅ assert.Equal(t, expected, result) assert.Equal(t, len(expected), result) assert.Equal(t, len(expectedFields), len(resultFields)) assert.EqualExportedValues(t, User{Name: "Rob"}, resultObj) assert.EqualValues(t, 42, result) // And so on... ``` **自动修复**:是。
**默认启用**:是。
**原因**:一个常见的错误,使得理解测试失败的原因变得更加困难。 检查器认为,期望值是一个基本字面量、常量,或者名称与模式匹配的变量 (`--expected-actual.pattern` 标志)。 计划在 `testify` 的 `v2` 版本中将[断言参数的顺序](https://github.com/stretchr/testify/issues/1089#Argument_order)更改为 更自然的(实际值,期望值)顺序。 ### float-compare ``` ❌ assert.Equal(t, 42.42, result) assert.EqualValues(t, 42.42, result) assert.Exactly(t, 42.42, result) assert.True(t, result == 42.42) assert.False(t, result != 42.42) ✅ assert.InEpsilon(t, 42.42, result, 0.0001) // Or assert.InDelta ``` **自动修复**:否。
**默认启用**:是。
**原因**:不要忘记[浮点数舍入问题](https://floating-point-gui.de/errors/comparison/)。 此检查器类似于 [floatcompare](https://github.com/golangci/golangci-lint/pull/2608) linter。 ### formatter ``` ❌ assert.ElementsMatch(t, certConfig.Org, csr.Subject.Org, "organizations not equal") assert.Error(t, err, fmt.Sprintf("Profile %s should not be valid", test.profile)) assert.Errorf(t, err, fmt.Sprintf("test %s", test.testName)) assert.Truef(t, targetTs.Equal(ts), "the timestamp should be as expected (%s) but was %s", targetTs) // And other go vet's printf checks... ✅ assert.ElementsMatchf(t, certConfig.Org, csr.Subject.Org, "organizations not equal") assert.Errorf(t, err, "Profile %s should not be valid", test.profile) assert.Errorf(t, err, "test %s", test.testName) assert.Truef(t, targetTs.Equal(ts), "the timestamp should be as expected (%s) but was %s", targetTs, ts) ``` **自动修复**:部分。
**默认启用**:是。
**原因**:代码简化、防止 bug、遵循 "Go 方式"。 `formatter` 检查器具有以下特性: #### 1) 检测断言中不必要的 `fmt.Sprintf`。有点类似于 [staticcheck 的 S1028 检查](https://staticcheck.dev/docs/checks/#S1028)。 #### 2) 使用 `go vet` 的 [printf](https://cs.opensource.google/go/x/tools/+/master:go/analysis/passes/printf/printf.go) 分析器的修补版分叉,验证断言格式字符串与相应参数的一致性。要 禁用此功能,请使用 `--formatter.check-format-string=false` 标志。 #### 3) 如果使用了格式字符串,则要求使用 f-assertions(例如 assert.Equal**f**)。默认禁用,使用 `--formatter.require-f-funcs` 标志来启用。
这有助于遵循 Go 的隐式约定 _"类 Printf 的函数必须以 `f` 结尾"_,并为迁移到 `testify` 的 `v2` 版本铺平道路。 通过这种方式,该检查器类似于 [goprintffuncname](https://github.com/jirfag/go-printf-func-name) linter(已包含在 [golangci-lint](https://golangci-lint.run/usage/linters/) 中)。
此外,f-assertions 格式字符串中的动词会被 IDE 高亮显示,例如 GoLand:
#### formatter 的历史参考
那些刚接触 `testify` 的人可能会对这种重复的 API 感到困惑: ``` func Equal(t TestingT, expected, actual any, msgAndArgs ...any) bool func Equalf(t TestingT, expected, actual any, msg string, args ...any) bool ``` f-functions(Equal**f**、Error**f** 等)是很久以前(2017 年)引入的,目的是为了解决 [uber-go/zap 的问题](https://github.com/stretchr/testify/issues/339):`go1.7 vet` 会对此 logger 的 [测试](https://github.com/uber-go/zap/blame/8f5ee80ab2dbc713823341ce30334cd9c03a98e5/flag_test.go#L60) 报错: ``` if tc.wantErr { // flag_test.go:61: possible formatting directive in Error call assert.Error(t, err, "Parse(%v) should fail.", tc.args) return } ``` 但是!那是 `go vet` 的 **printf** 分析器内部的逻辑错误,而不是 `testify` 的问题。 事实是,在 Go 1.7 中,**printf** 只是 [检查了函数的名称](https://github.com/golang/go/blob/2b7a7b710f096b1b7e6f2ab5e9e3ec003ad7cd12/src/cmd/vet/print.go#L69), 并没有考虑它所属的包,从而对一切可能的情况做出反应: ``` // isPrint records the unformatted-print functions. var isPrint = map[string]bool{ "error": true, "fatal": true, // ... } ``` 在相关的[问题](https://github.com/golang/go/issues/22936)提出后,此行为在 Go 1.10 中得到了 [修复](https://github.com/golang/go/blob/ad7c32dc3b6d5edc3dd72b3e15c80dc4f4c27064/src/cmd/vet/print.go#L62): ``` // isPrint records the print functions. var isPrint = map[string]bool{ "fmt.Errorf": true, "fmt.Fprint": true, // ... } ``` 现在 **printf** 只检查 Golang 标准库函数(除非另有配置),并且对 `testify` 的断言签名不再有异议。 尽管如此,f-functions 已经发布,从而产生了含糊不清的 API。 但维护者们肯定别无选择,只能根据 Go 的约定更改签名,因为这会 破坏向后兼容性: ``` func Equal(t TestingT, expected, actual any) bool func Equalf(t TestingT, expected, actual any, msg string, args ...any) bool ``` 但我希望它能在 `testify` 的 `v2` 中被 [引入](https://github.com/stretchr/testify/issues/1089#issuecomment-1695059265)。
#### 4)
验证 `msgAndArgs` 的第一个参数是 `string` 类型(基于 `testify`
维护者的[反馈]( ))且此字符串
不为空:
```
❌
assert.Equal(t, 1, strings.Count(b.String(), "hello"), tc)
assert.Equalf(t, want, got, "")
✅
assert.Equal(t, 1, strings.Count(b.String(), "hello"), "%+v", tc)
assert.Equal(t, want, got)
```
你可以使用 `--formatter.require-string-msg=false` 标志禁用此行为。
#### 5)
验证如果 message 不是字符串,`msgAndArgs` 中不存在任何参数:
```
❌
assert.True(t, cco.IsCardNumber(valid), i, valid) // Causes panic.
```
详情请见 [testify's issue](https://github.com/stretchr/testify/issues/1679)。
#### 6)
最后,它检查 `Fail` 和 `FailNow` 中的失败消息不会用作格式字符串(这样是行不通的):
```
❌
assert.Fail(t, "test case [%d] failed. %+v != %+v", idx, tc.expected, actual) // Causes panic.
✅
assert.Fail(t, "good luck!", "test case [%d] failed. %+v != %+v", idx, tc.expected, actual)
```
### go-require
```
go func() {
conn, err = lis.Accept()
require.NoError(t, err) ❌
if assert.Error(err) { ✅
assert.FailNow(t, msg) ❌
}
}()
```
**自动修复**:否。
**默认启用**:是。
**原因**:不正确的函数使用。 此检查器是 `go vet` 的 [testinggoroutine](https://cs.opensource.google/go/x/tools/+/master:go/analysis/passes/testinggoroutine/doc.go) 检查的深度改进版。 检查的核心在于,根据[文档](https://pkg.go.dev/testing#T), 导致 `t.FailNow`(本质上会导致 `runtime.GoExit`)的函数只能用于运行测试的 goroutine 中。 否则,它们将不会按照声明的那样工作,即结束测试函数。 你可以禁用 `go-require` 检查器并继续将 `require` 用作当前 goroutine 的终结器,但这可能 会导致 1. 测试中潜在的 resource leak; 2. 增加混乱,因为函数将不会被按预期使用。 通常,goroutines 内部的任何断言都是测试架构糟糕的标志。 尽量在主 goroutine 中执行它们,并将为此所需的数据分发到主 goroutine 中 ([示例](https://github.com/ipfs/kubo/issues/2043#issuecomment-164136026))。 简单地将 goroutines 中的所有 `require` 替换为 `assert` 也是个糟糕的解决方案 (比如 [这里](https://github.com/gravitational/teleport/pull/22567/files#diff-9f5fd20913c5fe80c85263153fa9a0b28dbd1407e53da4ab5d09e13d2774c5dbR7377)) —— 这只会掩盖问题。 该检查器默认启用,因为 `testinggoroutine` 在 `go vet` 中也是默认启用的。 此外,该检查器会对 HTTP handler(签名与 [http.HandlerFunc](https://pkg.go.dev/net/http#HandlerFunc) 匹配的函数和方法)中的 `require` 发出警告,因为 handler 运行在单独的 为 HTTP 连接提供服务的 [service goroutine](https://cs.opensource.google/go/go/+/refs/tags/go1.22.3:src/net/http/server.go;l=2782;drc=1d45a7ef560a76318ed59dfdb178cecd58caf948) 中。终止这些 goroutines 可能会导致未定义的行为并增加测试调试的难度。 你可以使用 `--go-require.ignore-http-handlers` 标志关闭此检查。 附注:请看与 goroutines 中的断言相关的 [testify's issue](https://github.com/stretchr/testify/issues/772)。 ### len ``` ❌ assert.Equal(t, 42, len(arr)) assert.Equal(t, len(arr), 42) assert.EqualValues(t, 42, len(arr)) assert.EqualValues(t, len(arr), 42) assert.Exactly(t, 42, len(arr)) assert.Exactly(t, len(arr), 42) assert.True(t, 42 == len(arr)) assert.True(t, len(arr) == 42) assert.Equal(t, value, len(arr)) assert.EqualValues(t, value, len(arr)) assert.Exactly(t, value, len(arr)) assert.True(t, len(arr) == value) assert.Equal(t, len(expArr), len(arr)) assert.EqualValues(t, len(expArr), len(arr)) assert.Exactly(t, len(expArr), len(arr)) assert.True(t, len(arr) == len(expArr)) ✅ assert.Len(t, arr, 42) assert.Len(t, arr, value) assert.Len(t, arr, len(expArr)) ``` **自动修复**:是。
**默认启用**:是。
**原因**:更合适的 `testify` API 以及更清晰的失败信息。 ### negative-positive ``` ❌ assert.Less(t, a, 0) assert.Greater(t, 0, a) assert.True(t, a < 0) assert.True(t, 0 > a) assert.False(t, a >= 0) assert.False(t, 0 <= a) assert.Greater(t, a, 0) assert.Less(t, 0, a) assert.True(t, a > 0) assert.True(t, 0 < a) assert.False(t, a <= 0) assert.False(t, 0 >= a) ✅ assert.Negative(t, a) assert.Positive(t, a) ``` **自动修复**:是。
**默认启用**:是
**原因**:更合适的 `testify` API 以及更清晰的失败信息。 同时也支持类型化的零值(如 `int8(0)`, ..., `uint64(0)`)。 ### nil-compare ``` ❌ assert.Equal(t, nil, value) assert.EqualValues(t, nil, value) assert.Exactly(t, nil, value) assert.NotEqual(t, nil, value) assert.NotEqualValues(t, nil, value) ✅ assert.Nil(t, value) assert.NotNil(t, value) ``` **自动修复**:是。
**默认启用**:是。
**原因**:防止 bug,并使用更合适的 `testify` API 和更清晰的失败信息。 将无类型的 `nil` 与非接口类型一起用于上述函数是毫无意义的: ``` assert.Equal(t, nil, eventsChan) // Always fail. assert.NotEqual(t, nil, eventsChan) // Always pass. ``` 正确的方式: ``` assert.Equal(t, (chan Event)(nil), eventsChan) assert.NotEqual(t, (chan Event)(nil), eventsChan) ``` 但在使用 `Equal`、`NotEqual` 和 `Exactly` 时,类型转换的方法对于函数类型仍然无效。 这里的最佳选择就是直接使用 `Nil` / `NotNil`(见[详情](https://github.com/stretchr/testify/issues/1524))。 ### regexp ``` ❌ assert.Regexp(t, regexp.MustCompile(`\[.*\] DEBUG \(.*TestNew.*\): message`), out) assert.NotRegexp(t, regexp.MustCompile(`\[.*\] TRACE message`), out) ✅ assert.Regexp(t, `\[.*\] DEBUG \(.*TestNew.*\): message`, out) assert.NotRegexp(t, `\[.*\] TRACE message`, out) ``` **自动修复**:是。
**默认启用**:是。
**原因**:简化代码。 ### require-error ``` ❌ assert.Error(t, err) // s.Error(err), s.Assert().Error(err) assert.ErrorIs(t, err, io.EOF) assert.ErrorAs(t, err, &target) assert.EqualError(t, err, "end of file") assert.ErrorContains(t, err, "end of file") assert.NoError(t, err) assert.NotErrorIs(t, err, io.EOF) ✅ require.Error(t, err) // s.Require().Error(err), s.Require().Error(err) require.ErrorIs(t, err, io.EOF) require.ErrorAs(t, err, &target) // And so on... ``` **自动修复**:否。
**默认启用**:是。
**原因**:这种对错误的“忽略”会导致进一步的 panic,使测试更难调试。 [testify/require](https://pkg.go.dev/github.com/stretchr/testify@master/require#hdr-Assertions) 允许 在测试失败时停止测试执行。 默认情况下,`require-error` 仅检查上面列出的 `*Error*` 断言。
你可以设置 `--require-error.fn-pattern` 标志来将检查限制为特定的调用(但仍限于上面的列表中)。 例如,`--require-error.fn-pattern="^(Errorf?|NoErrorf?)$"` 将仅检查 `Error`、`Errorf`、`NoError` 和 `NoErrorf`。 此外,为了尽量减少误报,`require-error` 会忽略: - `if` 条件中的断言; - bool 表达式中的断言; - 整个 `if-else[-if]` 块,如果在任何 `if` 条件中存在断言; - 代码块中的最后一个断言(如果在此断言之后没有方法/函数调用); - 显式 goroutine 中的断言(包括 `http.Handler`); - 显式 testing cleanup 函数或 suite teardown 方法中的断言; - `NoError` 断言序列。 ### suite-broken-parallel ``` func (s *MySuite) SetupTest() { s.T().Parallel() ❌ } // And other hooks... func (s *MySuite) TestSomething() { s.T().Parallel() ❌ for _, tt := range cases { s.Run(tt.name, func() { s.T().Parallel() ❌ }) s.T().Run(tt.name, func(t *testing.T) { t.Parallel() ❌ }) } } ``` **自动修复**:是。
**默认启用**:是。
**原因**:防止未定义行为。 `testify` 的 `v1` 版本不支持 suite 的并行测试和子测试。 根据[具体情况](./analyzer/testdata/src/debug/suite_broken_parallel_test.go),使用 `t.Parallel()` 会导致 - data race - panic - suite 钩子失效 - 静默忽略此指令 因此,`testify` 的维护者建议不要在 suite 内部使用并行测试。
### suite-dont-use-pkg
```
func (s *MySuite) TestSomething() {
❌ assert.Equal(s.T(), 42, value)
✅ s.Equal(42, value)
}
```
**自动修复**:是。
**默认启用**:是。
**原因**:更简单、更统一的代码。 ### suite-extra-assert-call 默认情况下,检查器要求你移除不必要的 `Assert()` 调用: ``` func (s *MySuite) TestSomething() { ❌ s.Assert().Equal(42, value) ✅ s.Equal(42, value) } ``` 但有时,人们反而希望与 `s.Assert()` 和 `s.Require()` 保持一致: ``` func (s *MySuite) TestSomething() { // ... ❌ s.Require().NoError(err) s.Equal(42, value) ✅ s.Require().NoError(err) s.Assert().Equal(42, value) } ``` 你可以通过 `--suite-extra-assert-call.mode=require` 启用此行为。 **自动修复**:是。
**默认启用**:是,处于 `remove` 模式下。
**原因**:更简单或更统一的代码。 ### suite-method-signature ``` ❌ func (s *MySuite) SetupTest(i int) { /* ... */ } func (s *MySuite) TestAlice(t *testing.T) { s.SetupTest(rand()) /* ... */ } func (s *MySuite) TestBob() { s.SetupTest(rand()) /* ... */ } ✅ func (s *MySuite) SetupTest() { s.setupTest(rand()) } // Use inversion-of-control's methods from suite interfaces. func (s *MySuite) TestAlice() { /* ... */ } // Keep test methods clean from args and returning values. func (s *MySuite) TestBob() { /* ... */ } func (s *MySuite) setupTest(i int) { /* ... */ } ``` **自动修复**:否。
**默认启用**:是。
**原因**:防止 panic 和 bug。 所有的 suite 功能方法都列在[这里](https://github.com/stretchr/testify/blob/master/suite/interfaces.go)。 ### suite-subtest-run ``` func (s *MySuite) TestSomething() { ❌ s.T().Run("subtest", func(t *testing.T) { assert.Equal(t, 42, result) }) ✅ s.Run("subtest", func() { s.Equal(42, result) }) } ``` **自动修复**:否。
**默认启用**:是。
**原因**:防止未定义行为。 根据 `testify` [文档](https://pkg.go.dev/github.com/stretchr/testify/suite#Suite.Run),`s.Run` 应该 用于运行子测试。此调用(除其他外)会使用新的 `t` 实例初始化 suite, 并保护测试免受未定义行为(如 data races)的影响。 自动修复被禁用,因为在大多数情况下,它需要重写子测试中的断言,并可能导致死代码。 该检查器与 [suite-dont-use-pkg](#suite-dont-use-pkg) 结合使用时特别有用。 ### suite-thelper ``` ❌ func (s *RoomSuite) assertRoomRound(roundID RoundID) { s.Equal(roundID, s.getRoom().CurrentRound.ID) } ✅ func (s *RoomSuite) assertRoomRound(roundID RoundID) { s.T().Helper() s.Equal(roundID, s.getRoom().CurrentRound.ID) } ``` **自动修复**:是。
**默认启用**:否。
**原因**:与非 suite 测试 helper 保持一致。明确标记 helper 方法。 实际上 `s.T().Helper()` 调用并不重要,因为 `testify` 无论如何 [都会](https://github.com/stretchr/testify/blob/882382d845cd9780bd93c1acc8e1fa2ffe266ca1/assert/assertions.go#L317)打印完整的 `Error Trace`。 该检查器更像是 [checkers.AdvancedChecker](https://github.com/Antonboom/testifylint/blob/676324836555445fded4e9afc004101ec6f597fe/internal/checkers/checker.go#L56) 的一个示例。 ### useless-assert 该检查器防止对同一个变量进行断言: ``` assert.Contains(t, tt.value, tt.value) assert.ElementsMatch(t, tt.value, tt.value) assert.Equal(t, tt.value, tt.value) assert.EqualExportedValues(t, tt.value, tt.value) // And other assert functions... assert.True(t, num > num) assert.True(t, num < num) assert.True(t, num >= num) assert.True(t, num <= num) assert.True(t, num == num) assert.True(t, num != num) assert.False(t, num > num) assert.False(t, num < num) assert.False(t, num >= num) assert.False(t, num <= num) assert.False(t, num == num) assert.False(t, num != num) ``` 并防止这些毫无意义的断言: ``` assert.Empty(t, "value") // Any string literal. assert.Error(t, nil) assert.False(t, false) // Any bool literal. assert.Implements(t, (*any)(nil), new(Conn)) assert.Negative(t, 42) // Any int literal. assert.Nil(t, nil) assert.NoError(t, nil) assert.NotEmpty(t, "value") // Any string literal. assert.NotImplements(t, (*any)(nil), new(Conn)) assert.NotNil(t, nil) assert.NotZero(t, 42) // Any int literal. assert.NotZero(t, "value") // Any string literal. assert.NotZero(t, nil) assert.NotZero(t, false) // Any bool literal. assert.Positive(t, 42) // Any int literal. assert.True(t, true) // Any bool literal. assert.Zero(t, 42) // Any int literal. assert.Zero(t, "value") // Any string literal. assert.Zero(t, nil) assert.Zero(t, false) // Any bool literal. assert.Negative(len(x)) assert.Less(len(x), 0) assert.Greater(0, len(x)) assert.GreaterOrEqual(len(x), 0) assert.LessOrEqual(0, len(x)) assert.Negative(uintVal) assert.Less(uintVal, 0) assert.Greater(0, uintVal) assert.GreaterOrEqual(uintVal, 0) assert.LessOrEqual(0, uintVal) ``` **自动修复**:否。
**默认启用**:是。
**原因**:防止 bug 和死代码。 ## 警告链 Linter 不会自动处理更改的“演变”。 在极少数情况下,它可能会对你的代码提出多次警告,例如: ``` assert.True(err == nil) // compares: use assert.Equal assert.Equal(t, err, nil) // error-nil: use assert.NoError assert.NoError(t, err) // require-error: for error assertions use require require.NoError(t, err) ``` 如果你有关于如何改进这一点的想法,请[贡献力量](./CONTRIBUTING.md)。 ## testify v2 `testify` 的第二个版本[承诺](https://github.com/stretchr/testify/issues/1089)提供更“令人愉悦”的 API,并 使得上述的一些检查器变得不再必要。 在这种情况下,不排除在 linter 中支持 `v2` 的可能性。 但目前看来,我们离 `v2` 还有[极远的距离](https://github.com/stretchr/testify/issues/1089#issuecomment-1812734472)。 相关的里程碑在[这里](https://github.com/stretchr/testify/milestone/4)。
**默认启用**:是。
**原因**:`testify` 没有任何 `init()` 魔法,因此这些作为 `_` 的 import 毫无用处,被视为无用代码。 ### bool-compare ``` ❌ assert.Equal(t, false, result) assert.EqualValues(t, false, result) assert.Exactly(t, false, result) assert.NotEqual(t, true, result) assert.NotEqualValues(t, true, result) assert.False(t, !result) assert.True(t, result == true) // And other variations... ✅ assert.True(t, result) assert.False(t, result) ``` **自动修复**:是。
**默认启用**:是。
**原因**:简化代码。 此外,`bool-compare` 还支持用户自定义类型,例如 ``` type Bool bool ``` 并通过将变量转换为内置 `bool` 来修复断言: ``` var predicate Bool ❌ assert.Equal(t, false, predicate) ✅ assert.False(t, bool(predicate)) ``` 使用 `--bool-compare.ignore-custom-types` 标志可关闭此行为。 ### compares ``` ❌ assert.True(t, a == b) assert.True(t, a != b) assert.True(t, a > b) assert.True(t, a >= b) assert.True(t, a < b) assert.True(t, a <= b) assert.False(t, a == b) // And so on... ✅ assert.Equal(t, a, b) assert.NotEqual(t, a, b) assert.Greater(t, a, b) assert.GreaterOrEqual(t, a, b) assert.Less(t, a, b) assert.LessOrEqual(t, a, b) ``` **自动修复**:是。
**默认启用**:是。
**原因**:更合适的 `testify` API 以及更清晰的失败信息。 如果 `a` 和 `b` 是指针,则必须改用 `assert.Same`/`NotSame`, 因为 `assert.Equal` 具有不合适的递归特性(基于 [reflect.DeepEqual](https://pkg.go.dev/reflect#DeepEqual))。 ### contains ``` ❌ assert.True(t, strings.Contains(a, "abc123")) assert.False(t, !strings.Contains(a, "abc123")) assert.False(t, strings.Contains(a, "abc123")) assert.True(t, !strings.Contains(a, "abc123")) assert.Contains(t, arr, 1, 2) assert.NotContains(t, arr, 1, 2) ✅ assert.Contains(t, a, "abc123") assert.NotContains(t, a, "abc123") assert.Subset(t, arr, 1, 2) assert.NotSubset(t, arr, 1, 2) ``` **自动修复**:部分。
**默认启用**:是。
**原因**:防止 bug、简化代码,以及使用更合适的 `testify` API 和更清晰的失败信息。 #### Kubernetes 测试中的 bug 示例
点击展开...
``` ❌ assert.Contains(t, cadvisorInfoToUserDefinedMetrics(&cInfo), statsapi.UserDefinedMetric{ UserDefinedMetricDescriptor: statsapi.UserDefinedMetricDescriptor{ Name: "qos", Type: statsapi.MetricGauge, Units: "per second", }, Time: metav1.NewTime(timestamp2), Value: 100, }, statsapi.UserDefinedMetric{ UserDefinedMetricDescriptor: statsapi.UserDefinedMetricDescriptor{ Name: "cpuLoad", Type: statsapi.MetricCumulative, Units: "count", }, Time: metav1.NewTime(timestamp2), Value: 2.1, }) ✅ assert.Subset(t, cadvisorInfoToUserDefinedMetrics(&cInfo), []statsapi.UserDefinedMetric{ { UserDefinedMetricDescriptor: statsapi.UserDefinedMetricDescriptor{ Name: "qos", Type: statsapi.MetricGauge, Units: "per second", }, Time: metav1.NewTime(timestamp2), Value: 100, }, { UserDefinedMetricDescriptor: statsapi.UserDefinedMetricDescriptor{ Name: "cpuLoad", Type: statsapi.MetricCumulative, Units: "count", }, Time: metav1.NewTime(timestamp2), Value: 2.1, }}) ```点击展开...
``` ❌ logLines := getLogLines(t, ql1File) /* { "time": "2025-09-14T07:49:51.652292+03:00", "params": { "query": "test statement" }, "error": "failure", "spanID": "0000000000000000", "foo": "bar" } */ require.Contains(t, logLines[0], "error", errors.New("failure")) require.Contains(t, logLines[0], "params", map[string]interface{}{"query": "test statement"}) ✅ logLines := getLogLines(t, ql1File) require.Contains(t, logLines[0], `"error":"failure"`) require.Contains(t, logLines[0], `"params":{"query":"test statement"}`) ```**默认启用**:是。
**原因**:更合适的 `testify` API 以及更清晰的失败信息。 此外,`empty` 会在 `*Emtpy` 断言中移除多余的 `len` 调用: ``` ❌ assert.Empty(t, len(arr)) assert.NotEmpty(t, len(arr)) ✅ assert.Empty(t, arr) assert.NotEmpty(t, arr) ``` 附注:`empty` 不会移除字符串转换并保持原样: - `string(bytes)` – 保持断言失败信息的可读性; - `string(str)` – 倾向于使用 [unconvert](https://golangci-lint.run/usage/linters/#unconvert)。 ### encoded-compare ``` ❌ assert.Equal(t, `{"foo": "bar"}`, body) assert.EqualValues(t, `{"foo": "bar"}`, body) assert.Exactly(t, `{"foo": "bar"}`, body) assert.Equal(t, expectedJSON, resultJSON) assert.Equal(t, expBodyConst, w.Body.String()) assert.Equal(t, fmt.Sprintf(`{"value":"%s"}`, hexString), result) assert.Equal(t, "{}", json.RawMessage(resp)) assert.Equal(t, expJSON, strings.Trim(string(resultJSONBytes), "\n")) // + Replace, ReplaceAll, TrimSpace assert.Equal(t, expectedYML, conf) ✅ assert.JSONEq(t, `{"foo": "bar"}`, body) assert.YAMLEq(t, expectedYML, conf) ``` **自动修复**:是。
**默认启用**:是。
**原因**:防止 bug,并使用更合适的 `testify` API 和更清晰的失败信息。 `encoded-compare` 会检测 JSON 风格的字符串常量(也可在 `fmt.Sprintf` 中使用)以及 JSON 风格/YAML 风格的命名 变量。如果变量被转换为 `json.RawMessage`,则会被无条件视为 JSON。 在修复时,`encoded-compare` 会移除对 `[]byte`、`string`、`json.RawMessage` 的不必要的转换,以及 `strings.Replace`、`strings.ReplaceAll`、`strings.Trim`、`strings.TrimSpace` 的调用,并在需要时 添加向 `string` 的转换。 ### equal-values ``` ❌ assert.EqualValues(t, 42, result.IntField) assert.NotEqualValues(t, 42, result.IntField) // And other variations with similar types (strings, numerics, structs, etc.)... ✅ assert.Equal(t, 42, result.IntField) assert.NotEqual(t, 42, result.IntField) ``` **自动修复**:是。
**默认启用**:是。
**原因**:使用更合适且更不易出错的 `testify` API。 此外: 1. 溢出与下溢问题[由 testify 自身覆盖](https://github.com/stretchr/testify/pull/1531)。 2. Nil 比较由 [nil-compare](#nil-compare) 覆盖。 ### error-is-as ``` ❌ assert.Error(t, err, errSentinel) // Typo, errSentinel hits `msgAndArgs`. assert.NoError(t, err, errSentinel) assert.IsType(t, err, errSentinel) assert.IsType(t, (*http.MaxBytesError)(nil), err) assert.IsNotType(t, err, errSentinel) assert.IsNotType(t, store.NotFoundError{}, err) assert.True(t, errors.Is(err, errSentinel)) assert.False(t, errors.Is(err, errSentinel)) assert.True(t, errors.As(err, &target)) assert.False(t, errors.As(err, &target)) ✅ assert.ErrorIs(t, err, errSentinel) assert.NotErrorIs(t, err, errSentinel) assert.ErrorAs(t, err, &target) assert.NotErrorAs(t, err, &target) ``` **自动修复**:部分。
**默认启用**:是。
**原因**:在前两种情况下,这是一个常见错误,会导致隐藏了对 sentinel errors 不正确的包装。 在其他情况下——更合适的 `testify` API 以及更清晰的失败信息。 此外,`error-is-as` 重复了 `go vet` 的 [errorsas 检查](https://cs.opensource.google/go/x/tools/+/master:go/analysis/passes/errorsas/errorsas.go) 逻辑,但不支持自动修复。 `assert.ErrorAs` 备忘单: ``` assert.ErrorAs(t, err, new(*http.MaxBytesError)) assert.ErrorAs(t, err, store.NotFoundError{}) mbErr := new(http.MaxBytesError) require.ErrorAs(t, err, &mbErr) assert.Equal(t, 100, mbErr.Limit) if mbErr := new(http.MaxBytesError); assert.ErrorAs(t, err, &mbErr) { assert.Equal(t, 100, mbErr.Limit) } ``` ### error-nil ``` ❌ assert.Nil(t, err) assert.Empty(t, err) assert.Zero(t, err) assert.Equal(t, nil, err) assert.EqualValues(t, nil, err) assert.Exactly(t, nil, err) assert.ErrorIs(t, err, nil) assert.IsType(t, err, nil) assert.NotNil(t, err) assert.NotEmpty(t, err) assert.NotZero(t, err) assert.NotEqual(t, nil, err) assert.NotEqualValues(t, nil, err) assert.NotErrorIs(t, err, nil) assert.IsNotType(t, err, nil) ✅ assert.NoError(t, err) assert.Error(t, err) ``` **自动修复**:是。
**默认启用**:是。
**原因**:更合适的 `testify` API 以及更清晰的失败信息。 ### expected-actual ``` ❌ assert.Equal(t, result, expected) assert.Equal(t, result, len(expected)) assert.Equal(t, len(resultFields), len(expectedFields)) assert.EqualExportedValues(t, resultObj, User{Name: "Rob"}) assert.EqualValues(t, result, 42) assert.Exactly(t, result, int64(42)) assert.JSONEq(t, result, `{"version": 3}`) assert.InDelta(t, result, 42.42, 1.0) assert.InDeltaMapValues(t, result, map[string]float64{"score": 0.99}, 1.0) assert.InDeltaSlice(t, result, []float64{0.98, 0.99}, 1.0) assert.InEpsilon(t, result, 42.42, 0.0001) assert.InEpsilonSlice(t, result, []float64{0.9801, 0.9902}, 0.0001) assert.IsType(t, result, (*User)(nil)) assert.NotEqual(t, result, "expected") assert.NotEqualValues(t, result, "expected") assert.NotSame(t, resultPtr, &value) assert.Same(t, resultPtr, &value) assert.WithinDuration(t, resultTime, time.Date(2023, 01, 12, 11, 46, 33, 0, nil), time.Second) assert.YAMLEq(t, result, "version: '3'") ✅ assert.Equal(t, expected, result) assert.Equal(t, len(expected), result) assert.Equal(t, len(expectedFields), len(resultFields)) assert.EqualExportedValues(t, User{Name: "Rob"}, resultObj) assert.EqualValues(t, 42, result) // And so on... ``` **自动修复**:是。
**默认启用**:是。
**原因**:一个常见的错误,使得理解测试失败的原因变得更加困难。 检查器认为,期望值是一个基本字面量、常量,或者名称与模式匹配的变量 (`--expected-actual.pattern` 标志)。 计划在 `testify` 的 `v2` 版本中将[断言参数的顺序](https://github.com/stretchr/testify/issues/1089#Argument_order)更改为 更自然的(实际值,期望值)顺序。 ### float-compare ``` ❌ assert.Equal(t, 42.42, result) assert.EqualValues(t, 42.42, result) assert.Exactly(t, 42.42, result) assert.True(t, result == 42.42) assert.False(t, result != 42.42) ✅ assert.InEpsilon(t, 42.42, result, 0.0001) // Or assert.InDelta ``` **自动修复**:否。
**默认启用**:是。
**原因**:不要忘记[浮点数舍入问题](https://floating-point-gui.de/errors/comparison/)。 此检查器类似于 [floatcompare](https://github.com/golangci/golangci-lint/pull/2608) linter。 ### formatter ``` ❌ assert.ElementsMatch(t, certConfig.Org, csr.Subject.Org, "organizations not equal") assert.Error(t, err, fmt.Sprintf("Profile %s should not be valid", test.profile)) assert.Errorf(t, err, fmt.Sprintf("test %s", test.testName)) assert.Truef(t, targetTs.Equal(ts), "the timestamp should be as expected (%s) but was %s", targetTs) // And other go vet's printf checks... ✅ assert.ElementsMatchf(t, certConfig.Org, csr.Subject.Org, "organizations not equal") assert.Errorf(t, err, "Profile %s should not be valid", test.profile) assert.Errorf(t, err, "test %s", test.testName) assert.Truef(t, targetTs.Equal(ts), "the timestamp should be as expected (%s) but was %s", targetTs, ts) ``` **自动修复**:部分。
**默认启用**:是。
**原因**:代码简化、防止 bug、遵循 "Go 方式"。 `formatter` 检查器具有以下特性: #### 1) 检测断言中不必要的 `fmt.Sprintf`。有点类似于 [staticcheck 的 S1028 检查](https://staticcheck.dev/docs/checks/#S1028)。 #### 2) 使用 `go vet` 的 [printf](https://cs.opensource.google/go/x/tools/+/master:go/analysis/passes/printf/printf.go) 分析器的修补版分叉,验证断言格式字符串与相应参数的一致性。要 禁用此功能,请使用 `--formatter.check-format-string=false` 标志。 #### 3) 如果使用了格式字符串,则要求使用 f-assertions(例如 assert.Equal**f**)。默认禁用,使用 `--formatter.require-f-funcs` 标志来启用。
这有助于遵循 Go 的隐式约定 _"类 Printf 的函数必须以 `f` 结尾"_,并为迁移到 `testify` 的 `v2` 版本铺平道路。 通过这种方式,该检查器类似于 [goprintffuncname](https://github.com/jirfag/go-printf-func-name) linter(已包含在 [golangci-lint](https://golangci-lint.run/usage/linters/) 中)。
此外,f-assertions 格式字符串中的动词会被 IDE 高亮显示,例如 GoLand:
#### formatter 的历史参考
点击展开...
那些刚接触 `testify` 的人可能会对这种重复的 API 感到困惑: ``` func Equal(t TestingT, expected, actual any, msgAndArgs ...any) bool func Equalf(t TestingT, expected, actual any, msg string, args ...any) bool ``` f-functions(Equal**f**、Error**f** 等)是很久以前(2017 年)引入的,目的是为了解决 [uber-go/zap 的问题](https://github.com/stretchr/testify/issues/339):`go1.7 vet` 会对此 logger 的 [测试](https://github.com/uber-go/zap/blame/8f5ee80ab2dbc713823341ce30334cd9c03a98e5/flag_test.go#L60) 报错: ``` if tc.wantErr { // flag_test.go:61: possible formatting directive in Error call assert.Error(t, err, "Parse(%v) should fail.", tc.args) return } ``` 但是!那是 `go vet` 的 **printf** 分析器内部的逻辑错误,而不是 `testify` 的问题。 事实是,在 Go 1.7 中,**printf** 只是 [检查了函数的名称](https://github.com/golang/go/blob/2b7a7b710f096b1b7e6f2ab5e9e3ec003ad7cd12/src/cmd/vet/print.go#L69), 并没有考虑它所属的包,从而对一切可能的情况做出反应: ``` // isPrint records the unformatted-print functions. var isPrint = map[string]bool{ "error": true, "fatal": true, // ... } ``` 在相关的[问题](https://github.com/golang/go/issues/22936)提出后,此行为在 Go 1.10 中得到了 [修复](https://github.com/golang/go/blob/ad7c32dc3b6d5edc3dd72b3e15c80dc4f4c27064/src/cmd/vet/print.go#L62): ``` // isPrint records the print functions. var isPrint = map[string]bool{ "fmt.Errorf": true, "fmt.Fprint": true, // ... } ``` 现在 **printf** 只检查 Golang 标准库函数(除非另有配置),并且对 `testify` 的断言签名不再有异议。 尽管如此,f-functions 已经发布,从而产生了含糊不清的 API。 但维护者们肯定别无选择,只能根据 Go 的约定更改签名,因为这会 破坏向后兼容性: ``` func Equal(t TestingT, expected, actual any) bool func Equalf(t TestingT, expected, actual any, msg string, args ...any) bool ``` 但我希望它能在 `testify` 的 `v2` 中被 [引入](https://github.com/stretchr/testify/issues/1089#issuecomment-1695059265)。
**默认启用**:是。
**原因**:不正确的函数使用。 此检查器是 `go vet` 的 [testinggoroutine](https://cs.opensource.google/go/x/tools/+/master:go/analysis/passes/testinggoroutine/doc.go) 检查的深度改进版。 检查的核心在于,根据[文档](https://pkg.go.dev/testing#T), 导致 `t.FailNow`(本质上会导致 `runtime.GoExit`)的函数只能用于运行测试的 goroutine 中。 否则,它们将不会按照声明的那样工作,即结束测试函数。 你可以禁用 `go-require` 检查器并继续将 `require` 用作当前 goroutine 的终结器,但这可能 会导致 1. 测试中潜在的 resource leak; 2. 增加混乱,因为函数将不会被按预期使用。 通常,goroutines 内部的任何断言都是测试架构糟糕的标志。 尽量在主 goroutine 中执行它们,并将为此所需的数据分发到主 goroutine 中 ([示例](https://github.com/ipfs/kubo/issues/2043#issuecomment-164136026))。 简单地将 goroutines 中的所有 `require` 替换为 `assert` 也是个糟糕的解决方案 (比如 [这里](https://github.com/gravitational/teleport/pull/22567/files#diff-9f5fd20913c5fe80c85263153fa9a0b28dbd1407e53da4ab5d09e13d2774c5dbR7377)) —— 这只会掩盖问题。 该检查器默认启用,因为 `testinggoroutine` 在 `go vet` 中也是默认启用的。 此外,该检查器会对 HTTP handler(签名与 [http.HandlerFunc](https://pkg.go.dev/net/http#HandlerFunc) 匹配的函数和方法)中的 `require` 发出警告,因为 handler 运行在单独的 为 HTTP 连接提供服务的 [service goroutine](https://cs.opensource.google/go/go/+/refs/tags/go1.22.3:src/net/http/server.go;l=2782;drc=1d45a7ef560a76318ed59dfdb178cecd58caf948) 中。终止这些 goroutines 可能会导致未定义的行为并增加测试调试的难度。 你可以使用 `--go-require.ignore-http-handlers` 标志关闭此检查。 附注:请看与 goroutines 中的断言相关的 [testify's issue](https://github.com/stretchr/testify/issues/772)。 ### len ``` ❌ assert.Equal(t, 42, len(arr)) assert.Equal(t, len(arr), 42) assert.EqualValues(t, 42, len(arr)) assert.EqualValues(t, len(arr), 42) assert.Exactly(t, 42, len(arr)) assert.Exactly(t, len(arr), 42) assert.True(t, 42 == len(arr)) assert.True(t, len(arr) == 42) assert.Equal(t, value, len(arr)) assert.EqualValues(t, value, len(arr)) assert.Exactly(t, value, len(arr)) assert.True(t, len(arr) == value) assert.Equal(t, len(expArr), len(arr)) assert.EqualValues(t, len(expArr), len(arr)) assert.Exactly(t, len(expArr), len(arr)) assert.True(t, len(arr) == len(expArr)) ✅ assert.Len(t, arr, 42) assert.Len(t, arr, value) assert.Len(t, arr, len(expArr)) ``` **自动修复**:是。
**默认启用**:是。
**原因**:更合适的 `testify` API 以及更清晰的失败信息。 ### negative-positive ``` ❌ assert.Less(t, a, 0) assert.Greater(t, 0, a) assert.True(t, a < 0) assert.True(t, 0 > a) assert.False(t, a >= 0) assert.False(t, 0 <= a) assert.Greater(t, a, 0) assert.Less(t, 0, a) assert.True(t, a > 0) assert.True(t, 0 < a) assert.False(t, a <= 0) assert.False(t, 0 >= a) ✅ assert.Negative(t, a) assert.Positive(t, a) ``` **自动修复**:是。
**默认启用**:是
**原因**:更合适的 `testify` API 以及更清晰的失败信息。 同时也支持类型化的零值(如 `int8(0)`, ..., `uint64(0)`)。 ### nil-compare ``` ❌ assert.Equal(t, nil, value) assert.EqualValues(t, nil, value) assert.Exactly(t, nil, value) assert.NotEqual(t, nil, value) assert.NotEqualValues(t, nil, value) ✅ assert.Nil(t, value) assert.NotNil(t, value) ``` **自动修复**:是。
**默认启用**:是。
**原因**:防止 bug,并使用更合适的 `testify` API 和更清晰的失败信息。 将无类型的 `nil` 与非接口类型一起用于上述函数是毫无意义的: ``` assert.Equal(t, nil, eventsChan) // Always fail. assert.NotEqual(t, nil, eventsChan) // Always pass. ``` 正确的方式: ``` assert.Equal(t, (chan Event)(nil), eventsChan) assert.NotEqual(t, (chan Event)(nil), eventsChan) ``` 但在使用 `Equal`、`NotEqual` 和 `Exactly` 时,类型转换的方法对于函数类型仍然无效。 这里的最佳选择就是直接使用 `Nil` / `NotNil`(见[详情](https://github.com/stretchr/testify/issues/1524))。 ### regexp ``` ❌ assert.Regexp(t, regexp.MustCompile(`\[.*\] DEBUG \(.*TestNew.*\): message`), out) assert.NotRegexp(t, regexp.MustCompile(`\[.*\] TRACE message`), out) ✅ assert.Regexp(t, `\[.*\] DEBUG \(.*TestNew.*\): message`, out) assert.NotRegexp(t, `\[.*\] TRACE message`, out) ``` **自动修复**:是。
**默认启用**:是。
**原因**:简化代码。 ### require-error ``` ❌ assert.Error(t, err) // s.Error(err), s.Assert().Error(err) assert.ErrorIs(t, err, io.EOF) assert.ErrorAs(t, err, &target) assert.EqualError(t, err, "end of file") assert.ErrorContains(t, err, "end of file") assert.NoError(t, err) assert.NotErrorIs(t, err, io.EOF) ✅ require.Error(t, err) // s.Require().Error(err), s.Require().Error(err) require.ErrorIs(t, err, io.EOF) require.ErrorAs(t, err, &target) // And so on... ``` **自动修复**:否。
**默认启用**:是。
**原因**:这种对错误的“忽略”会导致进一步的 panic,使测试更难调试。 [testify/require](https://pkg.go.dev/github.com/stretchr/testify@master/require#hdr-Assertions) 允许 在测试失败时停止测试执行。 默认情况下,`require-error` 仅检查上面列出的 `*Error*` 断言。
你可以设置 `--require-error.fn-pattern` 标志来将检查限制为特定的调用(但仍限于上面的列表中)。 例如,`--require-error.fn-pattern="^(Errorf?|NoErrorf?)$"` 将仅检查 `Error`、`Errorf`、`NoError` 和 `NoErrorf`。 此外,为了尽量减少误报,`require-error` 会忽略: - `if` 条件中的断言; - bool 表达式中的断言; - 整个 `if-else[-if]` 块,如果在任何 `if` 条件中存在断言; - 代码块中的最后一个断言(如果在此断言之后没有方法/函数调用); - 显式 goroutine 中的断言(包括 `http.Handler`); - 显式 testing cleanup 函数或 suite teardown 方法中的断言; - `NoError` 断言序列。 ### suite-broken-parallel ``` func (s *MySuite) SetupTest() { s.T().Parallel() ❌ } // And other hooks... func (s *MySuite) TestSomething() { s.T().Parallel() ❌ for _, tt := range cases { s.Run(tt.name, func() { s.T().Parallel() ❌ }) s.T().Run(tt.name, func(t *testing.T) { t.Parallel() ❌ }) } } ``` **自动修复**:是。
**默认启用**:是。
**原因**:防止未定义行为。 `testify` 的 `v1` 版本不支持 suite 的并行测试和子测试。 根据[具体情况](./analyzer/testdata/src/debug/suite_broken_parallel_test.go),使用 `t.Parallel()` 会导致 - data race - panic - suite 钩子失效 - 静默忽略此指令 因此,`testify` 的维护者建议不要在 suite 内部使用并行测试。
相关问题...
- https://github.com/stretchr/testify/issues/187 - https://github.com/stretchr/testify/issues/466 - https://github.com/stretchr/testify/issues/934 - https://github.com/stretchr/testify/issues/1139 - https://github.com/stretchr/testify/issues/1253**默认启用**:是。
**原因**:更简单、更统一的代码。 ### suite-extra-assert-call 默认情况下,检查器要求你移除不必要的 `Assert()` 调用: ``` func (s *MySuite) TestSomething() { ❌ s.Assert().Equal(42, value) ✅ s.Equal(42, value) } ``` 但有时,人们反而希望与 `s.Assert()` 和 `s.Require()` 保持一致: ``` func (s *MySuite) TestSomething() { // ... ❌ s.Require().NoError(err) s.Equal(42, value) ✅ s.Require().NoError(err) s.Assert().Equal(42, value) } ``` 你可以通过 `--suite-extra-assert-call.mode=require` 启用此行为。 **自动修复**:是。
**默认启用**:是,处于 `remove` 模式下。
**原因**:更简单或更统一的代码。 ### suite-method-signature ``` ❌ func (s *MySuite) SetupTest(i int) { /* ... */ } func (s *MySuite) TestAlice(t *testing.T) { s.SetupTest(rand()) /* ... */ } func (s *MySuite) TestBob() { s.SetupTest(rand()) /* ... */ } ✅ func (s *MySuite) SetupTest() { s.setupTest(rand()) } // Use inversion-of-control's methods from suite interfaces. func (s *MySuite) TestAlice() { /* ... */ } // Keep test methods clean from args and returning values. func (s *MySuite) TestBob() { /* ... */ } func (s *MySuite) setupTest(i int) { /* ... */ } ``` **自动修复**:否。
**默认启用**:是。
**原因**:防止 panic 和 bug。 所有的 suite 功能方法都列在[这里](https://github.com/stretchr/testify/blob/master/suite/interfaces.go)。 ### suite-subtest-run ``` func (s *MySuite) TestSomething() { ❌ s.T().Run("subtest", func(t *testing.T) { assert.Equal(t, 42, result) }) ✅ s.Run("subtest", func() { s.Equal(42, result) }) } ``` **自动修复**:否。
**默认启用**:是。
**原因**:防止未定义行为。 根据 `testify` [文档](https://pkg.go.dev/github.com/stretchr/testify/suite#Suite.Run),`s.Run` 应该 用于运行子测试。此调用(除其他外)会使用新的 `t` 实例初始化 suite, 并保护测试免受未定义行为(如 data races)的影响。 自动修复被禁用,因为在大多数情况下,它需要重写子测试中的断言,并可能导致死代码。 该检查器与 [suite-dont-use-pkg](#suite-dont-use-pkg) 结合使用时特别有用。 ### suite-thelper ``` ❌ func (s *RoomSuite) assertRoomRound(roundID RoundID) { s.Equal(roundID, s.getRoom().CurrentRound.ID) } ✅ func (s *RoomSuite) assertRoomRound(roundID RoundID) { s.T().Helper() s.Equal(roundID, s.getRoom().CurrentRound.ID) } ``` **自动修复**:是。
**默认启用**:否。
**原因**:与非 suite 测试 helper 保持一致。明确标记 helper 方法。 实际上 `s.T().Helper()` 调用并不重要,因为 `testify` 无论如何 [都会](https://github.com/stretchr/testify/blob/882382d845cd9780bd93c1acc8e1fa2ffe266ca1/assert/assertions.go#L317)打印完整的 `Error Trace`。 该检查器更像是 [checkers.AdvancedChecker](https://github.com/Antonboom/testifylint/blob/676324836555445fded4e9afc004101ec6f597fe/internal/checkers/checker.go#L56) 的一个示例。 ### useless-assert 该检查器防止对同一个变量进行断言: ``` assert.Contains(t, tt.value, tt.value) assert.ElementsMatch(t, tt.value, tt.value) assert.Equal(t, tt.value, tt.value) assert.EqualExportedValues(t, tt.value, tt.value) // And other assert functions... assert.True(t, num > num) assert.True(t, num < num) assert.True(t, num >= num) assert.True(t, num <= num) assert.True(t, num == num) assert.True(t, num != num) assert.False(t, num > num) assert.False(t, num < num) assert.False(t, num >= num) assert.False(t, num <= num) assert.False(t, num == num) assert.False(t, num != num) ``` 并防止这些毫无意义的断言: ``` assert.Empty(t, "value") // Any string literal. assert.Error(t, nil) assert.False(t, false) // Any bool literal. assert.Implements(t, (*any)(nil), new(Conn)) assert.Negative(t, 42) // Any int literal. assert.Nil(t, nil) assert.NoError(t, nil) assert.NotEmpty(t, "value") // Any string literal. assert.NotImplements(t, (*any)(nil), new(Conn)) assert.NotNil(t, nil) assert.NotZero(t, 42) // Any int literal. assert.NotZero(t, "value") // Any string literal. assert.NotZero(t, nil) assert.NotZero(t, false) // Any bool literal. assert.Positive(t, 42) // Any int literal. assert.True(t, true) // Any bool literal. assert.Zero(t, 42) // Any int literal. assert.Zero(t, "value") // Any string literal. assert.Zero(t, nil) assert.Zero(t, false) // Any bool literal. assert.Negative(len(x)) assert.Less(len(x), 0) assert.Greater(0, len(x)) assert.GreaterOrEqual(len(x), 0) assert.LessOrEqual(0, len(x)) assert.Negative(uintVal) assert.Less(uintVal, 0) assert.Greater(0, uintVal) assert.GreaterOrEqual(uintVal, 0) assert.LessOrEqual(0, uintVal) ``` **自动修复**:否。
**默认启用**:是。
**原因**:防止 bug 和死代码。 ## 警告链 Linter 不会自动处理更改的“演变”。 在极少数情况下,它可能会对你的代码提出多次警告,例如: ``` assert.True(err == nil) // compares: use assert.Equal assert.Equal(t, err, nil) // error-nil: use assert.NoError assert.NoError(t, err) // require-error: for error assertions use require require.NoError(t, err) ``` 如果你有关于如何改进这一点的想法,请[贡献力量](./CONTRIBUTING.md)。 ## testify v2 `testify` 的第二个版本[承诺](https://github.com/stretchr/testify/issues/1089)提供更“令人愉悦”的 API,并 使得上述的一些检查器变得不再必要。 在这种情况下,不排除在 linter 中支持 `v2` 的可能性。 但目前看来,我们离 `v2` 还有[极远的距离](https://github.com/stretchr/testify/issues/1089#issuecomment-1812734472)。 相关的里程碑在[这里](https://github.com/stretchr/testify/milestone/4)。
标签:Apache Flink, EVTX分析, Go, Ruby工具, SOC Prime, 代码规范检查, 开发工具, 日志审计, 测试工具, 错误基检测, 静态代码分析