Commands
Test all file
go test
Test all file including subdirectory
go test ./...
./...
will cache your testing result
Expires all test results
go clean -testcache
Ignore specific package
go test `go list ./... | grep -v your_go_app/utilities`
Test specific package
go test your_go_app/utilities/ip
Test specific func
go test -run TestListEvent
Benchmark
go test -bench=.
Race detection
go test -race
Coverage
Show coverage of code
go test -cover
PASS
coverage: 37.5% of statements
ok testing-example 0.016s
Generate coverage file
go test -coverprofile=coverage.out
Show func coverage based on coverage file
go tool cover -func=coverage.out
testing-example/main.go:11: main 0.0%
testing-example/main.go:18: SearchWordInFile 75.0%
total: (statements) 37.5%
Display coverage on browser based on coverage file
go tool cover -html=coverage.out
Examples
A simple example
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Sum(x int, y int) int {
return x + y
}
func TestSum(t *testing.T) {
total := Sum(5, 5)
assert.Equal(t, 10, total)
}
Use stretchr/testify to make testing clean
Example - http server
main.go
func handler() http.Handler {
r := http.NewServeMux()
r.HandleFunc("/health", health)
return r
}
func health(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "ok")
}
func main() {
log.Fatal(http.ListenAndServe(":8000", handler()))
}
main_test.go
func TestHealth(t *testing.T) {
req, _ := http.NewRequest("GET", "/health", nil)
rec := httptest.NewRecorder()
health(rec, req)
res := rec.Result()
defer res.Body.Close()
b, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, "ok", string(b))
}
func TestRouting(t *testing.T) {
s := httptest.NewServer(handler())
defer s.Close()
res, _ := http.Get(fmt.Sprintf("%s/health", s.URL))
defer res.Body.Close()
b, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, "ok", string(b))
}
Example - http server (use gorilla/mux)
main.go
func health(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "ok")
}
func Router() *mux.Router {
router := mux.NewRouter()
router.HandleFunc("/health", health).Methods("GET")
return router
}
func main() {
log.Fatal(http.ListenAndServe(":8000", Router()))
}
main_test.go
func TestHealth(t *testing.T) {
req, _ := http.NewRequest("GET", "/health", nil)
rec := httptest.NewRecorder()
Router().ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "ok", string(rec.Body.String()))
}
Mock
Eample - A simple example to show how to mock a func.
func TestSearchWordInFile(t *testing.T) {
b := SearchWordInFile(strings.NewReader("ABCDEFGHIJK"), "DEF")
assert.Equal(t, true, b)
}
func SearchWordInFile(r io.Reader, s string) bool {
b, _ := ioutil.ReadAll(r)
if strings.Index(string(b), s) > 0 {
return true
}
return false
}
Example - Mock struct’s func.
main.go
type Counter interface {
CountFaces(string) (int, error)
}
type OpenCV struct{}
func (cv *OpenCV) CountFaces(filepath string) (int, error) {
// Count faces from an image
return 5, nil
}
func GetFaceCount(c Counter, filepath string) (int, error) {
return c.CountFaces(filepath)
}
func main() {
cv := &OpenCV{}
count, _ := GetFaceCount(cv, "/tmp/fake-img.jpg")
fmt.Printf("Face count: %d\n", count)
}
main_test.go
type FakeOpenCV struct {
MockCount func(string) (int, error)
}
func (f *FakeOpenCV) CountFaces(s string) (int, error) {
return f.MockCount(s)
}
func TestDatabase(t *testing.T) {
fake := &FakeOpenCV{
MockCount: func(s string) (int, error) {
return 7, nil
},
}
count, err := GetFaceCount(fake, "/tmp/fake-img-test.jpg")
assert.NoError(t, err)
assert.Equal(t, 7, count)
}
main_test.go using stretchr/testify/mock
type FakeOpenCV struct {
mock.Mock
}
func (f *FakeOpenCV) CountFaces(s string) (int, error) {
args := f.Called(s)
return args.Int(0), nil
}
func TestDatabase(t *testing.T) {
fake := new(FakeOpenCV)
fake.On("CountFaces", "/tmp/fake-img-test.jpg").Return(7, nil)
count, err := GetFaceCount(fake, "/tmp/fake-img-test.jpg")
assert.NoError(t, err)
assert.Equal(t, 7, count)
}
Same example as above using GoMock
It has to be a package to generate mock file using GoMock.
workplace/counter/counter.go
package counter
type Counter interface {
CountFaces(string) (int, error)
}
type OpenCV struct{}
func (cv *OpenCV) CountFaces(filepath string) (int, error) {
// Count faces from an image
return 5, nil
}
func GetFaceCount(c Counter, filepath string) (int, error) {
return c.CountFaces(filepath)
}
workplace/counter/counter_test.go
package counter
import(
"testing"
"local-test/counter/mocks"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
func TestGetFaceCount(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mocks.NewMockCounter(ctrl)
m.EXPECT().CountFaces("").Return(7, nil)
count, err := GetFaceCount(m, "")
assert.NoError(t, err)
assert.Equal(t, 7, count)
}
CountFaces’ parameters passed by mock and via GetFaceCount must be the same.
Generate mock file
cd workplace/counter/
mockgen -destination=mocks/mock_counter.go -package=mocks workplace/counter Counter
workplace/counter/mocks is created by GoMock
Example - Mock controller and model, using pointer to fake data.
main.go
// Model
type table interface {
Get(int) error
}
type User struct {
ID int
Name string
}
func (u *User) Get(id int) error {
// Connect to DB and get user data by id.
u.ID = 6
u.Name = "Jack"
return nil
}
// Controller
type UserController struct {
User table
}
func (u *UserController) Info(resp http.ResponseWriter, req *http.Request) {
id := 5
_ = u.User.Get(id)
resp.Write([]byte(fmt.Sprintf("%v", u.User)))
fmt.Println(u.User) // &{6 Jack}
}
// Http server & Router
func main() {
userController := &UserController{User: &User{}}
r := http.NewServeMux()
r.HandleFunc("/user", userController.Info)
log.Fatal(http.ListenAndServe(":8000", r))
}
main_test.go
type FakeUser User
func (u *FakeUser) Get(id int) error { return nil }
func TestUserInfo(t *testing.T) {
userController := &UserController{User: &FakeUser{ID: 3, Name: "Bob"}}
req, _ := http.NewRequest("GET", "/", nil) // It won't be really routed, any path is okay.
rec := httptest.NewRecorder()
userController.Info(rec, req)
resp := rec.Result()
defer resp.Body.Close()
b, _ := ioutil.ReadAll(resp.Body)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, `&{3 Bob}`, string(b))
}
Go doesn’t support overriding method, becasue it’s embedded struct as opposed to inheritance
type DB interface {
Do()
}
type mysql struct{}
func (m *mysql) Do() {
fmt.Println(m.Custom())
}
func (m *mysql) Custom() string {
return "mysql"
}
type fakeMysql struct {
mysql
}
func (m *fakeMysql) Custom() string {
return "fake mysql"
}
func Do(db DB) {
db.Do()
}
func main() {
m := &mysql{}
Do(m)
f := &fakeMysql{}
Do(f)
}
output:
mysql
mysql
The second line is not what we expected as fake mysql
Stub
time.Now
main.go
var TimeNow = func() time.Time { return time.Now() }
func main() {
fmt.Println(GetTimestamp())
}
func GetTimestamp() int64 {
return TimeNow().UTC().Unix()
}
main_test.go
func TestGetTimestamp(t *testing.T) {
parsed, _ := time.Parse("2006-01-02 15:04:05", "2019-01-28 00:00:00")
TimeNow = func() time.Time { return parsed }
assert.Equal(t, parsed.Unix(), GetTimestamp())
}
time.Sleep
TODO, watch this video
Ginkgo
Ginkgo is a BDD testing framework for go.
Install
go get github.com/onsi/ginkgo/ginkgo
go get github.com/onsi/gomega
Create first test
Init
ginkgo bootstrap
ginkgo generate [app_name]
user_test.go :
var _ = Describe("User", func() {
Describe("Test User", func() {
Context("with SetName", func() {
data := map[string]interface{}{
"Name": "Jex",
"Address": "Taiwan",
}
It("should be a empty errMsg", func() {
Expect(SetName(data)).To(Equal(""))
})
})
})
})
Run
$ ginkgo
...略...
Ran 3 of 3 Specs in 6.895 seconds
SUCCESS! -- 3 Passed | 0 Failed | 0 Pending | 0 Skipped PASS
BeforeEach 在每個 Context 跑之前先執行
var _ = Describe("Sample", func() {
Describe("XXX", func() {
Describe("FFF", func() {
var qq string
BeforeEach(func() {
qq = "XXX"
fmt.Println("BeforeEach")
})
Context("Valid inputs for add function", func() {
It("TTT", func() {
qq = "ZZZZZ"
fmt.Println(qq)
Expect(4).Should(Equal(4))
})
It("TTT", func() {
fmt.Println(qq)
Expect(4).Should(Equal(4))
})
})
})
})
})
結果 :
BeforeEach // qq = XXX
ZZZZZ // qq = ZZZZ
BeforeEach // qq = XXX
XXX // qq = XXX
- 不管是 BeforeEach 賦與的變數或不同地方賦與的變數都要在 It 裡面才拿得到,在 It 外是取不到的,不知道是什麼原因?
- Describe / Context 似乎是用 goroutine 去跑的,如果有些值要先等前一個 Describe/context Set 完,執行時可能不會有你預期的結果,有可能根本沒 Set 到就先執行那段了
Benchmark
How to read benchmark result
Result
BenchmarkWorker-4 100000 19753 ns/op 1164 B/op 15 allocs/op
Explaination
ns/op
means that the loop ran 100000 times at a speed of 19753 ns per loop
B/op
is how many bytes were allocated per op
allocs/op
means how many distinct memory allocations occurred per op (single iteration)
ref: https://www.oipapio.com/question-4923866
ref: