https://github.com/ichiban/seams
Go static analyzer that reports untestable function/method calls
https://github.com/ichiban/seams
go golang static-analysis testing
Last synced: 5 months ago
JSON representation
Go static analyzer that reports untestable function/method calls
- Host: GitHub
- URL: https://github.com/ichiban/seams
- Owner: ichiban
- License: mit
- Created: 2019-10-20T03:07:19.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2019-11-03T13:49:21.000Z (over 6 years ago)
- Last Synced: 2024-06-20T07:29:52.215Z (about 2 years ago)
- Topics: go, golang, static-analysis, testing
- Language: Go
- Homepage:
- Size: 7.81 KB
- Stars: 1
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.md
Awesome Lists containing this project
README
# seams
Go static analyzer that reports untestable function/method calls.
## As a command
Add `seams` as a tool dependency in your `go.mod`:
```shellsession
$ go get -tool github.com/ichiban/seams/cmd/seams
```
Then, run via `go tool`:
```shellsession
$ go tool seams ./...
main.go:8:17: untestable function/method call: time.Parse
main.go:11:7: untestable function/method call: (time.Duration).Hours
main.go:11:7: untestable function/method call: (time.Time).Sub
main.go:11:16: untestable function/method call: time.Now
main.go:12:2: untestable function/method call: fmt.Printf
```
To automatically fix the issues by introducing seam variables:
```shellsession
$ go tool seams -fix ./...
```
## As an `analysis.Analyzer`
Add the package as a dependency:
```shellsession
$ go get github.com/ichiban/seams
```
Then, include `seams.Analyzer` in your checker.
```go
package main
import (
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/multichecker"
"golang.org/x/tools/go/analysis/passes/nilfunc"
"golang.org/x/tools/go/analysis/passes/printf"
"golang.org/x/tools/go/analysis/passes/shift"
"github.com/ichiban/seams"
)
func main() {
multichecker.Main(
// other analyzers of your choice
nilfunc.Analyzer,
printf.Analyzer,
shift.Analyzer,
seams.Analyzer,
)
}
```
## Automatic fixing with `-fix`
The `-fix` flag automatically transforms untestable calls into testable ones by introducing seam variables.
### Function calls
Function calls are transformed by creating a package-level variable and updating the call site:
```go
// Before
fmt.Printf("Hello")
// After
var printf = fmt.Printf
printf("Hello")
```
### Method calls
Method calls are transformed using method expressions. The receiver becomes the first argument:
```go
// Before
var b strings.Builder
b.WriteString("text")
// After
var stringsBuilderWriteString = (*strings.Builder).WriteString
stringsBuilderWriteString(&b, "text")
```
### Naming conventions
| Call type | Naming rule | Example |
|-----------|-------------|---------|
| Function | Lowercase first letter | `fmt.Printf` → `printf` |
| Method | Package + Type + Method | `(*strings.Builder).WriteString` → `stringsBuilderWriteString` |
If a variable with the generated name already exists, the fix will reuse it without adding a new declaration.
## What do you mean by untestable?
This static analyzer reports function/method calls that are of:
- functions/methods defined in other packages,
- not builtin functions,
- not in tests, nor
- in generated files
because those function/method calls can't be replaced by test doubles in tests.
For example, `time.Now()` isn't testable because we can't change its behaviour for our tests.
Though, `timeNow()` where `var timeNow = time.Now` is testable because we can change the behaviour in tests.
### untestable example
```go
package main
import (
"fmt"
"time"
)
var date = must(time.Parse(time.RFC3339, "2019-12-20T00:00:00+09:00"))
func main() {
d := date.Sub(time.Now()).Hours() / 24
fmt.Printf("%d days until Star Wars: The Rise of Skywalker\n", int(d))
}
func must(t time.Time, err error) time.Time {
if err != nil {
panic(err)
}
return t
}
```
### testable example
```go
package main
import (
"fmt"
"time"
)
var (
timeParse = time.Parse
timeDurationHours = time.Duration.Hours
timeTimeSub = time.Time.Sub
timeNow = time.Now
fmtPrintf = fmt.Printf
)
var date = must(timeParse(time.RFC3339, "2019-12-20T00:00:00+09:00"))
func main() {
d := timeDurationHours(timeTimeSub(date, timeNow())) / 24
fmtPrintf("%d days until Star Wars: The Rise of Skywalker\n", int(d))
}
func must(t time.Time, err error) time.Time {
if err != nil {
panic(err)
}
return t
}
```
```go
package main
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func Test_main(t *testing.T) {
t.Run("on the day", func(t *testing.T) {
assert := assert.New(t)
now, err := time.Parse(time.RFC3339, "2019-12-20T12:00:00+09:00")
assert.NoError(err)
n := timeNow
defer func() { timeNow = n }()
timeNow = func() time.Time {
return now
}
called := false
p := fmtPrintf
defer func() { fmtPrintf = p }()
fmtPrintf = func(format string, a ...interface{}) (int, error) {
assert.Equal("%d days until Star Wars: The Rise of Skywalker\n", format)
assert.Equal([]interface{}{0}, a)
called = true
return 0, nil
}
main()
assert.True(called)
})
}
```
## License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details