首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >TDD案例实战

TDD案例实战

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

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

step1: 需求分析/任务拆分

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

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

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

image.png
image.png

step2: 编写接口测试用例

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

image.png
image.png

step3. 编写单元测试用例

  • 测试用例上的通用信息进行封装
代码语言:go
AI代码解释
复制
  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
AI代码解释
复制
  // 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
AI代码解释
复制
// 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
AI代码解释
复制
// 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
AI代码解释
复制
// 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
AI代码解释
复制
# 生成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 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
工作面试必备:SQL 中的各种连接 JOIN 的区别总结!
尽管大多数开发者在日常工作中经常用到Join操作,如Inner Join、Left Join、Right Join等,但在面对特定查询需求时,选择哪种Join类型以及如何使用On和Where子句往往成为一大挑战。特别是对于初学者而言,这些概念可能显得抽象且难以区分。在实际应用中,错误地使用Join类型或混淆On与Where子句的功能,都可能导致数据查询结果的不准确或遗漏关键信息。
追逐时光者
2025/03/20
2110
工作面试必备:SQL 中的各种连接 JOIN 的区别总结!
sql连接查询(inner join、full join、left join、 right join)
需求:查询哪个顾客(customer_name)在哪一天(create_time)消费了多少钱(money)
用户2409797
2018/08/30
7.9K0
sql连接查询(inner join、full join、left join、 right join)
inner join、outer join、right join、left join 之间的区别
一、sql的left join 、right join 、inner join之间的区别   left join(左联接) 返回包括左表中的所有记录和右表中联结字段相等的记录    right join(右联接) 返回包括右表中的所有记录和左表中联结字段相等的记录   inner join(等值连接) 只返回两个表中联结字段相等的行        outer join(外连接) 可分为左外连接left outer join和右外连接right outer join
静谧的小码农
2019/07/04
5.1K0
面试官:left join后用on还是where?区别真的很大!
“写 SQL 查询时,你是否也曾纠结过,条件到底该放在 JOIN 上还是 WHERE 上?每次写到这里,脑海中都会浮现出这两个选项的优劣对比,就像是在选择两个看似相似却又略有不同的武器,打击目标的效率可能会因此大相径庭。那么,这两者到底有何不同?今天,我们就来一探究竟。”
一只牛博
2025/05/31
710
面试官:left join后用on还是where?区别真的很大!
SQL语句中 left join 后用 on 还是 where,区别大了!
前天写SQL时本想通过 A left B join on and 后面的条件来使查出的两条记录变成一条,奈何发现还是有两条。
捡田螺的小男孩
2021/06/15
1.3K0
SQL语句中 left join 后用 on 还是 where,区别大了!
SQLserver数据库之基本增删改查操作(2)
1.新增操作 --插入单行数据 insert into 表名(列名) values (列值) insert into Department(DepName) values(''); --直接拿现有表数据创建一个新表并填充 select 新建表列名 into 新建表名 from 原表名 select EmpId,EmpName into student from Employee; --将现有表数据添加到一个已有表 insert into 已有的新表(列名) select 原表列名 from 原
闻语博客
2021/01/21
1.1K0
MySQL多表联合查询
例 2:查询 tb_course 表中的 id 字段和 tb_students_info 表中的 course_id 字段相等的内容
Alone-林
2022/08/20
10.9K0
详解SQL Server连接(内连接、外连接、交叉连接)
在查询多个表时,我们经常会用“连接查询”。连接是关系数据库模型的主要特点,也是它区别于其它类型数据库管理系统的一个标志。
程序猿小亮
2021/01/28
4.6K0
京东面试官问:LEFT JOIN 关联表中用 ON 还是 WHERE 跟条件有什么区别?
之前有码友去京东面试,被问到 LEFT JOIN 关联表中用 ON 还是 WHERE 跟条件有什么区别,很快就答出来了,可是追问什么原因造成这一情况的,一时没回答上来。
JAVA葵花宝典
2021/05/11
4500
京东面试官问:LEFT JOIN 关联表中用 ON 还是 WHERE 跟条件有什么区别?
SQL中 inner join、left join、right join、full join 到底怎么选?详解来了
作为一名CURD工程师,联表查询应该就算是一项相对复杂的工作了吧,如果表结构复杂一点,左一连,右一连,内一连再加上外一连,很可能就被绕晕的,最终得到的数据集就不是自己理想中的结果;
一行Java
2022/04/07
1.1K0
SQL中 inner join、left join、right join、full join 到底怎么选?详解来了
工作总结之因为笛卡尔积问题写SQL搞了半天[害](附笛卡尔积总结)
文章目录 背景 需求 解决过程 结果 多表连接简介 背景 管控组同事反馈:宿舍总数异常,加起来的间数比深圳市人口都多,无疑数据是异常的 需求 使宿舍数据恢复正常。 解决过程 尝试过左连接,右连
Maynor
2022/06/12
1.8K0
工作总结之因为笛卡尔积问题写SQL搞了半天[害](附笛卡尔积总结)
Join,left join,right join(1)--连接原理(三十九)
前面说了mysql优化器访问数据库的方法有const,ref,ref_or_null,range,index,all。然后又分为条件全部是索引回表查询,和条件有非索引查询,则需要回表之后,在过滤。又有intersection合并索引和union并集索引,当两个单独二级索引查询,不是联合索引查询,可能会触发这两个索引查询,用and是intersection,用or是union查询,触发有两个注重点:
keying
2022/07/26
5050
【MySQL】多表联合查询、连接查询、子查询「建议收藏」
内连接:[inner] join:从左表中取出每一条记录,去右表中与所有的记录进行匹配: 匹配必须是某个条件在左表中与右表中相同最终才会保留结果,否则不保留.
全栈程序员站长
2022/08/02
5.6K0
【MySQL】多表联合查询、连接查询、子查询「建议收藏」
关于MySQL多表联合查询,你真的会用吗?
上节课给大家介绍了MySQL子查询的基本内容,具体可回顾MySQL子查询的基本使用方法(四),本节课我们准备给大家介绍MySQL的多表联合查询。大家都知道,MySQL多表联合查询包括内连接、外连接、笛卡尔积连接查询三种。今天我们先重点介绍常用的外连接与内连接查询,即left join /right join/inner join的基本用法。
用户7569543
2021/11/02
9.8K0
图解 SQL 里的各种 JOIN
从业以来主要在做客户端,用到的数据库都是表结构比较简单的 SQLite,以我那还给老师一大半的 SQL 水平倒也能对付。现在偶尔需要到后台的 SQL Server 里追查一些数据问题,就显得有点捉襟见肘了,特别是各种 JOIN,有时候傻傻分不清楚,于是索性弄明白并做个记录。
芋道源码
2019/10/29
8860
MySQL数据库基础(十一):多表查询
连接查询可以实现多个表的查询,当查询的字段数据来自不同的表就可以使用连接查询来完成。
Lansonli
2024/03/19
2220
MySQL数据库基础(十一):多表查询
10 分钟,带你彻底掌握 SQL 多表查询(建议收藏)
多表查询,也称为多表连接查询;作为关系型数据库最主要的查询方式,在日常工作中被广泛使用
AirPython
2021/02/05
1.2K0
10 分钟,带你彻底掌握 SQL 多表查询(建议收藏)
如何在 SQL 中使用 LEFT、RIGHT、INNER、OUTER、FULL 和 SELF JOIN?
在进行复杂的分析处理和数据发现时,一个表的数据通常不足以提供重要的见解,因此需要合并多个表。 SQL,作为与关系数据库通信的一种方法,允许您在表之间创建关系.
从大数据到人工智能
2022/03/22
2.4K0
如何在 SQL 中使用 LEFT、RIGHT、INNER、OUTER、FULL 和 SELF JOIN?
高级SQL查询-(聚合查询,分组查询,联合查询)[通俗易懂]
当遇到常见的统计总数、计算平局值等操作,可以使⽤聚合函数来实现,常见的聚合函数有:
全栈程序员站长
2022/09/05
5.2K0
面试官:left join 后用 on 和 where 有什么区别?
哈喽,我是狗哥。前天写 SQL 时本想通过 A left B join on and 后面的条件来使查出的两条记录变成一条,奈何发现还是有两条。
JavaFish
2022/01/17
6500
面试官:left join 后用 on 和 where 有什么区别?
推荐阅读
相关推荐
工作面试必备:SQL 中的各种连接 JOIN 的区别总结!
更多 >
交个朋友
加入腾讯云官网粉丝站
蹲全网底价单品 享第一手活动信息
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档