Обучение Golang — модульное тестирование httptest на основе Gin framework

Go

Модульное тестирование httptest на основе Gin framework

Прошлой ночью, когда я изучал курс МООК, я написал простую демонстрацию лотереи, намереваясь просто проверить, изменяются ли критические ресурсы в параллельном сценарии.

Затем, после долгого метания, тест прошел успешно, и я записал то, что делал.httptestЧему вы научились во время модульного тестирования.

1. Пример кода

Следующий код является необходимым тестовым содержимым, которое примерно имеет три функции:

  • домашняя страница индекса, запрос GET
  • Импорт пользователей лотереи, запрос POST
  • Розыгрыши, запрос GET

1. Глобальные переменные и основная функция

Не забудьте инициализировать блокировку, иначе она не будет работать.

// 用户列表 共享变量(临界资源)
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, "当前无参与抽奖的用户,请导入新的用户。")
 }
}

2. Функция тестового инструмента

существуетhttptestUtil.goФайл в основном инкапсулирует следующие служебные функции:

2.1 ParseToStr выводит пары ключ-значение на карте в виде строки запроса

// 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 Получить В соответствии с конкретным uri запроса инициировать запрос на получение и вернуть ответ

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 выводит пары ключ-значение на карте в виде строки запроса

Создайте запрос POST, данные формы начинаются сquerystringФорма добавляется после uri

Примечание. Параметры формы form можно передавать черезquerystringФорма прикреплена к адресу URI для доставки

Таким образом, запрос POST для получения параметров должен вызываться, когдаc.Query("users"), вместоc.PostFprm("users"), не говоря уже оc.Param("users)

Конечно, использовать напрямуюc.ShouldBind()Пусть GIN автоматически определяет, каким способом является параметры запроса.
код показывает, как показано ниже:

// 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, и инициируется почтовый запрос для возврата ответа.

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

3. Запустите тест httptest

GolangСпецификация должна рекомендовать способ написания тестовой функции, которая начинается с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())
}

4. Запустите модульные тесты и просмотрите результаты

Результат работы показан на рисунке:
抽奖功能_并发测试
Как показано на рисунке:
На моем персональном компьютере время выполнения теста: 9,21 с, по названию поля пользователей также видно, что он выполнялся 100 000 раз, потому что он выполняется параллельно, поэтому порядок не должен отображаться последовательно от 1 до 100 000 (кто захватывает ЦП, кто выполняет ресурс)

5. Резюме

Я начал практиковать проект в 7 часов вечера прошлой ночью, проводил модульные тесты и спал 6 часов между ними. После утреннего пробуждения, после обучения и изучения теста прошлой ночью, мысли на утро были очень ясными.Не только модульный тест был успешным, но и тестовый код, с которым я возился до этого, был отрефакторен и оптимизирован. Официально он не был завершен до 11 часов утра.

пишу в первый разGolangМодульный тест httptest, весь процесс состоит в том, чтобы учиться и практиковаться во время поиска, и, наконец, преуспел. Напишите об опыте тестирования httptest:

  1. Перед тестированием инкапсулируйте методы запроса, такие как get put, и инкапсулируйте их в httptestUtil для упрощения тестирования.
  2. Гибкие среды тестирования приложений, такие какTestify, можно написать гораздо меньше, если судить (в основном используется для оценки кода ответа и сущности ответа). Я только началif elseЯ написал много суждений, а позже изучил этот тестовый фреймворк
  3. Тестовый код должен быть максимально кратким, чтобы обеспечить удобочитаемость и удобство сопровождения. В противном случае напишите кучу кода, который легко логически запутать, и выглядит очень раздражающим, что влияет на тестовый разум и точность теста.
  4. Если вы столкнулись с новой тестовой проблемой, постарайтесь провести как можно больше исследований, успокоиться и подумать об этом. Скорее всего, проблема, с которой вы боретесь, не является настоящей причиной
  5. Если тест прошел хорошо, то все в порядке, если тест не прошел, вы искали много информации и потратили много времени на тест, то вы должны написать блог (или заметку) в конце, чтобы запишите, что вы узнали и подумали, иначе в будущем вы столкнетесь с проблемами.
  6. Тестовый код лучше всего размещать в блоге (или примечании к приложению) для удобного просмотра позже.
  7. Самое главное иметь ясный ум. Тест легко вызвать у людей головокружение и раздражительность, не сдавайтесь, остановитесь на некоторое время, сделайте перерыв и дайте мозгу расслабиться.

Использованная литература:
1.Официальный тестовый документ Джина
2.Модульное тестирование на основе фреймворка golang gin
3.Улучшайте тесты и макеты GO с помощью Testify