Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Go项目实战-学会对代码逻辑层进行BDD测试

Go项目实战-学会对代码逻辑层进行BDD测试

作者头像
KevinYan
发布于 2025-05-15 05:39:52
发布于 2025-05-15 05:39:52
9500
代码可运行
举报
文章被收录于专栏:网管叨bi叨网管叨bi叨
运行总次数:0
代码可运行

前面两节我们的单元测试主要集中在对项目基础设施层的代码进行单元测试,针对Dao数据操作层我们讲解了如何在不实际对项目数据库进行CURD的情况下使用了sqlmock的方式进行单元测试。而对于外部API对接层则是教会大家用gock实现无侵入的HTTP Mock,对有API请求的代码进行单元测试。

今天我们更进一步,从项目代码的基础设施层来到逻辑层和用户接口层。逻辑层的代码肯定更注重逻辑,所以我们在这里会引入goconvey 这个库实现,让它帮助我们实现BDD(行为驱动测试),goconvey支持树形结构方便构造各种场景,让我们能更容易地基于 goconvey 来组织的单测。本文大纲如下:

goconvey 的 安装命令如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
go get github.com/smartystreets/goconvey 

输入命令后,安装过程如下所示:

关于goconvey的使用方法详解,这里就不在给大家举简单的例子进行说明了,还是按照前面几篇的风格,给大家提供一个我在公众号上写的 goconvey 入门详解。

  • 使用 Go Convey 做BDD测试的入门指南

逻辑层单元测试实战

我们项目各业务的核心逻辑都主要集中在领域服务 domainservice 中,按照我们为项目做的的单元测试目录规划,它的单元测试_test.go 文件都应该放在test/domainservice 目录中。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
.
|---test
|     |---controller    # controller 的测试用例
|     |---dao    # dao 的测试用例
|     |---domainservice # 逻辑层领域服务的测试用例
|     |---library # 外部API对接的测试用例 

TestMain 入口设置

依照惯例,在每个要写单元测试的package中,我门都需要在包内测试的统一入口TestMain中做一些公共基础性的工作。

我们在TestMain中加上Convey 的SuppressConsoleStatistics和PrintConsoleStatistics,用于在测试完成后输出测试结果.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package domainservice

import (
    "testing"
    . "github.com/smartystreets/goconvey/convey"
)

func TestMain(m *testing.M) {
// convey在TestMain场景下的入口
 SuppressConsoleStatistics()
 result := m.Run()
// convey在TestMain场景下的结果打印
 PrintConsoleStatistics()
 os.Exit(result)
}

这么设置后,输出的测试结果会按照单测中Convey书写的层级分层级显示,这个输出结果我会在下面的实战案例中展示给大家。

注意这里convey包的导入方式使用了 import . 的语法,import . "github.com/smartystreets/goconvey/convey",这样是为了方便大家直接使用 convey 包中的各种定义,无需再像 convey.Convey 这样加包前缀。

实战案例一:密码复杂度的BDD测试

在案例一种我们找一个相对简单的工具函数来演示怎么用convey帮助我们组织用例。我们在用户注册和重设密码种使用过一个检查用户密码复杂度的工具函数。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func PasswordComplexityVerify(s string) bool {
var (
  hasMinLen  = false
  hasUpper   = false
  hasLower   = false
  hasNumber  = false
  hasSpecial = false
 )
iflen(s) >= 8 {
  hasMinLen = true
 }
for _, char := range s {
switch {
case unicode.IsUpper(char):
   hasUpper = true
case unicode.IsLower(char):
   hasLower = true
case unicode.IsNumber(char):
   hasNumber = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
   hasSpecial = true
  }
 }
return hasMinLen && hasUpper && hasLower && hasNumber && hasSpecial
}

接下来我们就给 PasswordComplexityVerify 函数编写测试用例。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func TestPasswordComplexityVerify(t *testing.T) {
 Convey("Given a simple password", t, func() {
  password := "123456"
  Convey("When run it for password complexity checking", func() {
   result := util.PasswordComplexityVerify(password)
   Convey("Then the checking result should be false", func() {
    So(result, ShouldBeFalse)
   })
  })
 })

 Convey("Given a complex password", t, func() {
  password := "123@1~356Wrx"
  Convey("When run it for password complexity checking", func() {
   result := util.PasswordComplexityVerify(password)
   Convey("Then the checking result should be true", func() {
    So(result, ShouldBeTrue)
   })
  })
 })
}

在这个测试函数中,首先我们从正向和负向两个方面对函数进行单元测试,正向测试和负向测试都是什么呢,用通俗易懂的文字解释就是:

  • 正向测试:提供正确的入参,期待被测对象返回正确的结果。
  • 负向测试:提供错误的入惨,期待被测对象返回错误的结果或者对应的异常。

通过这个例子,正好说一下在使用goconvy的过程中需要注意的几个点:

  • Convey 可以嵌套的,这样我们就可以构造出来一条测试的场景路径,帮助我们写出BDD风格的单测。
  • Convey 嵌套使用时函数的参数有区别
    • 最上层Convey 为Convey(description string, t *testing.T, action func())
    • 其他层级的嵌套 Convey 不需要传入 *testing.T,为Convey(description string, action func())

结合我们在 description 参数中的描述,我们就可以建立起来类似 BDD (行为驱动测试)的语义:

  • Given【给定某些初始条件】
    • Given a simple passowrd 给定一个简单密码
  • When 【当一些动作发生后】
    • When run it for password complexity checking 当对它进行复杂度检查时
  • Then 【结果应该是】
    • Then the checking result should be false 结果应该是 false

BDD测试中的描述信息通常使用的是Given、When、Then引导的状语从句,如果喜欢用中文写描述信息也要记得使用类似语境的句子。

咱们用 go test -v 命令来看看测试运行的效果,我们可以看到输出的测试结果会按照单测中Convey书写的层级,分层级显示。

实战案例二:用户注册的BDD测试

通过上面一个相对简单的例子,相信大家对goconvey库的使用已经有所了解,那么接下来我们再来看一下,怎么为逻辑层中那些复杂的代码逻辑编写单元测试。

我选用的是用户注册的领域服务方法,来给大家展示为业务逻辑代码编写单元测试,整个测试用 goconvey 组织用例的行为路径,使用 gomonkey 对 RegisterUser 方法中依赖的其他方法进行Mock,整个测试方法的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func TestUserDomainSvc_RegisterUser(t *testing.T) {
 Convey("Given a user for RegisterUser of UserDomainSvc", t, func() {
  givenUser := &do.UserBaseInfo{
   Nickname:  "Kevin",
   LoginName: "kevin@go-mall.com",
   Verified:  0,
   Avatar:    "",
   Slogan:    "Keep tang ping",
   IsBlocked: 0,
   CreatedAt: time.Date(2025, 1, 31, 23, 28, 0, 0, time.Local),
   UpdatedAt: time.Date(2025, 1, 31, 23, 28, 0, 0, time.Local),
  }
  planPassword := "123@1~356Wrx"
var s *dao.UserDao
// 让UserDao的CreateUser返回Mock数据
  gomonkey.ApplyMethod(s, "CreateUser", func(_ *dao.UserDao, user *do.UserBaseInfo, password string) (*model.User, error) {
   passwordHash, _ := util.BcryptPassword(planPassword)
   userResult := &model.User{
    ID:        1,
    Nickname:  givenUser.Nickname,
    LoginName: givenUser.LoginName,
    Verified:  givenUser.Verified,
    Password:  passwordHash,
    Avatar:    givenUser.Avatar,
    Slogan:    givenUser.Slogan,
    CreatedAt: givenUser.CreatedAt,
    UpdatedAt: givenUser.UpdatedAt,
   }
   return userResult, nil
  })

  Convey("When the login name of user is not occupied", func() {
   gomonkey.ApplyMethod(s, "FindUserByLoginNam", func(_ *dao.UserDao, loginName string) (*model.User, error) {
    returnnew(model.User), nil
   })
   Convey("Then user should be created successfully", func() {
    user, err := domainservice.NewUserDomainSvc(context.TODO()).RegisterUser(givenUser, planPassword)
    So(err, ShouldBeNil)
    So(user.ID, ShouldEqual, 1)
    So(user, ShouldEqual, givenUser)
   })
  })

  Convey("When the login name of user has already been occupied by other users", func() {
   gomonkey.ApplyMethod(s, "FindUserByLoginNam", func(_ *dao.UserDao, loginName string) (*model.User, error) {
    return &model.User{LoginName: givenUser.LoginName}, nil
   })
   Convey("Then the user's registration should be unsuccessful", func() {
    user, err := domainservice.NewUserDomainSvc(context.TODO()).RegisterUser(givenUser, planPassword)
    So(user, ShouldBeNil)
    So(err, ShouldNotBeNil)
    So(err, ShouldEqual, errcode.ErrUserNameOccupied)
   })
  })
 })
}

在这个测试方法中,我在顶层Convey中嵌套了两个并列的Convey方法来组织正向和负向的单元测试,之所以不跟上面那个案例一样写两个并列的顶层Convey方法是因为被测方法 RegisterUser 的入参数太难构造,这也正好给大家展示了我们使用Convey设计单元测试的行为路径时的灵活性。

这里我们提供了两个测试用例,正向用例中让 RegisterUser 依赖的Dao方法 CreateUser 返回创建成功的结果,预期 RegisterUser 返回正确的结果。

而负向用例中则让 CreateUser 返回用户名在数据库中已存在时返回的结果,同时预期 RegisterUser 会返回用户名已被占用的错误 errcode.ErrUserNameOccupied 。

最后咱们用 go test -v命令来看看测试运行的效果

Controller 的单元测试

到现在为止我们的单元测试实战案例已经覆盖了数据访问Dao层、API对接层和领域服务层。还剩下一个用户接口层没有涉及到,即项目的Controller方法该怎么做单元测试呢?

首先我觉得,按照我们项目的分层架构来说Controller是负责接受和验证请求和调用下层拿到结果返回响应的,在这里包含核心业务逻辑。如果我们能把它依赖的下层的单元测试做到位,Controller的单元测试可以不做。

不过我们知道有个验证项目质量的数据指标叫:测试覆盖率,这个指标肯定越高越好,所以这里我在简单地把Controller 处理函数的单元测试给大家过一下。

在 Web 项目中 Controller 里都是API接口的请求处理函数,为它们编写单元测试需要用到Go自带的net/http/httptest包, 它可以mock一个HTTP请求和响应记录器,让我们的 server 端接收并处理我们 mock 的HTTP请求,同时使用响应记录器来记录 server 端返回的响应内容。

这里我们那用户登陆这个接口给大家演示它的Controller函数是怎么做单元测试的,它的单元测试如下。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func TestLoginUser(t *testing.T) {
 Convey("Given right login name and password", t, func() {
  loginName := "yourName@go-mall.com"
  password := "12Qa@6783Wxf3~!45"

  Convey("When use them to Login through API /user/login", func() {
   var s *appservice.UserAppSvc
   gomonkey.ApplyMethod(s, "UserLogin", func(_ *appservice.UserAppSvc, _ *request.UserLogin) (*reply.TokenReply, error) {
    LoginReply := &reply.TokenReply{
     AccessToken:   "70624d19b6644b0bbf8169f51fb5a91f132edebc",
     RefreshToken:  "d16e22fef5cb7f6c69355c9a3c6ce8d1d3b37a84",
     Duration:      7200,
     SrvCreateTime: "2025-02-01 15:34:35",
    }
    return LoginReply, nil
   })

   var b bytes.Buffer
   json.NewEncoder(&b).Encode(map[string]string{"login_name": loginName, "password": password})
   req := httptest.NewRequest(http.MethodPost, "/user/login", &b)
   req.Header.Set("platform", "H5")
   gin.SetMode(gin.ReleaseMode) // 不让它在控制台里输出路由信息
   g := gin.New()
   router.RegisterRoutes(g)
   // mock一个响应记录器
   w := httptest.NewRecorder()
   // 让server端处理mock请求并记录返回的响应内容
   g.ServeHTTP(w, req)

   Convey("Then the user will login successfully", func() {
    So(w.Code, ShouldEqual, http.StatusOK)
    // 检验响应内容是否复合预期
    var resp map[string]interface{}
    json.Unmarshal([]byte(w.Body.String()), &resp)
    respData := resp["data"].(map[string]interface{})
    So(respData["access_token"], ShouldNotBeEmpty)
   })
  })
 })

在这个单元测试中我们还是会用 goconvey来组织测试的行为路径,用 gomonkey 给Controller函数调用的应用服务方法做打桩返回Mock结果,不然就跟用POSTMAN 请求接口一样咧,那样的话如果下层代码里有数据库CURD更新之类操作的话还是会去实际访问数据库的,这显然不是我们想要的。

对于Controller方法的验证主要聚焦于请求参数的验证以及响应结果的验证,因为 Controller 在我们项目的分层设计中就只干这两件事。

总结

通过这几节单元测试实战的内容大家应该能体会到,我们为项目做好分层设计的一个优点--好测试。每个分层都有具体的职责,每块代码的边界不至于过大,这样我们做单元测试代码写起来会更简单。如果把所有逻辑都耦合在Controller 函数那种代码,写单元测试的难度先不说,有效性也很难保证,因为测试的颗粒度太大必然导致很难测出代码内部的问题。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-05-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 网管叨bi叨 微信公众号,前往查看

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

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Go项目实战--数据Dao层代码的单元测试实战
上节课我给大家介绍了怎么给Go项目做单元测试的规划,当然这里仅限于跟咱们课程里的实战项目一样分层架构设计做的还可以的项目哦,要是所有逻辑都耦合在Controller里,那这个规划就不适用了。。。,所有逻辑都耦合在Controller里还做个锤子的单元测试,直接上线让用户给你测(手机系统都能这么干的。。。你们怕啥)
KevinYan
2025/04/23
1030
Go项目实战--数据Dao层代码的单元测试实战
白话Golang单元测试
最近学习某个 Golang 单元测试的课程,发现其中推荐使用 gomonkey 这种黑科技,让人略感意外,毕竟在软件开发领域,诸如依赖注入之类的概念已经流传了几十年了,本文希望通过一个例子的演化过程,来总结出 Golang 单元测试的最佳实战。
LA0WAN9
2021/12/14
5180
从头到脚说单测——谈有效的单元测试
导语 非常幸运的是,从4月份至今,我能够全身心投入到腾讯新闻的单元测试专项任务中,从无知懵懂,到不断深入理解的过程,与开发同学互帮互助,受益匪浅。在此过程中,得到了质量总监、新闻总监和乔帮主的倾囊指导,真心感谢!!我希望把所有心得,总结成一篇较为全面的文章,分享给其他团队。时刻牢记:1. 不要滥用mock 2. 基于意图。 在我们谈到单元测试,大都清楚是测试函数符合预期,国外很多大公司都将单测执行的很好,国内成功的案例则相对有限。在本文中,笔者将在腾讯新闻项目中亲身经历单测从无到有的实践过程梳理为可读
腾讯技术工程官方号
2019/08/20
11.7K0
从头到脚说单测——谈有效的单元测试
深入解析GoConvey:Go测试工具的新选择
本文将为您详细介绍GoConvey这款在GitHub上广受欢迎的Go语言测试工具,尤其是它相对于Go标准库的testing库的优势,以及它们在定位上的不同。
运维开发王义杰
2023/08/10
7260
深入解析GoConvey:Go测试工具的新选择
一文了解一线互联网大厂的 Golang 单测最佳实战经验
Go 单测里面,最常见的就是通过 gomonkey(stub) 打桩或者 mocker(mock) 的模拟来替换掉我们原本的执行逻辑,因此首先我们要对这两种方式有一个比较深入的理解,要理解为何 Go 单测的时候能够替换掉原来的方法!!!
Allen.Wu
2023/03/01
2.7K0
一文了解一线互联网大厂的 Golang 单测最佳实战经验
Golang 单元测试详尽指引
文末有彩蛋。 作者:yukkizhang,腾讯 CSIG 专项技术测试工程师 本篇文章站在测试的角度,旨在给行业平台乃至其他团队的开发同学,进行一定程度的单元测试指引,让其能够快速的明确单元测试的方式方法。 本文主要从单元测试出发,对Golang的单元测试框架、Stub/Mock框架进行简单的介绍和选型推荐,列举出几种针对于Mock场景的最佳实践,并以具体代码示例进行说明。 一、单元测试 1. 单元测试是什么 单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向
腾讯技术工程官方号
2020/10/26
4.7K0
Go语言单元测试
学习Golang的时候遇到了一些单元测试问题,发现有些工具是真的好用,就记录在此,主要包括monkey、convey,还有数据库Mock等。
有财君
2023/03/21
7230
Go语言单元测试
Go:测试库(GoConvey,testify,GoStub,GoMonkey)对比及简介
assert库是这样的一个库,它有一系列函数来适应各种各样不同的场景需求,下面是一个简单的判断值是否符合预期的demo:
Freedom123
2024/03/29
6450
Go:测试库(GoConvey,testify,GoStub,GoMonkey)对比及简介
Golang 单元测试合集整理,(我最常用 gomonkey)欢迎收藏
无论写什么样的语言,单元测试都是必不可少的,它可以极大的提高我们的代码质量,减少各种低级错误和 bug
阿兵云原生
2023/09/14
2.1K0
Golang 单元测试合集整理,(我最常用 gomonkey)欢迎收藏
每天坚持20分钟编写测试
go tool cover -html=cover.out -o coverage_xxxxx.html
李子健
2022/07/06
1290
手把手教你如何进行 Golang 单元测试
点击上方蓝字,发现更多精彩 导语 本篇是对单元测试的一个总结,通过完整的单元测试手把手教学,能够让刚接触单元测试的开发者从整体上了解一个单元测试编写的全过程。最终通过两个问题,也能让写过单元测试的开发者收获单测执行时的一些底层细节知识。 引入 随着工程化开发在司内大力的推广,单元测试越来越受到广大开发者的重视。在学习的过程中,发现网上针对 Golang 单元测试大多从理论角度出发介绍,缺乏完整的实例说明,晦涩难懂的 API 让初学接触者难以下手。 本篇不准备大而全的谈论单元测试、笼统的介绍 Golang
腾讯VTeam技术团队
2021/06/02
1.5K0
一文说尽Golang单元测试实战的那些事儿
导语 | 单元测试,通常是单独测试一个方法、类或函数,让开发者确信自己的代码在按预期运行,为确保代码可以测试且测试易于维护。腾讯后台开发工程师张力结合了公司级漏洞扫描系统洞犀在DevOps上探索的经验,以Golang为例,列举了编写单元测试需要的工具和方法,然后针对写单测遇到的各种依赖问题,详细介绍了通过Mock的方式解决各种常用依赖,方便读者在写go语言UT的时候,遇到依赖问题,能够快速找到解决方案。最后再和大家探讨一下关于单元测试上的一些思考。 一、前言 单元测试,通常是单独测试一个方法、类或函数
腾讯云开发者
2021/07/28
1.4K0
Golang单元测试
Go提供了test工具用于代码的单元测试,test工具会查找包下以_test.go结尾的文件,调用测试文件中以 Test或Benchmark开头的函数并给出运行结果
仙人技术
2021/08/31
8340
Golang单元测试
Go项目实战-代码里有API调用时单元测试怎么做?
上一节课我给大家展示了项目Dao层里的CURD操作我们该怎么做单元测试,尤其是Insert、Update这些需要对数据库进行更改的操作怎么用sqlMock的方式让我们既不用真正对数据库发起操作也能验证这些操作是否符合预期。
KevinYan
2025/05/07
1110
Go项目实战-代码里有API调用时单元测试怎么做?
golang测试框架goconvey的使用
前面我们介绍了golang测试框架里面的testify, 下面让了解一下另一个用的也比较多的断言框架goconvey。
Johns
2021/08/31
3.9K0
从头到脚说单测——谈有效的单元测试(上篇)
作 者 杨迪,腾讯PCG高级工程师 商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。 作者导语 从4月份至今,我能够全身心投入到腾讯新闻的单元测试专项任务中,从无知懵懂,到不断深入理解的过程,与开发同学互帮互助,受益匪浅。在此过程中,得到了质量总监等等优秀同事的倾囊指导,真心感谢!!我希望把所有心得,总结成一篇较为全面的文章,分享给其他团队。时刻牢记:1. 不要滥用mock 2. 基于意图。 一. 为单元测试“正名” 我曾经认为,单元测试面向的是一个函数。任何走出一个函数的测试,
WeTest质量开放平台团队
2019/08/15
2.6K0
从头到脚说单测——谈有效的单元测试(上篇)
走近微服务,第4部分:使用GoConvey进行测试和模拟
应该如何测试微服务?在为这个特定领域制定测试方案时,需要考虑哪些特别的挑战?在本博客系列的第4部分中,我们将一窥究竟。
用户2176511
2018/06/20
3.5K0
走近微服务,第4部分:使用GoConvey进行测试和模拟
Golang 测试教程
How to write test with golang 代码示例 TDD(Test-Driven development) 测试驱动开发 内置的 testing 库 、 表格驱动、样本测试、TestMain 第三方:goconvey Monkey 猴子补丁 数据库 mock travisCI 代码覆盖率 TDD 快速实现功能 再设计和重构 软件测试 在指定的条件下,操作程序,发现程序错误 单元测试 对软件的组成单元进行测试,最小单位:函数 包含三个步骤: 指定输入 指定预期 函数结果和指定的预期
谢伟
2019/03/11
1.7K0
Golang 测试教程
『Go 语言学习专栏』-- 第十一期
12.png golang-11.png 大家好,我叫谢伟,是一名程序员。 最近更新不是很频繁,主要是我手头有好些事需要解决,比如更换环境,比如出去见识人,以便更好的认识自己,知道自己的短板在哪。 很早之前我就意识到:每隔半年需要出去走走,哪怕不是真的更换工作,你也应该出去走走,去市场检验一下自己是否在对应的岗位有竞争力,你的市场价位是多少。 好,本节的主题是:单元测试。 测试其实分很多种,就我在企业中的认识,一般称为测试工程师,从事的应该是所谓的集成测试(或者说是 AC测试(Acceptance C
谢伟
2018/06/06
5540
使用 gomonkey Mock 函数及方法
在 Golang 语言中,写单元测试的时候,不可避免的会涉及到对其他函数及方法的 Mock,即在假设其他函数及方法响应预期结果的同时,校验被测函数的响应是否符合预期。
CG国斌
2022/06/05
2.6K0
相关推荐
Go项目实战--数据Dao层代码的单元测试实战
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验