这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
03测试
- 回归测试 回归测试是测试已经通过测试的软件在更改后是否仍能正常工作。
- 继承测试 集成测试是在将多个模块组合成系统后进行的测试。
- 单元测试 单元测试是对软件中的独立单元进行的测试。
3.1 单元测试
3.1.1 单元测试-规则
- 所有测试文件以_test.go结尾。
- func TestXxx(*testing.T)。
- 初始化逻辑放到TestMain中。
func TestMain(m *testing.M) {
//测试前:数据装载、配置初始化等前置工作
code := m.Run()
//测试后:释放资源等收尾工作
}
3.1.2 单元测试-例子
func HelloTom() string {
return "Jerry"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expected %s do not match actual %s", expectOutput, output)
}
}
3.1.3 单元测试-运行
go test [flags][packages]
3.1.4 单元测试-assert
"github.com/stretchr/testify/assert"
3.1.5 单元测试-覆盖率
- 衡量代码是否经过了足够的测试。
- 评价项目的测试水准
- 评估项目达到的水准测试等级
go test project_name_test.go --cover
// 返回时间、覆盖率(代码行数百分比)
- 一般覆盖率:50%~60%,较高覆盖率80%。
- 测试分支相互独立、全面覆盖。
- 测试单元粒度足够小,函数单一职责。
3.2 单元测试-依赖
外部依赖=>稳定&幂等 幂等:输入一个值,每次返回的值都一样。 稳定:独立,解耦。
3.3 单元测试-文件处理
为了防止测试函数时,对本地文件进行破坏/更改。 使用下面的mock函数进行测试。
3.4 单元测试-Mock
快速Mock函数
monkey:https://github.com/bouk/monkey
- 为一个函数打桩
- 为一个方法打桩
3.5 基准测试
- 优化代码,需要对当前代码分析
- 内置的测试框架提供了基准测试的能力
3.5.1 基准测试-例子
下面是一个简单的 Go 基准测试示例:
package main
import (
"fmt"
"testing"
)
func BenchmarkHello(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Sprintf("hello")
}
}
运行这个基准测试,可以使用 go test -bench=. 命令,它会运行所有的基准测试,输出的每一行都是一个测试的结果。
这个例子中,我们测试了 fmt.Sprintf("hello") 的执行效率。可以看到,b.N 会被设置为一个足够大的值,使得整个函数执行足够长的时间,以便于得出合理的结果。
3.5.2 基准测试-运行
3.5.3 基准测试-优化
fastrand在高并发场景更快
04 项目实践
- 需求设计
- 代码开发
- 测试运行
4.1 需求描述
社区话题页面
- 展示话题(标题,文字描述)和回帖列表
- 暂不考虑前端页面实现,仅仅实现一个本地web服务
- 话题和回帖数据用文件储存
4.2 需求用例
浏览消费用户
- User → Topic
- User → PostList
4.3 ER图-Entity Relationship Diagram
Topic
- id
- title
- content
- create_time Post
- id
- topic_id
- content
- create_time
4.4 分层结构
- 数据层:数据Model,外部数据的增删改查。
- 逻辑层:业务Entity,处理核心业务逻辑输出。
- 视图层:视图view,处理和外部的交互逻辑。
File → Repository → Service → Controller → Client
4.5 组件工具
- Gin高性能go web框架
https://github.com/gin-gonic/gin#installation
- Go mod
// 初始化gomod文件
go mod init
// 添加gin依赖
go get gopkg.in/gin-gonic/gin.v1@v1.3.0
4.6 Repository-index
将数据行定义为内存的map,实现快速索引。
var (
topicIndexMap map[int64]*Topic
postIndexMap map[int64][]*Post
)
//初始化话题数据索引
func initTopicIndexMap(filePath string) error {
open, err :=os.Open(filePath + "topic")
if err != nil {
return err
}
scanner := bufio.NewScanner(open)
topicTmMap := make(map[int64]*Topic)
for scanner.Scan() {
text := scanner.Text()
var topic Topic
if err := json.Unmarshal([]byte(text), &topic); err != nil {
}
topicTmpMap[topic.Id] = &topic
}
topicTmpMap[topic.Id] = topicTmpMap
return nil
}
读取文件并解析成 Topic 结构体对象的。它会打开一个文件,读取文件中的每一行,将每一行解析成 Topic 结构体对象,并将其存入一个 map 中。这个 map 以 topic.Id 为 key,topic 结构体指针为 value。最后,它将该 map 赋值给 topicTmpMap 变量。
4.6.1 Repository-查询
// 适合高并发模式下,只执行一次的场景。 即单立模式。
var topicOnce sync.Once
4.7 Service
逻辑流程 参数校验→准备数据→组装实体
4.8 Controller
- 构建View对象
- 业务错误码
4.9 Router
- 初始化数据索引
- 初始化引擎配置
- 构建路由
- 启动服务