go单元测试及工程实践 | 青训营笔记

71 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天

1、测试

测试是避免事故的最后屏障。 测试一般分为,回归测试一般是QA同学手 动通过终端回归-些固定的主流程场景 ,集成测试是对系统功能维度做测试验证,而单元测试测试开发阶段,开发者对单独的函数、模块做功能验证,层级从上至下,测试成本逐渐减低,而测试覆盖率确逐步上升,所以单元测试的覆盖率-定程度上决定这代码的质量。

image.png

单元测试

包括接口,函数,模块等;用最后的校对来保证代码的功能与我们的预期相符;单元测试一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上既保证了新功能本身的正确性,又未破坏原有代码的正确性。另一方面可以提升效率,在代码有bug的情况下,通过编写单测,可以在一个个较短周期内定位和修复问题。

image.png

单元测试规则

image.png 通常我们使用assert来将期望的值和输出的值进行比对,来判断测试是否通过

package main

func HelloTom() string {
   return "vjh"
}

image.png

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)
   }
}

image.png 注意方法命名,大写

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)
}

image.png

代码覆盖率

如何衡量代码是否经过了足够的测试?

如何评价项目的测试水准?

如何评估项目是否达到了高水准测试等级

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
}

image.png

单元测试的覆盖率

image.png

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)

image.png

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)
}

image.png

关闭内联优化需要关闭才可以

基准测试

基准测试是在一定的工作负载之下检测程序性能的一种方法,和普通测试不同的是,默认情况下不运行任何基准测试。我们需要通过-bench命令行标志参数手工指定要运行的基准测试函数。该参数是一个正则表达式,用于匹配要执行的基准测试函数的名字,默认值是空的。其中“.”模式将可以匹配所有基准测试函数。

-bench=.  代表执行所有的bench开头的用例

image.png

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、工程实践

image.png

基于三层架构来实现

image.png

image.png 项目目录

image.png

以查询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
}