Software engineering notes

Go Testing

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

Benchmark

How to read benchmark result

Result

BenchmarkWorker-4                 100000             19753 ns/op            1164 B/op         15 allocs/op

Explaination

ref: https://www.oipapio.com/question-4923866

ref: