当前位置:网站首页>TDD案例实战
TDD案例实战
2022-06-28 23:00:00 【Johns】
使用gin框架实现一个简单的手机号密码登录服务step1: 需求分析/任务拆分
step1: 需求分析/任务拆分
这个阶段至少要确认以下内容:
- 使用什么样的协议?
- 输入/输出参数有哪些?
- 其他细节
- 参数验证的规则
- 接口安全策略(签名规则)
需求分析后我们一般会做任务拆分/分解, 然后产出接口文档, 这个阶段一般需要前后端开发,产品,测试共同讨论:
step2: 编写接口测试用例
这个阶段我们主要是针对之前定义好的接口文档, 编写接口测试用例
step3. 编写单元测试用例
- 测试用例上的通用信息进行封装
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
)- 编写测试用例
// 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
// 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的话, 我们需要为这个方法独立设计测试用例:
// 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方法
// 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%覆盖了.
这并不说明我们的代码绝对没有问题, 只能说明我们的代码相对简洁
# 生成html报告
go test -coverprofile=c.out ./...
go tool cover -html=c.out -o coverage.htmlstep6. 一键执行所有的接口测试用例
- 需要注意浏览器时间和服务机器的时钟是否同步 http://127.0.0.1:3000/api/open/run_auto_test?id=18&token=e701e78e60a655867895a7d1e44f7afeeb94b3b2879d83ce8e4ed5c6589e8d4c&mode=html&email=false&download=false
image.png
接口测试报告:
边栏推荐
- Heavyweight! CDA certification test preparation Q & A Online
- After crossing, she said that the multiverse really exists
- Is it safe to open a stock account online?
- Differences among CPU, GPU, TPU and NPU
- 台式机没声音怎么样才能解决
- Career consultation | in the data analysis interview, it is only reliable to introduce yourself in this way
- hiredis的代码示例
- Simple understanding of counting and sorting
- Zadig + sonarqube, ensuring the safety of the development process
- 长投学堂帮忙开证券账户是安全靠谱的吗?个人如何开
猜你喜欢

How to solve the problem of desktop without sound

【深度学习】(2) Transformer 网络解析,代码复现,附Pytorch完整代码
![[deep learning] (3) encoder mechanism in transformer, complete pytoch code attached](/img/cb/d385bee7a229e8d11f5fa8af66311f.gif)
[deep learning] (3) encoder mechanism in transformer, complete pytoch code attached

微搭低代码中实现二维码生成

邂逅阿维塔 11:强产品力下久违的新鲜感

Ansible production environment usage scenario (7): batch deployment of elk clients

DBNN实验进展

Windows mysql5.7 enable binlog log

分享im即时通讯开发之WebSocket:概念、原理、易错常识

What does project management really manage?
随机推荐
Qt5.15中qsrand,srand随机数生成函数已弃用问题
00 后云原生工程师:用 Zadig 为思创科技(广州公交)研发开源节流
How powerful is the Zadig build? Practice together
Zadig + cave Iast: let safety dissolve in continuous delivery
全面掌握const的用法《一》
国盛证券开户是真的安全可靠吗
LeCun预言AGI:大模型和强化学习都是斜道!我的世界模型才是新路
Is it safe and reliable for changtou school to help open a securities account? How to drive
Tanghongbin, Yaya live CTO: to truly localize, the product should not have the attribute of "origin"
Windows mysql5.7 enable binlog log
Oracle删除归档日志及添加定时任务
Code example of hiredis
Hit the industry directly | the flying propeller launched the industry's first model selection tool
Research Report on workers: middle-aged people account for the highest proportion of naked words
【深度学习】(3) Transformer 中的 Encoder 机制,附Pytorch完整代码
How to solve the problem of desktop without sound
Zadig + sonarqube, ensuring the safety of the development process
LeetCode 324 摆动排序 II[排序 双指针] HERODING的LeetCode之路
Powerful open source API interface visual management platform Yapi
Progress of dbnn experiment