Testing

Mock Data

1. refactor code to use mock data 

假设需要测试一个接口,只希望测试logic,并不希望有data层面的交互(test方法中需要初始化db等配置)

首先需要改造UserServer, 让 userData interface 注入,然后我可以实现userDataImpl (有*gorm.db注入),也可以实现mockDataimpl(内置map,内存存储数据)

type UserServer struct {
    proto.UnimplementedUserServer
    db UserData
}

// data interface
type UserData interface {
  // handler中和databse交互的方法 GetUserByMobile(ctx context.Context, mobile
string) (User, error) // 这个方法查询出来后用来注入空User中,所以返回User Create(user User) (tx *gorm.DB) } // mock the data tier for test type userDataMock struct { userinfo map[string]User } func (u *userDataMock) GetUserByMobile(ctx context.Context, mobile string) (User, error) { return u.userinfo[mobile], nil } func (u *userDataMock) Create(user User) (tx *gorm.DB) { u.userinfo[user.Mobile] = user return nil } // impl the data tier, need db type userDataImpl struct { db *gorm.DB } func (u *userDataImpl) GetUserByMobile(ctx context.Context, mobile string) (User, error) { return User{}, nil } func (u *userDataImpl) Create(user *User) (tx *gorm.DB) { return u.db.Create(&user) } // init the impl func NewUserDataMock() *userDataMock { return &userDataMock{userinfo: make(map[string]User)} } func NewUserData(db *gorm.DB) *userDataImpl { return &userDataImpl{db: db} }

handler方法:

func (s *UserServer) CreateUser(ctx context.Context, req *proto.CreateUserInfo) (*proto.UserInfoRsp, error) {
    var user User
    user, _ = s.db.GetUserByMobile(ctx, req.Mobile) // UserServer的userData interface,适配mock数据需求和正常db连接需求
    //result := s.db.Where(&User{Mobile: req.Mobile}).First(&user)
    //if result.RowsAffected == 1 {
    //    return nil, status.Errorf(codes.AlreadyExists, "user already exists")
    //}
    user.Mobile = req.Mobile
    user.NickName = req.NickName

    options := &password.Options{16, 100, 30, sha512.New}
    salt, encodedPwd := password.Encode("generic password", options)
    user.Password = fmt.Sprintf("$sha512$%s$%s", salt, encodedPwd)
    //result = global.DB.Create(&user)
    //if result.Error != nil {
    //    return nil, status.Errorf(codes.Internal, result.Error.Error())
    //}
    _ = s.db.Create(user)
    // create user后database生成id -> 返回给调用方
    rsp := ModelToRsp(user)
    return &rsp, nil
}

以上,可以手动加入实现类,手动测试 

2. 也使用mock data工具,自动生成impl方便测试

2.1 install

go get -u github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen

2.2 create mock pkg, create user.go for generating 

package mock

import "context"

type User struct {
    Mobile string
    Name   string
}

type UserServer struct {
    Db UserData
}

// data interface => the mockgen will generate this impl
type UserData interface {
    GetUserByMobile(ctx context.Context, mobile string) (User, error)
}
// this is the server's method containing logic
func (u *UserServer) GetUserByMobile(ctx context.Context, mobile string) (User, error) {
    // get data
    user, _ := u.Db.GetUserByMobile(ctx, mobile)
    // business logic we focus on
    if user.Name == "bobo_18" {
        user.Name = "bobo_17"
    }
    return user, nil
}

2.2 cd to mock 

mockgen -source user.go -destination=./mock1/user_mock.go -package=mock1  

generate: user_mock.go 

2.3 write Testing()

func TestGetUserByMobile(t *testing.T) {
    // prepare,自动生成一个userData的impl类,满足mobile=18就返回Name=bobo_18
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockUserData := NewMockUserData(ctrl)
    mockUserData.EXPECT().GetUserByMobile(gomock.Any(), "18").Return(
        mock.User{Name: "bobo_18"}, nil)

    // inject data & invoke method to test
    userServer := mock.UserServer{
        Db: mockUserData,
    }
    user, err := userServer.GetUserByMobile(context.Background(), "18")
    if user.Name != "bobo_17" {   // given the data, test the logic 
        t.Errorf("error: %v", err)
    }
}

Fuzz Test 

模糊测试:生成随机测试数据,detect边界情况

package fuzz

import (
    "github.com/stretchr/testify/assert"
    "testing"
    "unicode/utf8"
)

func Reverse(s string) string {
    b := []byte(s)

    for i, j := 0, len(s)-1; i < len(s)/2; i, j = i+1, j-1 {
        b[i], b[j] = b[j], b[i]
    }
    return string(b)
}

// Begin with TestXXX()
func TestReverse(t *testing.T) {
    testcases := []struct {
        in  string
        out string
    }{
        {"hello", "olleh"},
        {"ni", "in"},
    }
    for _, test := range testcases {
        out := Reverse(test.in)
        assert.Equal(t, out, test.out)
    }
}

// use fuzzing to randomly generate params
// Begin with FuzzXXX()
func FuzzReverse(f *testing.F) {
    testcases := []string{
        "hello",
        " ",
        "dss*!",
    }
    for _, cases := range testcases {
        f.Add(cases)
    }
    f.Fuzz(func(t *testing.T, in string) {
        out := Reverse(in)
        // fuzz测试,只能使用技巧判断
        assert.Equal(t, len(in), len(out))
        double := Reverse(out)
        assert.Equal(t, double, in)
        // 如果不同时为utf8必然错
        if utf8.ValidString(in) && !utf8.ValidString(out) {
            t.Errorf("failed")
        }
    })
}

基于seed生成input需要使用: go test -fuzz=Fuzz 

结果:Failed

生成了:

 点进去:Fail的原因:中文字符转换成[]byte后不对称

fuzz test和 unit test是互补的
go test会把外部数据放到testdata/

Go monkey

go monkey(for stubbing): A func()调用 B func(),不希望调用B,只关注A函数的逻辑
动态补丁patch,允许对任何function/interface/global variable mock
Not invasive to the code

go-monkey is a stubbing and patching library for the Go programming language, primarily used in unit testing. It allows developers to dynamically replace the implementations of functions and methods during testing, which helps control dependencies and environments, thereby simplifying the testing process and improving the stability and controllability of tests.

Key Features

  1. Function Stubbing:

    • Using ApplyFunc, you can replace the implementation of a regular function.
  2. Method Stubbing:

    • Using ApplyMethod, you can replace the implementation of a method associated with a specific type.
  3. Instance Method Stubbing:

    • Using ApplyInstanceMethod, you can replace the implementation of an instance method for a specific object.
  4. Chaining Patches:

    • You can chain multiple patches together to replace various functions or methods within a single test.

go get github.com/agiledragon/gomonkey@v2.0.2

1. function stubbing

func Minus(a, b int) (int, error) {
    return a - b, nil
}

func ComputeMinus(a, b int) (int, error) {
    result, _ := Minus(a, b)
    return result + 10, nil
}

func TestCompute1(t *testing.T) {
    patches := gomonkey.ApplyFunc(Minus, func(a, b int) (int, error) {
        return 2, nil
    })
    defer patches.Reset()

    sum, _ := ComputeMinus(1, 2)
    fmt.Println(sum)
    if sum != 12 {
        t.Errorf("sum is %d,we want 12", sum)
        //t.Errorf("sum is %d,we want 12", sum)
    }
}

2. method stubbing

type Computer struct {
}

func (c *Computer) Sum(a, b int) (int, error) {
    return a + b, nil
}

func ComputeSum(a, b int) (int, error) {
    var c Computer
    result, _ := c.Sum(a, b)
    return result + 10, nil
}

func TestCompute2(t *testing.T) {
    var c *Computer
    patches := gomonkey.ApplyMethod(reflect.TypeOf(c), "Sum", func(_ *Computer, a, b int) (int, error) {
        return 2, nil
    })
    defer patches.Reset()

    sum, _ := ComputeSum(1, 2)
    fmt.Println(sum)
    if sum != 12 {
        t.Errorf("sum is %d,we want 12", sum)
    }
}

3. global variable

var num = 10

func TestGlobalVar(t *testing.T) {
    patches := gomonkey.ApplyGlobalVar(&num, 12)
    defer patches.Reset()

    if num != 12 {
        t.Errorf("num is %d,we want 12", num)
    }
}

Ginkgo & go convey

BDD testing framework

demo:

package ginkgo

import (
    . "github.com/onsi/ginkgo/v2"
    . "github.com/onsi/gomega"
    "testing"
)

func TestBooks(t *testing.T) {
    RegisterFailHandler(Fail)
    RunSpecs(t, "Books Suite") // run a collection called "Books Suite"
}

var _ = Describe("Books", func() {
    // prepare data
    var (
        longBook  string
        shortBook string
    )
    BeforeEach(func() {
        longBook = "long"
        shortBook = "short"
    })
    AfterEach(func() {
        longBook = "long"
        shortBook = "short"
    })
    Describe("add a book", func() {
        It("it should be able to add a book", func() {
            Expect("long1").To(Equal(longBook))               //fail
        })
        It("it should not be able to add a book", func() {
            Expect("short").To(Equal(shortBook))              // pass
        })
    })

    Describe("delete a book", func() {

    })

})

imtegrate with mock/go monkey test => target at core business logic, not for each testcases

// prepare for mock/go monkey 
    var (
        patches *gomonkey.Patches
        ctrl    *gomock.Controller
    )
    BeforeEach(func() {

        ctrl := gomock.NewController(GinkgoT())
    })
    AfterEach(func() {
        ctrl.Finish()
        patches.Reset()
    })

 

posted @ 2024-06-21 07:00  PEAR2020  阅读(2)  评论(0编辑  收藏  举报