Go进阶--httptest
单元测试的原则,就是你所测试的函数方法,不要受到所依赖环境的影响,比如网络访问等,因为有时候我们运行单元测试的时候,并没有联网,那么总不能让单元测试因为这个失败吧?所以这时候模拟网络访问就有必要了。
对于go的web应用程序中往往需要与其他系统进行交互, 比如通过http访问其他系统, 此时就需要一种方法用于打桩
来模拟Web服务端和客户端,httptest包即Go语言针对Web应用提供的解决方案。
httptest 可以方便的模拟各种Web服务器和客户端,以达到测试的目的。
基本使用
假设在server中handler已经写好: main_test.go
package main
import (
"io"
"log"
"net/http"
)
func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
// A very simple health check.
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
// In the future we could report back on the status of our DB, or our cache
// (e.g. Redis) by performing a simple PING, and include them in the response.
_, err := io.WriteString(w, `{"alive": true}`)
if err != nil {
log.Printf("reponse err ")
}
}
func main() {
// 路由与视图函数绑定
http.HandleFunc("/health-check", HealthCheckHandler)
// 启动服务,监听地址
err := http.ListenAndServe(":9999", nil)
if err != nil {
log.Fatal(err)
}
}
测试代码如下:
main.go
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestHealthCheckHandler(t *testing.T) {
//创建一个请求
req, err := http.NewRequest("GET", "/health-check", nil)
if err != nil {
t.Fatal(err)
}
// 我们创建一个 ResponseRecorder (which satisfies http.ResponseWriter)来记录响应
rr := httptest.NewRecorder()
//直接使用HealthCheckHandler,传入参数rr,req
HealthCheckHandler(rr, req)
// 检测返回的状态码
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
// 检测返回的数据
expected := `{"alive": true}`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}
在不启动服务的情况下即可web接口的测试
# go test -v main_test.go main.go
# 输出
root@failymao:/mnt/d/gopath/httptest# go test -v main_test.go main.go
=== RUN TestHealthCheckHandler
--- PASS: TestHealthCheckHandler (0.00s)
PASS
ok command-line-arguments 0.031s
运行这个单元测试,就可以看到访问/health-check的结果里,并且我们没有启动任何HTTP服务就达到了目的。这个主要利用httptest.NewRecorder()创建一个http.ResponseWriter,模拟了真实服务端的响应,这种响应时通过调用http.DefaultServeMux.ServeHTTP方法触发的。
扩展使用
如果Web Server有操作数据库的行为,需要在init函数中进行数据库的连接。
参考官方文档中的样例编写的另外一个测试代码:
func TestHealthCheckHandler2(t *testing.T) {
reqData := struct {
Info string `json:"info"`
}{Info: "P123451"}
reqBody, _ := json.Marshal(reqData)
fmt.Println("input:", string(reqBody))
// 使用httptes.NewRequest请求接口
req := httptest.NewRequest(
http.MethodPost,
"/health-check",
bytes.NewReader(reqBody),
)
req.Header.Set("userid", "wdt")
req.Header.Set("commpay", "brk")
// 使用server,返回的是reponser
rr := httptest.NewRecorder()
HealthCheckHandler(rr, req)
result := rr.Result()
body, _ := ioutil.ReadAll(result.Body)
fmt.Println(string(body))
if result.StatusCode != http.StatusOK {
t.Errorf("expected status 200,",result.StatusCode)
}
}
不同的地方:
- http.NewRequest替换为httptest.NewRequest。
- httptest.NewRequest的第三个参数可以用来传递body数据,必须实现io.Reader接口。
- httptest.NewRequest不会返回error,无需进行err!=nil检查。
- 解析响应时没直接使用ResponseRecorder,而是调用了Result函数。
接口context使用
代码如下
func TestGetProjectsHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/api/users", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
// e.g. func GetUsersHandler(ctx context.Context, w http.ResponseWriter, r *http.Request)
handler := http.HandlerFunc(GetUsersHandler)
// Populate the request's context with our test data.
ctx := req.Context()
ctx = context.WithValue(ctx, "app.auth.token", "abc123")
ctx = context.WithValue(ctx, "app.user",
&YourUser{ID: "qejqjq", Email: "user@example.com"})
// Add our context to the request: note that WithContext returns a copy of
// the request, which we must assign.
req = req.WithContext(ctx)
handler.ServeHTTP(rr, req)
// Check the status code is what we expect.
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
}
模拟调用
还有一个模拟调用的方式,是真的在测试机上模拟一个服务器,然后进行调用测试。
将上面的代码进行改造
main.go
package main
import (
"encoding/json"
"log"
"net/http"
)
type Response struct {
Code int64 `json:"code"`
Data map[string]interface{} `json:"data"`
Msg string `json:"msg"`
}
// json 返回
func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
// A very simple health check.
response := Response{
Code: 200,
Msg: "ok",
Data: map[string]interface{}{"alive": true},
}
// In the future we could report back on the status of our DB, or our cache
// (e.g. Redis) by performing a simple PING, and include them in the response.
res, err := json.Marshal(response)
// _, err := io.WriteString(w, `{"alive": true}`)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Printf("reponse err ")
return
}
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(res)
if err != nil {
log.Printf("reponse err ")
}
}
func main() {
// 路由与视图函数绑定
http.HandleFunc("/health-check", HealthCheckHandler)
// 启动服务,监听地址
err := http.ListenAndServe(":9999", nil)
if err != nil {
log.Fatal(err)
}
}
main_mock_test.go
package main
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"testing"
)
func mockServer() *httptest.Server {
// API调用处理函数
healthHandler := func(rw http.ResponseWriter, r *http.Request) {
response := Response{
Code: 200,
Msg: "ok",
Data: map[string]interface{}{"alive": true},
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusOK)
_ = json.NewEncoder(rw).Encode(response)
}
// 适配器转换
return httptest.NewServer(http.HandlerFunc(healthHandler))
}
func TestHealthCheck3(t *testing.T) {
// 创建一个模拟的服务器
server := mockServer()
defer server.Close()
// Get请求发往模拟服务器的地址
request, err := http.Get(server.URL)
if err != nil {
t.Fatal("创建Get失败")
}
defer request.Body.Close()
log.Println("code:", request.StatusCode)
js, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Fatal(err)
}
log.Printf("body:%s\n", js)
}
模拟服务器的创建使用的是httptest.NewServer函数,它接收一个http.Handler
处理API请求的接口。 代码示例中使用了Hander的适配器模式,http.HandlerFunc
是一个函数类型,实现了http.Handler接口,这里是强制类型转换,不是函数的调用
执行测试:
# 指令
go test -v main_mock_test.go main.go
# 输出
=== RUN TestHealthCheck3
2021/07/22 23:20:27 code: 200
2021/07/22 23:20:27 body:{"code":200,"data":{"alive":true},"msg":"ok"}
--- PASS: TestHealthCheck3 (0.01s)
PASS
ok command-line-arguments 0.032s
测试覆盖率
尽可能的模拟更多的场景来测试我们代码的不同情况,但是有时候的确也有忘记测试的代码,这时候我们就需要测试覆盖率作为参考了。
由单元测试的代码,触发运行到的被测试代码的代码行数占所有代码行数的比例,被称为测试覆盖率,代码覆盖率不一定完全精准,但是可以作为参考,可以帮我们测量和我们预计的覆盖率之间的差距。
main.go
func Tag(tag int){
switch tag {
case 1:
fmt.Println("Android")
case 2:
fmt.Println("Go")
case 3:
fmt.Println("Java")
default:
fmt.Println("C")
}
}
main_test
func TestTag(t *testing.T) {
Tag(1)
Tag(2)
}
使用go test
工具运行单元测试,和前几次不一样的是 ,要显示测试覆盖率,所以要多加一个参数-coverprofile,所以完整的命令为:go test -v -coverprofile=c.out
,-coverprofile是指定生成的覆盖率文件,例子中是c.out,这个文件一会我们会用到。现在看终端输出,已经有了一个覆盖率。
# 执行
$ go test -v -coverprofile=c.out main_test.go main.go
# 输出
=== RUN TestTag
Android
Go
--- PASS: TestTag (0.00s)
PASS
coverage: 60.0% of statements
ok command-line-arguments 0.005s coverage: 60.0% of statements
coverage: 60.0% of statements,60%的测试覆盖率,还没有到100%
那么看看还有那些代码没有被测试到。
这就需要我们刚刚生成的测试覆盖率文件c.out生成测试覆盖率报告了。生成报告有go为我们提供的工具,使用go tool cover -html=c.out -o=tag.html
,即可生成一个名字为tag.html的HTML格式的测试覆盖率报告,这里有详细的信息告诉我们哪一行代码测试到了,哪一行代码没有测试到。
上图中可以看到,标记为绿色的代码行已经被测试了;标记为红色的还没有测试到,有2行的,现在我们根据没有测试到的代码逻辑,完善我的单元测试代码即可。
func TestTag(t *testing.T) {
Tag(1)
Tag(2)
Tag(3)
Tag(6)
}
单元测试完善为如上代码,再运行单元测试,就可以看到测试覆盖率已经是100%了,大功告成。
参考
Go进阶--httptest的更多相关文章
- nodejs进阶(6)—连接MySQL数据库
1. 建库连库 连接MySQL数据库需要安装支持 npm install mysql 我们需要提前安装按mysql sever端 建一个数据库mydb1 mysql> CREATE DATABA ...
- nodejs进阶(4)—读取图片到页面
我们先实现从指定路径读取图片然后输出到页面的功能. 先准备一张图片imgs/dog.jpg. file.js里面继续添加readImg方法,在这里注意读写的时候都需要声明'binary'.(file. ...
- JavaScript进阶之路(一)初学者的开始
一:写在前面的问题和话 一个javascript初学者的进阶之路! 背景:3年后端(ASP.NET)工作经验,javascript水平一般般,前端水平一般般.学习资料:犀牛书. 如有误导,或者错误的地 ...
- nodejs进阶(3)—路由处理
1. url.parse(url)解析 该方法将一个URL字符串转换成对象并返回. url.parse(urlStr, [parseQueryString], [slashesDenoteHost]) ...
- nodejs进阶(5)—接收请求参数
1. get请求参数接收 我们简单举一个需要接收参数的例子 如果有个查找功能,查找关键词需要从url里接收,http://localhost:8000/search?keyword=地球.通过前面的进 ...
- nodejs进阶(1)—输出hello world
下面将带领大家一步步学习nodejs,知道怎么使用nodejs搭建服务器,响应get/post请求,连接数据库等. 搭建服务器页面输出hello world var http = require ...
- [C#] 进阶 - LINQ 标准查询操作概述
LINQ 标准查询操作概述 序 “标准查询运算符”是组成语言集成查询 (LINQ) 模式的方法.大多数这些方法都在序列上运行,其中的序列是一个对象,其类型实现了IEnumerable<T> ...
- Java 进阶 hello world! - 中级程序员之路
Java 进阶 hello world! - 中级程序员之路 Java是一种跨平台的语言,号称:"一次编写,到处运行",在世界编程语言排行榜中稳居第二名(TIOBE index). ...
- C#进阶系列——WebApi 接口返回值不困惑:返回值类型详解
前言:已经有一个月没写点什么了,感觉心里空落落的.今天再来篇干货,想要学习Webapi的园友们速速动起来,跟着博主一起来学习吧.之前分享过一篇 C#进阶系列——WebApi接口传参不再困惑:传参详解 ...
随机推荐
- CentOS 7安装Python3 笔记
当前系统为阿里云的CentOS7.3 64位操作系统. 为了能让后续安装的软件(django,uwsgi,nginx等)尽量减少出现bug的几率,先把可能的依赖包都安装上. 一.安装依赖包 yum - ...
- 第二篇 -- Go语言转义字符与变量声明
上节我们讲了GO语言的环境搭建以及创建了Hello World程序.今天学习一下Go语言基础语法 开发前准备 1. 首先创建一个Project02 2. 在Project02下面新建一个test1.g ...
- 第十六篇 -- SuperIO学习
一.SuperIO 这次主要研究SuperIO读取以及控制风扇转速的问题. 参考文章:https://huchanghui123.github.io/Linux/Linux-Superio-CPU-F ...
- linux系统下操作mysql数据库常见命令
一. 备份数据库(如: test): ①可直接进入后台即可.(MySQL的默认目录:/var/lib/mysql ) ②输入命令: [root@obj mysql]# mysqldump -u roo ...
- VS Code的插件安装位置改变
VS Code的相关配置 VS Code的插件安装位置改变 可以通过创建连接,将默认的extensions位置,改变到D盘 Windows 链接彻底解决 vscode插件安装位置问题 mklink / ...
- CentOS 7 安装虚拟机
1.本次安装centos7 安装使用的软件是VitrualBox 虚拟机软件 Oracle公司的虚拟机软件,免费商品(大家可以百度搜索去官网下载) 1:我这里使用的是阿里的centos7的镜像(大家可 ...
- Jetpack MVVM 实战项目,附带源码+视频,收藏!
从读者的反馈来看,近期大部分安卓开发已跳出舒适圈,开始尝试认识和应用 Jetpack MVVM 到实际的项目开发中. 只可惜,关于 Jetpack MVVM,网上多是 东拼西凑.人云亦云.通篇贴代码 ...
- mysql《一》
一.启动和停止服务器 通过管理员权限打开cmd命令指示符 通过 net stop mysql(自己的服务器名字) 停止服务器 通过 net start mysql(自己的服务器名字) 启动服务器 ...
- 安全工具推荐之w13scan篇
先上链接:https://github.com/w-digital-scanner/w13scan 这是一款漏洞发现工具,支持三大主流平台:windows.Linux.Mac 又一款完全免费开源的工具 ...
- uniapp 实现信息推送(App)
废话不多说直接上代码 以下代码需写在onlaunch生命周期内 onlaunch(){// onlaunch应用级生命周期 :当uni-app 初始化完成时触发(全局只触发一次) //#ifdef A ...