测试
测试是避免事故的最后一道屏障
测试一般分为
- 回归测试一般是QA同学手动通过终端回归一些固定的主流程场景
- 集成测试是对系统功能维度做测试验证
- 单元测试测试开发阶段,开发者对单独的函数、模块做功能验证
层级从上至下,测试成本逐渐减低,而测试覆盖率确逐步上升,所以单元测试的覆盖率一定程度上决定这代码的质量。
单元测试
单元测试主要包括
- 输入
- 测试单元
- 输出
- 校对
单元的概念比较广,包括接口,函数,模块等;
用最后的校对来保证代码的功能与我们的预期相符;
单元测试一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上既保证了新功能本身的正确性,又未破坏原有代码的正确性。另一方面可以提升效率,在代码有bug的情况下,通过编写单元测试,可以在一个较短周期内定位和修复问题。
规则
-
所有测试文件以_test.go结尾
-
命令规范:func TestXxx(*testing.T)
-
初始化逻辑放在TestMain中
例子
运行
go test [flags] [packages]
assert
覆盖率
通过代码覆盖率
- 衡量代码是否经过了足够的测试
- 评价项目的测试水准
- 评估项目是否达到了高水准测试等级
这是有一个判断是否及格的函数,超过60分,返回true,否则返回false,第二个是对输入为70 的单元测试,我们执行第二个的单测,通过指定cover参数,我们看输出了覆盖率为66.7%。一共三行,我们的单测试执行了2行,所以为66.7%
下一步就是提升覆盖率,我们可以增加一个不及格的测试case,重新执行所有单侧,最终覆盖率为100%
Tips
- 在实际项目中,一般的要求是50%~60%覆盖率,而对于资金型服务,覆盖率可能要求达到80%+;
- 测试分支相互独立,全面覆盖
- 要求函数体足够小,这样就比较简单的提升覆盖率,也符合函数设计的单一职责。
依赖
外部依赖⇒稳定&幂等
工程中复杂的项目,一般会依赖File、DB、Cache,而我们的单测需要保证稳定性和幂等性:
- 稳定是指相互隔离,能在任何时间,任何环境,运行测试。
- 幂等是指每一次测试运行都应该产生与之前一样的结果。
而要实现这一目的就要用到mock机制。
文件处理
下面举个栗子,将文件中的第一行字符串中的11替换成00,执行单测,测试通过,而我们的单测需要依赖本地的文件,如果文件被修改或者删除测试就会fail。为了保证测试case的稳定性,我们对读取文件函数进行mock,屏蔽对于文件的依赖。
Mock
这里我们用了Monkey,monkey是一个开源的mock测试库,可以对method,或者实例的方法进行mock。
Mockey Patch 的作用域在 Runtime,在运行时通过通过 Go 的 unsafe 包,能够将内存中函数的地址替换为运行时函数的地址。 ,将待打桩函数或方法的实现跳转到。
快速Mock函数
- 为一个函数打桩
- 为一个方法打桩
下面是一个mock的使用样例,通过patch对ReadFirstLine进行打桩mock,默认返回line110,这里通过defer卸载mock,这样整个测试函数就摆脱了本地文件的束缚和依赖。
对ReadFirstLine打桩测试,不在依赖本地文件
基准测试
Go 语言还提供了基准测试框架,基准测试是指测试一段程序的运行性能及耗费 CPU 的程度。而我们在实际项目开发中,经常会遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析,这就用到了基准测试。使用方法类似于单元测试。
- 优化代码,需要对当前代码分析
- 内置的测试框架提供了基准测试的能力
例子
这里举一个服务器负载均衡的例子,首先我们有10个服务器列表,每次随机执行select函数随机选择一个执行。
基准测试以Benchmark开头,入参是testing.B, 用b中的N值反复递增循环测试 (对一个测试用例的默认测试时间是 1 秒,当测试用例函数返回时还不到 1 秒,那么 testing.B 中的 N 值将按 1、2、5、10、20、50……递增,并以递增后的值重新进行用例函数测试。) ResetTimer重置计时器,我们再reset之前做了init或其他的准备操作,这些操作不应该作为基准测试的范围;runparallel是多协程并发测试;执行2个基准测试,发现代码在并发情况下存在劣化,主要原因是rand为了保证全局的随机性和并发安全,持有了一把全局锁。
优化
为了解决这一随机性能问题,字节开源了一个高性能随机数方法fastrand;我们这边再做一下基准测试,性能提升了百倍。主要的思路是牺牲了一定的数列一致性,在大多数场景是适用的,在后面遇到随机的场景可以尝试用一下。
项目实战
需求背景
以下面页面为例,页面的功能包括话题详情,回帖列表,支持回帖,点赞,和回帖回复,我们今天就以此为需求模型,开发一个该页面所涉及的服务端小功能。
需求描述
社区话题页面
- 展示话题(标题,文字描述)和回帖列表
- 暂不考虑前端页面实现,仅仅实现一个本地web服务
- 话题和回帖数据用文件存储
需求用例
我们从用例分析一步步拆解实现,主要涉及21功能点,用户浏览消费,涉及页面的展示,包括话题内容和回帖的列表,其实从图中我们应该会抽出2个实体
ER图-Entity Relationship Diagram
- 话题
- 帖子
Er图,用来描述现实世界的概念模型。 有了模型实体,属性以及之间的联系,对我们后续做开发就提供了比较清晰的思路。回到需求。两个个实体主要包括,实体的属性,实体的联系*************;有了实体模型,下一步就是思考代码结构设计。我们采用典型的分层结构设计
分层结构
整体分为三层:
- repository数据层:数据层关联底层数据模型,也就是这里的model,封装外部数据的增删改查,我们的数据存储在本地文件,通过文件操作拉取话题,帖子数据;数据层面向逻辑层,对service层透明,屏蔽下游数据差异,也就是不管下游是文件,还是数据库,还是微服务等,对service层的接口模型是不变的。
- service逻辑层:处理核心业务逻辑,计算打包业务实体entiy,对应我们的需求,就是话题页面,包括话题和回帖列表,并上送给视图层;
- controoler视图层:负责处理和外部的交互逻辑,以view视图的形式返回给客户端,对于我们需求,我们封装json格式化的请求结果,api形式访问就好。
组件工具
下面介绍下开发涉及的基础组件和工具,首先是gin,高性能开源的go web框架,我们基于gin 搭建web服务器,这里我们只是简单的使用,主要涉及路由分发,不会涉及其他复杂的概念。
因为我们引入了web框架,所以就涉及go module依赖管理,如前面依赖管理内容讲解,我们首先通过go mod init初始化go mod管理配置文件,然后go get下载gin依赖。
有了框架依赖,我们只需要关注业务本身的实现,从repository→service→controller,我们一步步实现。
Repository
Topic
Post
index
一方面查询我们可以用全扫描遍历的方式,但是这虽然能达到我们的目的,但是并非高效的方式,所以这里引出索引的概念,索引就像书的目录,可以引导我们快速查找定位我们需要的结果;这里我们用map实现内存索引,在服务对外暴露前,利用文件元数据初始化全局内存索引,这样就可以实现O(1)的时间复杂度查找操作。
初始化话题内存索引:首先是打开文件,基于file 初始化scanner,通过迭代器方式遍历数据行,转化为结构体存储至内存map,大家自行实现一下回帖list内存索引吧。
查询
有了内存索引,下一步就是实现查询操作就比较简单了,直接根据查询key获得map中的value就好了,这里用到了sync.once,主要适用高并发的场景下只执行一次的场景,这里的基于once的实现模式就是我们平常说的单例模式,减少存储的浪费。
Service
实体
流程
代码流程编排
可用性
并行处理
关于prepareInfo方法,话题和回帖信息的获取都依赖topicid,这就可以并行执行,提高执行效率。********;大家在后期做项目开发中,一定要思考流程是否可以并行,通过压榨CPU,降低接口耗时,不要一味的串行实现,浪费多核cpu的资源
Controller
这里我们定义一个view对象,通过code msg打包业务状态信息,用data承载业务实体信息,输入*******
Router
- 初始化数据索引
- 初始化引擎配置
- 构建路由
- 启动服务
运行
最后执行go run 本地启动web服务,通过curl命令请求服务暴露的接口****