go 单元测试

单元测试作用

上家公司,由于项目过多依赖于服务器环境,在本地压根跑不起来,所以从不写单元测试。后来换了家做业务的公司,逐渐意识到单元测试的重要性。写了一周多的单元测试。好处总结有两点

  1. 提前发现和拦截低级 bug,减少后期测试成本
  2. 重构或迭代后,进行归回测试,防止 改出bug
  3. 作为 CI/CD 第一环节

单元测试用例

定义一个方法

func Split(s, sep string) (result []string) {
	i := strings.Index(s, sep)

	for i > -1 {
		result = append(result, s[:i])
		s = s[i+len(sep):] // 这里使用len(sep)获取sep的长度
		i = strings.Index(s, sep)
	}
	result = append(result, s)
	return
}

表格驱动测试

// 表格驱动测试
func TestList(t *testing.T) {
	type test struct {
		input string
		sep   string
		want  []string
	}

	tests := map[string]test{ // 测试用例使用map存储
		"simple":      {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
		"wrong sep":   {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
		"more sep":    {input: "abcd", sep: "bc", want: []string{"a", "d"}},
		"leading sep": {input: "枯藤老树昏鸦", sep: "老", want: []string{"枯藤", "树昏鸦"}},
	}
	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			got := Split(tc.input, tc.sep)
			if !reflect.DeepEqual(got, tc.want) {
				t.Errorf("want: %+v, got: %+v;", tc.want, got)
			}
		})
	}
}

运行

go test -v  ./...

知识点总结

  1. t.Run() 是go推出的子测试;如果使用传统方法,第一个 case 失败,后面的 case 就不会跑了
  2. 报错能直接告诉你,是哪个case错了
  3. 单独运行某个函数:go test -v ./spilt/... -run=TestList
  4. 单独运行某个 case: go test -v ./spilt/... -run=TestList/leading_sep

使用 testify mock 数据

原始方法

type User struct {
	ID   int64
	Name string
}

type UserRepo interface {
	GetByID(ctx context.Context, id int64) (*User, error)
}

type UserService struct {
	repo UserRepo
}

func NewUserService(repo UserRepo) *UserService {
	return &UserService{repo: repo}
}

func (s *UserService) GetUserName(ctx context.Context, id int64) (string, error) {
	user, err := s.repo.GetByID(ctx, id)
	if err != nil {
		return "", err
	}
	return user.Name, nil
}

测试方法

package d2

import (
	"context"
	"errors"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

type MockUserRepo struct {
	mock.Mock // 这就是剧本,接下来就用它来演戏
}

func (m *MockUserRepo) GetByID(ctx context.Context, id int64) (*User, error) {
	//记录一次调用 + 从预设的返回值仓库里,按参数匹配拿结果
	args := m.Called(ctx, id)

	var user *User
	if args.Get(0) != nil {
		user = args.Get(0).(*User)
	}

	return user, args.Error(1)
}

func TestUserService_GetUserName(t *testing.T) {
	ctx := context.Background()

	tests := []struct {
		name      string
		id        int64
		mockSetup func(repo *MockUserRepo)
		wantName  string
		wantErr   bool
	}{
		{
			name: "success",
			id:   1,
            //定义 mock 的入参和出参
			mockSetup: func(repo *MockUserRepo) {
				repo.On(
					"GetByID",
					mock.Anything,
					int64(1),
				).Return(
					&User{ID: 1, Name: "Tom"},
					nil,
				)
			},
			wantName: "Tom",
			wantErr:  false,
		},
		{
			name: "repo error",
			id:   2,
			mockSetup: func(repo *MockUserRepo) {
				repo.On(
					"GetByID",
					mock.Anything,
					int64(2),
				).Return(
					nil,
					errors.New("db error"),
				)
			},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			repo := new(MockUserRepo)
			tt.mockSetup(repo)

			svc := NewUserService(repo)

			name, err := svc.GetUserName(ctx, tt.id)

			if tt.wantErr {
				assert.Error(t, err)
				assert.Empty(t, name)
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.wantName, name)
			}

			repo.AssertExpectations(t)
		})
	}
}

posted @ 2026-02-02 01:39  沧海一声笑rush  阅读(5)  评论(0)    收藏  举报