Our company has a Go language in the development stack. And sometimes, when writing unit tests for applications written in Go, we have difficulties. In this article, we will talk about some of the points that we take into account when writing tests. Let's take a look at how they can be used with examples.
We use interfaces when developing
, . . , . , , Redis - :
package yourpackage
import (
"context"
"github.com/go-redis/redis/v8"
)
func CheckLen(ctx context.Context, client *redis.Client, key string) bool {
val, err := client.Get(ctx, key).Result()
if err != nil {
return false
}
return len(val) < 10
}
:
package yourpackage
import (
"context"
"testing"
"github.com/go-redis/redis/v8"
)
func TestCheckLen(t *testing.T) {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
err := rdb.Set(ctx, "some_key", "value", 0).Err()
if err != nil {
t.Fatalf("redis return error: %s", err)
}
got := CheckLen(ctx, rdb, "some_key")
if !got {
t.Errorf("CheckLen return %v; want true", got)
}
}
, Redis ? , Redis CI? Redis? โ !
:
package yourpackage
import (
"context"
"github.com/go-redis/redis/v8"
)
type Storage interface {
Set(ctx context.Context, key string, v interface{}) error
Get(ctx context.Context, key string) (string, error)
}
type RedisStorage struct {
Redis *redis.Client
}
func (rs *RedisStorage) Set(ctx context.Context, key string, v interface{}) error {
return rs.Redis.Set(ctx, key, v, 0).Err()
}
func (rs *RedisStorage) Get(ctx context.Context, key string) (string, error) {
return rs.Redis.Get(ctx, key).Result()
}
func CheckLen(ctx context.Context, storage Storage, key string) bool {
val, err := storage.Get(ctx, key)
if err != nil {
return false
}
return len(val) < 10
}
, , , Redis Memcached. :
package yourpackage
import (
"context"
"testing"
)
type testRedis struct{}
func (t *testRedis) Get(ctx context.Context, key string) (string, error) {
return "value", nil
}
func (t *testRedis) Set(ctx context.Context, key string, v interface{}) error {
return nil
}
func TestCheckLen(t *testing.T) {
ctx := context.Background()
storage := &testRedis{}
got := CheckLen(ctx, storage, "some_key")
if !got {
t.Errorf("CheckLen return %v; want true", got)
}
}
, . . . . mockery.
. :
mockery --recursive=true --inpackage --name=Storage
:
package yourpackage
import (
"context"
"testing"
mock "github.com/stretchr/testify/mock"
)
func TestCheckLen(t *testing.T) {
ctx := context.Background()
storage := new(MockStorage)
storage.On("Get", mock.Anything, "some_key").Return("value", nil)
got := CheckLen(ctx, storage, "some_key")
if !got {
t.Errorf("CheckLen return %v; want true", got)
}
, - , , Logrus.
package yourpackage
import (
log "github.com/sirupsen/logrus"
)
func Minus(a, b int) int {
log.Infof("Minus(%v, %v)", a, b)
return a - b
}
func Plus(a, b int) int {
log.Infof("Plus(%v, %v)", a, b)
return a + b
}
func Mul(a, b int) int {
log.Infof("Mul(%v, %v)", a, b)
return a + b //
}
:
package yourpackage
import "testing"
func TestPlus(t *testing.T) {
a, b, expected := 3, 2, 5
got := Plus(a, b)
if got != expected {
t.Errorf("Plus(%v, %v) return %v; want %v", a, b, got, expected)
}
}
func TestMinus(t *testing.T) {
a, b, expected := 3, 2, 1
got := Minus(a, b)
if got != expected {
t.Errorf("Minus(%v, %v) return %v; want %v", a, b, got, expected)
}
}
func TestMul(t *testing.T) {
a, b, expected := 3, 2, 6
got := Mul(a, b)
if got != expected {
t.Errorf("Mul(%v, %v) return %v; want %v", a, b, got, expected)
}
}
, , :
time="2021-03-22T22:09:54+03:00" level=info msg="Plus(3, 2)"
time="2021-03-22T22:09:54+03:00" level=info msg="Minus(3, 2)"
time="2021-03-22T22:09:54+03:00" level=info msg="Mul(3, 2)"
--- FAIL: TestMul (0.00s)
yourpackage_test.go:55: Mul(3, 2) return 5; want 6
FAIL
FAIL gotest2/yourpackage 0.002s
FAIL
, . , . :
package yourpackage
import (
"io"
"testing"
"github.com/sirupsen/logrus"
)
type logCapturer struct {
*testing.T
origOut io.Writer
}
func (tl logCapturer) Write(p []byte) (n int, err error) {
tl.Logf((string)(p))
return len(p), nil
}
func (tl logCapturer) Release() {
logrus.SetOutput(tl.origOut)
}
func CaptureLog(t *testing.T) *logCapturer {
lc := logCapturer{T: t, origOut: logrus.StandardLogger().Out}
if !testing.Verbose() {
logrus.SetOutput(lc)
}
return &lc
}
func TestPlus(t *testing.T) {
defer CaptureLog(t).Release()
a, b, expected := 3, 2, 5
got := Plus(a, b)
if got != expected {
t.Errorf("Plus(%v, %v) return %v; want %v", a, b, got, expected)
}
}
func TestMinus(t *testing.T) {
defer CaptureLog(t).Release()
a, b, expected := 3, 2, 5
got := Minus(a, b)
if got != expected {
t.Errorf("Minus(%v, %v) return %v; want %v", a, b, got, expected)
}
}
func TestMul(t *testing.T) {
defer CaptureLog(t).Release()
a, b, expected := 3, 2, 5
got := Mul(a, b)
if got != expected {
t.Errorf("Mul(%v, %v) return %v; want %v", a, b, got, expected)
}
}
, , :
--- FAIL: TestMul (0.00s)
yourpackage_test.go:16: time="2021-03-22T22:10:52+03:00" level=info msg="Mul(3, 2)"
yourpackage_test.go:55: Mul(3, 2) return 5; want 6
FAIL
FAIL gotest2/yourpackage 0.002s
FAIL
, . . cover, :
$ go tool cover -help
Usage of 'go tool cover':
Given a coverage profile produced by 'go test':
go test -coverprofile=c.out
...
Display coverage percentages to stdout for each function:
go tool cover -func=c.out
:
$ go test -coverprofile=c.out ./...
ok gotestcover/minus 0.001s coverage: 100.0% of statements
? gotestcover/mul [no test files]
ok gotestcover/plus 0.001s coverage: 100.0% of statements
, 100 % . :
$ go tool cover -func=c.out
gotestcover/minus/minus.go:4: Minus 100.0%
gotestcover/plus/plus.go:4: Plus 100.0%
total: (statements) 100.0%
- . . , . , , . HTML-. , , , , . , :
go test -coverpkg=./... -coverprofile=c.out ./โฆ
:
$ go tool cover -func=c.out
gotestcover/minus/minus.go:4: Minus 100.0%
gotestcover/mul/mul.go:4: Mul 0.0%
gotestcover/plus/plus.go:4: Plus 100.0%
total: (statements) 66.7%
Go - . - -, , , Python, ยซ ยป.
, ? , . , . :
func TestRunMain(t *testing.T) {
main()
}
, , . , . , . , . main
. main
, . web- graceful shutdown, , . web-, curl, .
( https://gobyexample.com/http-servers):
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"time"
)
func hello(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "hello\n")
}
func headers(w http.ResponseWriter, req *http.Request) {
for name, headers := range req.Header {
for _, h := range headers {
fmt.Fprintf(w, "%v: %v\n", name, h)
}
}
}
func main() {
http.HandleFunc("/hello", hello)
http.HandleFunc("/headers", headers)
// ,
// ,
server := &http.Server{Addr: ":8090", Handler: nil}
//
go func() {
server.ListenAndServe()
}()
//
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(ctx)
}
:
// +build testrunmain
package main
import "testing"
func TestRunMain(t *testing.T) {
main()
}
+build testrunmain
, , tag. :
$ go test -v -tags testrunmain -coverpkg=./... -coverprofile=c.out ./...
=== RUN TestRunMain
curl:
$ curl 127.0.0.1:8090/hello
hello
, Ctrl+C:
$ go test -v -tags testrunmain -coverpkg=./... -coverprofile=c.out ./...
=== RUN TestRunMain
^C--- PASS: TestRunMain (100.92s)
PASS
coverage: 80.0% of statements in ./...
ok gobintest 100.926s coverage: 80.0% of statements in ./โฆ
, headers
:
$ go tool cover -func=c.out
gobintest/main.go:12: hello 100.0%
gobintest/main.go:16: headers 0.0%
gobintest/main.go:24: main 100.0%
total: (statements) 80.0%
, , Go. , .