前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TDD案例实战

TDD案例实战

原创
作者头像
Johns
修改2022-06-30 10:20:33
6040
修改2022-06-30 10:20:33
举报
文章被收录于专栏:代码工具代码工具

使用gin框架实现一个简单的手机号密码登录服务step1: 需求分析/任务拆分 案例代码地址: https://github.com/GuoGuiRong/TDD-demo

step1: 需求分析/任务拆分

这个阶段至少要确认以下内容:

  • 使用什么样的协议?
  • 输入/输出参数有哪些?
  • 其他细节
    • 参数验证的规则
    • 接口安全策略(签名规则)

需求分析后我们一般会做任务拆分/分解, 然后产出接口文档, 这个阶段一般需要前后端开发,产品,测试共同讨论:

image.png
image.png

step2: 编写接口测试用例

这个阶段我们主要是针对之前定义好的接口文档, 编写接口测试用例

image.png
image.png

step3. 编写单元测试用例

  • 测试用例上的通用信息进行封装
代码语言:go
复制
  var (
  	// 正常响应
  	Success = RetResp{0, "success"}
  
  	// 异常请求
  	BadRequest = RetResp{8000004000, "invalid request"}
  
  	// 登录请求参数解析异常
  	LoginParamsParseFailed = RetResp{8000004001, "request params parse failed"}
  
  	// 登录请求参数sign非法
  	LoginSignIllegal = RetResp{8000004002, "request sign illegal"}
  
  	// 登录请求过期
  	LoginTimestampExpire = RetResp{8000004003, "request timestamp expire"}
  
  	// 登录请求参数nonce非法
  	LoginNonceIllegal = RetResp{8000004004, "request nonce illegal"}
  
  	// 登录请求参数phone_number非法
  	LoginPhoneNumberIllegal = RetResp{8000004005, "request phone_number illegal"}
  
  	// 登录请求参数password非法
  	LoginPwdIllegal = RetResp{8000004006, "request password illegal"}
  
  	// 登录请求参数sign错误
  	LoginSignCheckFailed = RetResp{8000004007, "request sign check failed"}
  
  	// 账号phone_number不存在
  	LoginPhoneNumberNotExist = RetResp{8000004008, "request phone_number not exist"}
  
  	// 用户password错误
  	LoginPwdCheckFailed = RetResp{8000004009, "request password incorrect"}
  )
  
  // 通用常量
  var (
  	ExistPhoneNumber = "18018726093"
  	ExistPasswd      = "TesPwdT123"
  	Underline        = "_"
  	LoginPwdMinLen   = 6
  	LoginPwdNonceLen = 8
  	LoginPwdMaxLen   = 20
  	LoginSignLen     = 32
  )
  • 编写测试用例
代码语言:go
复制
  // TestLoginPwdService A1: 登陆服务
  func TestLoginPwdService(t *testing.T) {
      tests := []struct {
        name     string
        args     Args
        wantCode int
      }{
        {"Correct Request Params", CorrectRequestParams, pkg.Success.Code},
        {"Bad Request", BadRequestParams, pkg.BadRequest.Code},
        {"Request Expire Time", RequestExpiredTime, pkg.LoginTimestampExpire.Code},
        {"Request PhoneNumber Invalid", RequestPhoneNumberInvalid, pkg.LoginPhoneNumberIllegal.Code},
        {"Request Password too short", RequestPwdTooShort, pkg.LoginPwdIllegal.Code},
        {"Request Password too long", RequestPwdTooLong, pkg.LoginPwdIllegal.Code},
        {"Request Invalid Password Content", RequestInvalidPwdContent, pkg.LoginPwdCheckFailed.Code},
        {"Request Sign Invalid: length not equals 32", RequestSignWithBadLength, pkg.LoginSignIllegal.Code},
        {"Request Invalid Sign Content", RequestSignWithBadContent, pkg.LoginSignCheckFailed.Code},
        {"Request Nonce Invalid", RequestNonceInvalid, pkg.LoginNonceIllegal.Code},
      }
  
      ast := assert.New(t)
      for _, tt := range tests {
        // 准备, mock一个gin.Context, 并把用例数据载入其中
        var ctx *gin.Context
        var w *responseWriter
        if tt.wantCode != pkg.BadRequest.Code {
          ctx, _, w = buildRequest(tt.args.PhoneNumber, tt.args.Password,
            tt.args.Timestamp, tt.args.Nonce, tt.args.Sign)
        } else {
          ctx, _, w = buildBadRequest(tt.args.Timestamp, tt.args.Nonce, tt.args.Sign)
        }
  			
        // 执行用例
        LoginPwdService(ctx)
        resp := LoginPwdResp{}
        err := json.Unmarshal([]byte(w.Buff.String()), &resp)
        
        // 断言
        ast.False(err != nil, tt.name)
        ast.Equal(resp.Code, tt.wantCode, tt.name)
      }
    }

step4. 实现LoginPwdService

代码语言:go
复制
// LoginPwdService 密码登陆
func LoginPwdService(c *gin.Context) {

	// step1. 参数解析
	req, err := ParseLoginReqParams(c)
	if err != nil {
		// 退出
		c.JSON(http.StatusBadRequest, gin.H{
			"code":    pkg.BadRequest.Code,
			"message": err.Error(),
		})
		return
	}

	// step2. 参数验证
	code, err := CheckLoginReqParams(req.PhoneNumber, req.Password, req.Timestamp, req.Nonce, req.Sign)
	if err != nil {
		// 退出
		c.JSON(http.StatusBadRequest, gin.H{
			"code":    code,
			"message": err.Error(),
		})
		return
	}

	// step3: 签名验证
	code, err = CheckLoginSignature(req.PhoneNumber, req.Password, req.Timestamp, req.Nonce, req.Sign)
	if err != nil {
		// 退出
		c.JSON(http.StatusBadRequest, gin.H{
			"code":    code,
			"message": err.Error(),
		})
		return
	}

	// step4: 密码确认
	code, err = CheckLoginPwd(req.PhoneNumber, req.Password)
	if err != nil {
		// 退出
		c.JSON(http.StatusBadRequest, gin.H{
			"code":    code,
			"message": err.Error(),
		})
		return
	}

	// step5. 生成token
	token := pkg.MD5(req.Password+req.Timestamp, req.PhoneNumber)
	data := map[string]string{
		"token": token,
	}

	// step6. 响应结果验证
	c.JSON(http.StatusBadRequest, gin.H{
		"code":    pkg.Success.Code,
		"message": pkg.Success.Msg,
		"data":    data,
	})
	return
}

遇到子方法需要独立实现咋么办?

当我们编写实现时, 可能发现有些地方需要独立实现, 比如我们需要一个独立的CheckLoginSignature方法, 使用TDD的话, 我们需要为这个方法独立设计测试用例:

代码语言:go
复制
// CheckSignature 签名检查
func CheckLoginSignature(phoneNumber, password, reqTimestamp, nonce, sign string) (code int, err error) {
  // 先不做具体实现, 只是定义输入输出
  // 初始化默认使用异常输出
  ....
  
	return pkg.LoginSignCheckFailed.Code, nil
}

// TestCheckSignature A0:测试签名
func TestCheckSignature(t *testing.T) {
	tests := []struct {
		name     string
		args     Args
		wantCode int
		wantErr  bool
	}{
		{"Correct Sign", CorrectRequestParams, pkg.Success.Code, false},
		{" Sign With Bad length ", RequestSignWithBadLength, pkg.LoginSignCheckFailed.Code, true},
		{" Sign Is Illegal ", RequestSignWithBadContent, pkg.LoginSignCheckFailed.Code, true},
	}

	ast := assert.New(t)
	for _, tt := range tests {
		got, err := CheckLoginSignature(tt.args.PhoneNumber, tt.args.Password, tt.args.Timestamp, tt.args.Nonce,
			tt.args.Sign)
		ast.Equal(got, tt.wantCode, tt.name)
		ast.Equal(err != nil, tt.wantErr, tt.name)
	}
}

然后我们为了让测试用例TestCheckSignature通过, 实现CheckLoginSignature方法

代码语言:go
复制
// CheckSignature 签名检查
// MD5({phone_number}_{password}_{时间戳},{随机串})
func CheckLoginSignature(phoneNumber, password, reqTimestamp, nonce, sign string) (int, error) {
	sourceStr := phoneNumber + pkg.Underline + password + pkg.Underline + reqTimestamp
	signStr := pkg.MD5(sourceStr, nonce)
	fmt.Sprint("sign:" + signStr)
	if signStr != sign {
		return pkg.LoginSignCheckFailed.Code, errors.New(pkg.LoginSignCheckFailed.Msg)
	}
	return pkg.Success.Code, nil
}

当把内部子方法都使用TDD的方式实现后, 再实现LoginPwdService里面的具体调用, 最后执行TestLoginPwdService完成整个接口的单元测试.

step5. 执行所有单元测试

在项目目录下, 本地执行go test .../, 查看是否有没有覆盖到的地方, 我这里可以看到文件100%覆盖了.

这并不说明我们的代码绝对没有问题, 只能说明我们的代码相对简洁

image.png
image.png
代码语言:shell
复制
# 生成html报告
go test -coverprofile=c.out  ./...
go tool cover -html=c.out -o coverage.html
image.png
image.png

step6. 一键执行所有的接口测试用例

接口测试报告:

image.png
image.png

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • step2: 编写接口测试用例
  • step3. 编写单元测试用例
  • step4. 实现LoginPwdService
    • 遇到子方法需要独立实现咋么办?
    • step5. 执行所有单元测试
    • step6. 一键执行所有的接口测试用例
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档