Hints for Writing Tests in Go Applications

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
      
      



Logrus, . , Zap , .





Go - . , . , , . , . 





, . . 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. , . 





Go? : , , .








All Articles