昨天晚上在学习慕课网的课程时,写了个简单的抽奖demo,打算简单测试在并发场景下临界资源是否被修改的问题。

httptest

一.实例代码

以下代码是要的测试内容,大致有三个功能:

  • index 首页,GET请求
  • 导入抽奖用户,POST请求
  • 抽奖,GET请求

1.全局变量及main函数

记得初始化锁,否则不起作用。

// 用户列表 共享变量(临界资源)
var userList []string
// gin引擎
var router *gin.Engine
// 互斥锁
var mux sync.Mutex

func main() {
 router.Run(":8080")
}

2.初始化路由

主要初始化了三个功能的路由

func init() {
 router = gin.Default()
 // 路由组
 userGroup := router.Group("/user")
 {
  // 首页
  userGroup.GET("/index", Index)
  // 导入用户
  userGroup.POST("/import", ImportUsers)
  // 抽奖
  userGroup.GET("/lucky", GetLuckyUser)
 }
}

3.三个主要功能

请求成功后,每个页面都是返回一个字符串(包含各自的信息)

3.1 首页

func Index(c *gin.Context) {
 c.String(http.StatusOK, "当前参与抽奖的用户人数:%d", len(userList))
}

3.2 导入用户

func ImportUsers(c *gin.Context) {
 strUsers := c.Query("users")
 users := strings.Split(strUsers, ",")
 // 在操作 全局变量 userList 之前加互斥锁,加完锁记得释放
 mux.Lock()
 defer mux.Unlock()
 // 统计当前已经在参加抽奖的用户数量
 currUserCount := len(userList)

 // 将页面提交的用户导入到 userList 中,参与抽奖
 for _, user := range users {
  user = strings.TrimSpace(user)
  if len(user) > 0 {
   userList = append(userList, user)
  }
 }
 // 统计当前总共参加抽奖人数
 userTotal := len(userList)
 c.String(http.StatusOK, "当前参与抽奖的用户数量:%d,导入的用户数量:%d", userTotal, (userTotal - currUserCount))
}

3.3 抽奖

func GetLuckyUser(c *gin.Context) {
 var user string
 // 在操作 全局变量 userList 之前加互斥锁,加完锁记得释放
 mux.Lock()
 defer mux.Unlock()
 
 count := len(userList)
 if count > 1 {
  
  seed := time.Now().UnixNano()
  // 以随机数设置中奖用户, [0,count)中的随机值
  lottery_index := rand.New(rand.NewSource(seed)).Int31n(int32(count))
  user = userList[lottery_index]
  // 当前参与抽奖用户减 1
  userList = append(userList[0:lottery_index], userList[lottery_index+1:]...)
  c.String(http.StatusOK, "中奖用户为:%s,剩余用户数:%d", user, count-1)
  
 } else if count == 1 {
  user = userList[0]
  userList = userList[0:0] // 清空参与抽奖的用户列表
  c.String(http.StatusOK, "中奖用户为:%s,剩余用户数:%d", user, count-1)
 } else {
  c.String(http.StatusOK, "当前无参与抽奖的用户,请导入新的用户。")
 }
}

二.测试工具函数

httptestUtil.go

2.1 ParseToStr 将map中的键值对输出成querystring形式

// ParseToStr 将map中的键值对输出成querystring形式
func ParseToStr(mp map[string]string) string {
 values := ""
 for key, val := range mp {
  values += "&" + key + "=" + val
 }
 temp := values[1:]
 values = "?" + temp
 return values
}

2.2 Get 根据特定请求uri,发起get请求返回响应

func Get(uri string, router *gin.Engine) *httptest.ResponseRecorder {
 // 构造get请求
 req := httptest.NewRequest("GET", uri, nil)
 // 初始化响应
 w := httptest.NewRecorder()

 // 调用相应的handler接口
 router.ServeHTTP(w, req)
 return w
}

2.3 ParseToStr 将map中的键值对输出成querystring形式

querystring
querystring
c.Query("users")c.PostFprm("users")c.Param("users)
c.ShouldBind()
// PostForm 根据特定请求uri和参数param,以表单形式传递参数,发起post请求返回响应
func PostForm(uri string, param map[string]string, router *gin.Engine) *httptest.ResponseRecorder {
 req := httptest.NewRequest("POST", uri+ParseToStr(param), nil)
 // 初始化响应
 w := httptest.NewRecorder()
 // 调用相应handler接口
 router.ServeHTTP(w, req)
 return w
}

2.4 PostJson 根据特定请求uri和参数param,以Json形式传递参数,发起post请求返回响应

// PostJson 根据特定请求uri和参数param,以Json形式传递参数,发起post请求返回响应
func PostJson(uri string, param map[string]interface{}, router *gin.Engine) *httptest.ResponseRecorder {
 // 将参数转化为json比特流
 jsonByte, _ := json.Marshal(param)
 // 构造post请求,json数据以请求body的形式传递
 req := httptest.NewRequest("POST", uri, bytes.NewReader(jsonByte))
 // 初始化响应
 w := httptest.NewRecorder()
 // 调用相应的handler接口
 router.ServeHTTP(w, req)
 return w
}

三.开始进行 httptest 测试

Test
TestMVC
func TestMVC(t *testing.T) {
 var w *httptest.ResponseRecorder
 assert := assert.New(t)
 
 // 1.测试 index 请求
 urlIndex := "/user/index"
 w = Get(urlIndex, router)
 assert.Equal(200, w.Code)
 assert.Equal("当前参与抽奖的用户人数:0", w.Body.String())

 // 2.测试 import 请求,导入用户数
 var wg sync.WaitGroup // 定义wg, 用来阻塞 goroutine
 for i := 0; i < 100000; i++ {

  // 开一个等待
  wg.Add(1)
  go func(i int) { // i 不属于临界资源,是安全的
   defer wg.Done() // 一个 goroutine 跑完后要减1,

   // 测试 /user/import 请求,模拟从 form 表单中获取数据
   param := make(map[string]string)
   param["users"] = "user" + strconv.Itoa(i)
   urlImport := "/user/import"
   w = PostForm(urlImport, param, router)
   assert.Equal(200, w.Code)

  }(i)
 }
 // 等待上面的协程运行完,再接着测试
 wg.Wait()
 // 3.测试 urlIndex 请求,查看当前参与抽奖用户是否为 for 循环总数
 w = Get(urlIndex, router)
 assert.Equal(200, w.Code)
 assert.Equal("当前参与抽奖的用户人数:100000", w.Body.String())

 // 4.测试 抽奖
 urlLucky := "/user/lucky"
 w = Get(urlLucky, router)
 assert.Equal(200, w.Code)

 // 5.抽奖一次之后,再发起 index 请求,查看查看当前参与抽奖用户是否减 1
 w = Get(urlIndex, router)
 assert.Equal(200, w.Code)
 assert.Equal("当前参与抽奖的用户人数:99999", w.Body.String())
}

四.运行单元测试,查看结果

运行结果如图:


如图:
在我个人电脑上,测试运行耗时:9.21s;根据users字段的名字也说明了执行了 100000次,因为是并发执行的,所以顺序肯定不是从1到100000按序显示的(谁抢到CPU资源谁执行)

五.总结

从昨天晚上7点开始练习项目,进行单元测试,中间睡了6个小时吧。早上起来后,经过昨晚测试的磨练和学习,上午思路很清晰,不仅单元测试成功了,还将之前自己鼓捣的测试代码进行了重构和优化,直到今天上午11点多才正式完成。

第一次写Golang的httptest单元测试,整个过程就是边搜边学边实践,最后总算成功了。写一下 httptest 测试心得吧:

Testifyif else

参考资料:
1.
2.
3.