这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
1、测试
测试是避免事故的最后屏障。 测试一般分为,回归测试一般是QA同学手 动通过终端回归-些固定的主流程场景 ,集成测试是对系统功能维度做测试验证,而单元测试测试开发阶段,开发者对单独的函数、模块做功能验证,层级从上至下,测试成本逐渐减低,而测试覆盖率确逐步上升,所以单元测试的覆盖率-定程度上决定这代码的质量。
单元测试
包括接口,函数,模块等;用最后的校对来保证代码的功能与我们的预期相符;单元测试一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上既保证了新功能本身的正确性,又未破坏原有代码的正确性。另一方面可以提升效率,在代码有bug的情况下,通过编写单测,可以在一个个较短周期内定位和修复问题。
单元测试规则
通常我们使用assert来将期望的值和输出的值进行比对,来判断测试是否通过
package main
func HelloTom() string {
return "vjh"
}
package main
import "testing"
func TestHelloTom(t *testing.T) {
output := HelloTom()
expect := "vh"
if output != expect {
t.Errorf("expected %s not match actural %s", expect, output)
}
}
注意方法命名,大写
assert版本的
package main
import (
"github.com/go-playground/assert/v2"
"testing"
)
func TestHelloTom(t *testing.T) {
output := HelloTom()
expect := "vjh"
assert.Equal(t, expect, output)
}
代码覆盖率
如何衡量代码是否经过了足够的测试?
如何评价项目的测试水准?
如何评估项目是否达到了高水准测试等级
package main
import (
"github.com/go-playground/assert/v2"
"testing"
)
func TestHelloTom(t *testing.T) {
output := HelloTom()
expect := "vjh"
assert.Equal(t, expect, output)
}
func TestJudgePass(t *testing.T) {
ispass := JudgePass(65)
expect := true
assert.Equal(t, expect, ispass)
}
func TestJudgePassNot(t *testing.T) {
ispass := JudgePass(50)
expect := false
assert.Equal(t, expect, ispass)
}
package main
func HelloTom() string {
return "vjh"
}
func JudgePass(sc int8) bool {
if sc > 60 {
return true
}
return false
}
单元测试的覆盖率
mock
func JudgePass(sc int8) bool {
if sc > 60 {
return true
}
return false
}
func ThroughJuggePass(score int8) string {
res := JudgePass(score)
res1 := "不正确"
if res {
res1 = "测试结果正确"
}
return res1
}
如代码所示,此时我们的代码依赖于外部函数JudgePass,而这个函数可能会发生变化,为了解决这种依赖,我们可以mock一个函数。 这里使用monkey进行:bouk/monkey: Monkey patching in Go (github.com)
func JudgePass(sc int8) bool {
if sc > 60 {
return true
}
return false
}
func ThroughJuggePass(score int8) string {
res := JudgePass(score)
res1 := "不正确"
if res == true {
res1 = "测试结果正确"
}
return res1
}
我们在ThroughJuggePass调用了一个函数,可以使用monkey对这个函数打桩,返回我们想要的结果
func TestThroughJuggePass(t *testing.T) {
monkey.Patch(JudgePass, func(sc int8) bool {
return true
})
defer monkey.Unpatch(JudgePass)
res := ThroughJuggePass(20)
assert.Equal(t, "测试结果正确", res)
}
关闭内联优化需要关闭才可以
基准测试
基准测试是在一定的工作负载之下检测程序性能的一种方法,和普通测试不同的是,默认情况下不运行任何基准测试。我们需要通过-bench命令行标志参数手工指定要运行的基准测试函数。该参数是一个正则表达式,用于匹配要执行的基准测试函数的名字,默认值是空的。其中“.”模式将可以匹配所有基准测试函数。
-bench=. 代表执行所有的bench开头的用例
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Select()
}
}
func BenchmarkSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Select()
}
})
}
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i + 100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
2、工程实践
基于三层架构来实现
项目目录
以查询topic为例 dao的具体代码,主要是查询和主题有关的信息
package repository
import (
"github.com/Moonlight-Zhao/go-project-example/util"
"sync"
"time"
)
type Topic struct {
Id int64 `gorm:"column:id"`
UserId int64 `gorm:"column:user_id"`
Title string `gorm:"column:title"`
Content string `gorm:"column:content"`
CreateTime time.Time `gorm:"column:create_time"`
}
func (Topic) TableName() string {
return "topic"
}
type TopicDao struct {
}
var topicDao *TopicDao
var topicOnce sync.Once
func NewTopicDaoInstance() *TopicDao {
topicOnce.Do(
func() {
topicDao = &TopicDao{}
})
return topicDao
}
func (*TopicDao) QueryTopicById(id int64) (*Topic, error) {
var topic Topic
err := db.Where("id = ?", id).Find(&topic).Error
if err != nil {
util.Logger.Error("find topic by id err:" + err.Error())
return nil, err
}
return &topic, nil
}
service
package service
import (
"errors"
"fmt"
"github.com/Moonlight-Zhao/go-project-example/repository"
"sync"
)
type TopicInfo struct {
Topic *repository.Topic
User *repository.User
}
type PostInfo struct {
Post *repository.Post
User *repository.User
}
type PageInfo struct {
TopicInfo *TopicInfo
PostList []*PostInfo
}
func QueryPageInfo(topicId int64) (*PageInfo, error) {
return NewQueryPageInfoFlow(topicId).Do()
}
func NewQueryPageInfoFlow(topId int64) *QueryPageInfoFlow {
return &QueryPageInfoFlow{
topicId: topId,
}
}
//定义需要用的结构体
type QueryPageInfoFlow struct {
topicId int64
pageInfo *PageInfo
topic *repository.Topic
posts []*repository.Post
userMap map[int64]*repository.User
}
func (f *QueryPageInfoFlow) Do() (*PageInfo, error) {
//职责单一化
if err := f.checkParam(); err != nil {
return nil, err
}
if err := f.prepareInfo(); err != nil {
return nil, err
}
if err := f.packPageInfo(); err != nil {
return nil, err
}
return f.pageInfo, nil
}
func (f *QueryPageInfoFlow) checkParam() error {
if f.topicId <= 0 {
return errors.New("topic id must be larger than 0")
}
return nil
}
func (f *QueryPageInfoFlow) prepareInfo() error {
//获取topic信息
var wg sync.WaitGroup
wg.Add(2)
var topicErr, postErr error
go func() {//可以使用两个线程来并行执行
defer wg.Done()
topic, err := repository.NewTopicDaoInstance().QueryTopicById(f.topicId)
if err != nil {
topicErr = err
return
}
f.topic = topic
}()
//获取post列表
go func() {
defer wg.Done()
posts, err := repository.NewPostDaoInstance().QueryPostByParentId(f.topicId)
if err != nil {
postErr = err
return
}
f.posts = posts
}()
wg.Wait()
if topicErr != nil {
return topicErr
}
if postErr != nil {
return postErr
}
//获取用户信息
uids := []int64{f.topic.Id}
for _, post := range f.posts {
uids = append(uids, post.Id)
}
userMap, err := repository.NewUserDaoInstance().MQueryUserById(uids)
if err != nil {
return err
}
f.userMap = userMap
return nil
}
func (f *QueryPageInfoFlow) packPageInfo() error {
//topic info
userMap := f.userMap
topicUser, ok := userMap[f.topic.UserId]
if !ok {
return errors.New("has no topic user info")
}
//post list
postList := make([]*PostInfo, 0)
for _, post := range f.posts {
postUser, ok := userMap[post.UserId]
if !ok {
return errors.New("has no post user info for " + fmt.Sprint(post.UserId))
}
postList = append(postList, &PostInfo{
Post: post,
User: postUser,
})
}//封装
f.pageInfo = &PageInfo{
TopicInfo: &TopicInfo{
Topic: f.topic,
User: topicUser,
},
PostList: postList,
}
return nil
}
controller
package handler
import (
"strconv"
"github.com/Moonlight-Zhao/go-project-example/service"
)
type PageData struct {
Code int64 `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
func QueryPageInfo(topicIdStr string) *PageData {
//参数转换
topicId, err := strconv.ParseInt(topicIdStr, 10, 64)
if err != nil {
return &PageData{
Code: -1,
Msg: err.Error(),
}
}
//获取service层结果
pageInfo, err := service.QueryPageInfo(topicId)
if err != nil {
return &PageData{
Code: -1,
Msg: err.Error(),
}
}
return &PageData{
Code: 0,
Msg: "success",
Data: pageInfo,
}
}
使用gin搭建本地服务器
package main
import (
"github.com/Moonlight-Zhao/go-project-example/handler"
"github.com/Moonlight-Zhao/go-project-example/repository"
"github.com/Moonlight-Zhao/go-project-example/util"
"gopkg.in/gin-gonic/gin.v1"
"os"
)
func main() {
if err := Init(); err != nil {
os.Exit(-1)
}
r := gin.Default()
r.Use(gin.Logger())
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.GET("/community/page/get/:id", func(c *gin.Context) {
topicId := c.Param("id")
data := handler.QueryPageInfo(topicId)
c.JSON(200, data)
})
r.POST("/community/post/do", func(c *gin.Context) {
uid, _ := c.GetPostForm("uid")
topicId, _ := c.GetPostForm("topic_id")
content, _ := c.GetPostForm("content")
data := handler.PublishPost(uid, topicId, content)
c.JSON(200, data)
})
err := r.Run()
if err != nil {
return
}
}
func Init() error {
if err := repository.Init(); err != nil {
return err
}
if err := util.InitLogger(); err != nil {
return err
}
return nil
}