golang的测试单元

84 阅读6分钟

测试单元在软件开发中起着至关重要的作用,主要有以下几个方面:

一、保证代码质量

  1. 错误检测:

    • 测试单元能够在开发过程中及时发现代码中的错误。例如,对于一个数学计算函数,如果传入特定的输入值时产生了错误的输出,单元测试可以迅速捕捉到这个问题。
    • 通过对各种边界情况和特殊输入的测试,可以确保代码在不同场景下都能正确运行。比如对于一个处理字符串的函数,测试单元可以验证空字符串、非常长的字符串以及包含特殊字符的字符串等情况下函数的行为是否正确。
  2. 回归测试:

    • 当对现有代码进行修改或扩展时,测试单元可以确保新的更改没有破坏已有的功能。例如,在一个不断演进的软件项目中,如果对某个模块进行了优化,单元测试可以快速验证该模块以及与之相关的其他模块是否仍然正常工作。
    • 如果没有单元测试,开发人员很难确定新的代码变更是否引入了新的错误,而单元测试可以提供一种可靠的方式来进行回归测试,保证软件的稳定性。

二、提高开发效率

  1. 快速反馈:

    • 单元测试执行速度快,可以在短时间内给出测试结果。这使得开发人员能够迅速了解他们所做的代码更改是否正确,从而及时进行调整。例如,在编写代码的过程中,开发人员可以频繁地运行单元测试,以便在出现问题时立即进行修复,而不必等到整个系统集成测试时才发现问题,这样可以大大节省开发时间。
    • 相比集成测试或系统测试,单元测试可以在开发的早期阶段发现问题,减少了后期调试的时间和成本。
  2. 可维护性:

    • 良好的单元测试可以提高代码的可维护性。当其他开发人员接手一个项目时,通过阅读现有的单元测试代码,可以更快地理解代码的功能和预期行为。例如,单元测试中的测试用例可以清晰地展示各种输入情况下函数的输出,帮助新的开发人员了解代码的逻辑和边界条件。
    • 如果需要对代码进行修改,单元测试可以作为一种安全网,确保修改后的代码仍然能够正常工作。开发人员可以在修改代码后立即运行单元测试,以验证他们的更改没有破坏现有功能。

三、促进良好的设计

  1. 解耦和模块化:

    • 为了编写有效的单元测试,代码通常需要具有高内聚、低耦合的特点。这意味着开发人员需要将代码设计成可独立测试的模块,从而促进了良好的软件设计。例如,一个复杂的业务逻辑如果被拆分成多个小的、可测试的函数,不仅便于测试,也使得代码更易于理解和维护。
    • 单元测试迫使开发人员考虑代码的可测试性,从而促使他们设计出更加清晰、简洁和模块化的架构。
  2. 文档作用:

    • 单元测试可以作为一种形式的代码文档。测试用例中的输入和预期输出可以帮助其他开发人员理解代码的功能和使用方法。例如,一个函数的单元测试可以清晰地展示该函数在不同输入情况下的行为,相当于为该函数提供了一种实际的使用示例。
    • 相比传统的文档,单元测试更加准确地反映了代码的实际行为,因为它们是与代码同步更新的。如果代码发生了变化,相应的单元测试也需要进行调整,从而保证了文档的时效性。

    实际使用案例

    首先满足解藕和模块化,特别是在logic层,避免无法测试

    • 不满足的case:
    func (l *AnswerLogic) HandleAnswerTotal(ctx context.Context, sessionId string, uid int64) error {
    	// 创建或者更新问题汇总
    	modelTotalOpt := &aimodel.AiAnswerTotal{
    		SessionId: sessionId,
    		Uid:       uid,
    	}
    	modelTotal, err := l.daoTotal.FindOne(ctx, modelTotalOpt)
    	if err != nil {
    		return err
    	}
    	// 创建会话记录
    	if modelTotal == nil {
    		userInfo, err := l.user.UserInfo(cast.ToUint64(uid))
    		if err != nil {
    			logger.Error("ConsumerLogic.HandleAnswerTotal UserInfo error", zap.Reflect("id", uid), zap.Error(err))
    		}
    		modelTotalOpt.Status = aimodel.StatusUnFinish
    		modelTotalOpt.BusinessLine = userInfo.BusinessLine
    		modelTotalOpt.Role = userInfo.Role
    		modelTotalOpt.Nickname = userInfo.Nickname
    		modelTotalOpt.CreatedAt = time.Now().Unix()
    		modelTotalOpt.Nums = 1
    		if err = l.daoTotal.Create(ctx, modelTotalOpt); err != nil {
    			return err
    		}
    		return nil
    	}
    	columns := make(map[string]interface{}, 0)
    	// 会话数量+1
    	columns["nums"] = modelTotal.Nums + 1 // 会话数量+1
    	// 好评数量+1
    	columns["useful_nums"] = modelTotal.UsefulNums + 1
    	// 计算会话持续时长
    	columns["session_duration"] = time.Now().Unix() - modelTotal.CreatedAt
    	columns["updated_at"] = time.Now().Unix()
    	_, err = l.daoTotal.Update(ctx, modelTotal, columns)
    	return err
    }
    
    • 修改之后,满足的case:
    func (l *AnswerLogic) HandleAnswerTotal(ctx context.Context, sessionId string, uid int64) error {
    	// 创建或者更新问题汇总
    	modelTotalOpt := &aimodel.AiAnswerTotal{
    		SessionId: sessionId,
    		Uid:       uid,
    	}
    	modelTotal, err := l.daoTotal.FindOne(ctx, modelTotalOpt)
    	if err != nil {
    		return err
    	}
    	// 创建会话记录
    	if modelTotal == nil {
    		return l.createAnswerTotal(ctx, uid, modelTotalOpt)
    	}
    	// 更新会话记录
    	err = l.updateAnswerTotal(ctx, modelTotal, err)
    	return err
    }
    

    满足解藕和模块化之后,每个小的功能都可以用来进行单元测试

    gotests

    gotests -all -w example.go
    gotests -only "multiply" -w example.go
    
    • 生成go-kit框架中的logic层的测试单元
    func TestAnswerLogic_updateAnswerTotal(t *testing.T) {
    	masterDB, slaveDB := test.InitMysql()
    	type fields struct {
    		daoDetail aidao.DetailDao
    		daoTotal  aidao.TotalDao
    	}
    	type args struct {
    		ctx        context.Context
    		modelTotal *aimodel.AiAnswerTotal
    		err        error
    	}
    	tests := []struct {
    		name    string
    		fields  fields
    		args    args
    		wantErr bool
    	}{
    		{
    			name: "Test Case 1: Success",
    			fields: fields{
    				daoDetail: aidao.NewMockDetailDao(masterDB, slaveDB),
    				daoTotal:  aidao.NewMockTotalDao(masterDB, slaveDB),
    			},
    			args: args{
    				ctx: context.Background(),
    				modelTotal: &aimodel.AiAnswerTotal{
    					Uid: 10000,
    				},
    			},
    		},
    	}
    	for _, tt := range tests {
    		t.Run(tt.name, func(t *testing.T) {
    			l := &AnswerLogic{
    				daoDetail: tt.fields.daoDetail,
    				daoTotal:  tt.fields.daoTotal,
    			}
    			if err := l.updateAnswerTotal(tt.args.ctx, tt.args.modelTotal, tt.args.err); (err != nil) != tt.wantErr {
    				t.Errorf("AnswerLogic.updateAnswerTotal() error = %v, wantErr %v", err, tt.wantErr)
    			}
    		})
    	}
    }
    
    • masterDB, slaveDB := test.InitMysql(),获取db的主从库连接

    • 增加dao层的mock实例

      ``

    • 执行测试单元go test -run=TestAnswerLogic_updateAnswerTotal

    mock类型

    • gomock:模拟函数的调用和返回

      • 比如第三方的接口开发时间晚,无法正常调用。则使用mockgen命令生成模拟对象
      • gotests的case中模拟http请求的返回结果
      func TestAnswerLogic_answerAction(t *testing.T) {
      	type fields struct {
      		aiService ai.AiService
      	}
      	type args struct {
      		ctx       *gin.Context
      		req       aistruct.AnswerReq
      		answerReq *ai.AnswerReq
      	}
      	// 创建ai服务
      	ctrl := gomock.NewController(t)
      	defer ctrl.Finish()
      	mockAi := ai.NewMockAiService(ctrl)
      	mockAi.EXPECT().
      		Answering(gomock.Any(), gomock.Any(), gomock.Any()).
      		Do(func(ctx context.Context, answerReq *ai.AnswerReq, ch chan string) {
      			ch <- "hello vincy1"
      			ch <- "hello vincy2"
      			ch <- "hello vincy3"
      			close(ch)
      			return
      		})
      	w := httptest.NewRecorder()
      	req, _ := http.NewRequest("GET", "/", nil)
      	c, _ := gin.CreateTestContext(w)
      	c.Request = req
      	tests := []struct {
      		name    string
      		fields  fields
      		args    args
      		want    string
      		wantErr bool
      	}{
      		{
      			name: "Test Case 1: Success",
      			fields: fields{
      				aiService: mockAi,
      			},
      			args: args{
      				ctx: c,
      				req: aistruct.AnswerReq{
      					Uid: 10000,
      				},
      			},
      			want: "hello vincy1\nhello vincy2\nhello vincy3\n",
      		},
      	}
      	for _, tt := range tests {
      		t.Run(tt.name, func(t *testing.T) {
      			l := &AnswerLogic{
      				aiService: tt.fields.aiService,
      			}
      			got, err := l.answerAction(tt.args.ctx, tt.args.req, tt.args.answerReq)
      			if (err != nil) != tt.wantErr {
      				t.Errorf("AnswerLogic.answerAction() error = %v, wantErr %v", err, tt.wantErr)
      				return
      			}
      			if got != tt.want {
      				t.Errorf("AnswerLogic.answerAction() = %v, want %v", got, tt.want)
      			}
      		})
      	}
      }
      
    • sqlmock

      • 目前我们的13环境不存在网络隔离的情况,不展开说
    • httpmock

      • 对我来讲,几乎用不到。gomock模拟函数的返回就已经足够