《Go Web 编程》之第8章 应用测试
第8章 应用测试
8.1 Go与测试
//包
testing
//工具
go test
//文件
*_test.go
//函数
func TestXxx(t *testing.T) {
//方法
t.Error
t.Fail
t.Skip
}
8.2 单元测试
单元(unit),模块化部分,函数或方法。
单元测试已测试套件(test suite)形式运行,验证特定行为而创建的单元测试用例集合。
post.json
{
"id" : 1,
"content" : "Hello World!",
"author" : {
"id" : 2,
"name" : "Sau Sheong"
},
"comments" : [
{
"id" : 1,
"content" : "Have a great day!",
"author" : "Adam"
},
{
"id" : 2,
"content" : "How are you today?",
"author" : "Betty"
}
]
}
main.go
package main
import (
"encoding/json"
"fmt"
"os"
)
type Post struct {
Id int `json:"id"`
Content string `json:"content"`
Author Author `json:"author"`
Comments []Comment `json:"comments"`
}
type Author struct {
Id int `json:"id"`
Name string `json:"name"`
}
type Comment struct {
Id int `json:"id"`
Content string `json:"content"`
Author string `json:"author"`
}
// decode JSON from file to struct
func decode(filename string) (post Post, err error) {
jsonFile, err := os.Open(filename)
if err != nil {
fmt.Println("Error opening JSON file:", err)
return
}
defer jsonFile.Close()
decoder := json.NewDecoder(jsonFile)
err = decoder.Decode(&post)
if err != nil {
fmt.Println("Error decoding JSON:", err)
return
}
return
}
func main() {
_, err := decode("post.json")
if err != nil {
fmt.Println("Error:", err)
}
}
main_test.go
package main
import (
"testing"
)
// Test the decode function
func TestDecode(t *testing.T) {
post, err := decode("post.json")
if err != nil {
t.Error(err)
}
if post.Id != 1 {
t.Error("Post ID is not the same as post.json", post.Id)
}
if post.Content != "Hello World!" {
t.Error("Post content is not the same as post.json", post.Id)
}
}
// Test the encode function
func TestEncode(t *testing.T) {
t.Skip("Skipping encoding for now")
}
testing.T常用函数:
- Log,类似fmt.Println;
- Logf,类似fmt.Printf;
- Fail,标记失败,测序函数继续执行;
- FailNow,标记失败,测序函数停止执行;
- Error=Log+Fail
- Errorf=Logf+Fail
- Fatal=Log+FailNow
- Fatalf=Logf+FailNow
go test
go test -v -cover
-v 具体(verbose)标志,更详细的信息
-cover 覆盖率标志
8.2.1 跳过测试用例
测试驱动开发(test-driven development,TDD)时,测试用例持续失败,直到函数真正地实现,Skip函数可暂时跳过指定的测试用例和耗时的测试用例(完整性检查时,sanity check)。
短暂标志-short会根据用户编写测试代码的方式,跳过测试部分或全部。
func TestLongRunningTest(t *testing.T) {
if testing.Short() { //加了-short标志为真
t.Skip("Skipping long-running test in short mode")
}
time.Sleep(10*time.Second)
}
go test -short
8.2.2 并行测试
若单元测试可独立进行,可并行提高测试速度。
parallel_test.go
package main
import (
"testing"
"time"
)
func TestParallel_1(t *testing.T) {
t.Parallel()
time.Sleep(1 * time.Second)
}
func TestParallel_2(t *testing.T) {
t.Parallel()
time.Sleep(2 * time.Second)
}
func TestParallel_3(t *testing.T) {
t.Parallel()
time.Sleep(3 * time.Second)
}
go test -v parallel_test.go
go test -v parallel_test.go -parallel 3
//-parallel 3
//最多并行运行3各测试用例
8.2.3 基准测试
testing包支持两种类型测试:
- 检验程序功能性的功能测试(functional testing);
- 查明任务单元性能的基准测试(benchmarking)。
func BenchmarkXxx(*testing.B){ ... }
package main
import (
"testing"
)
func BenchmarkDecode(b *testing.B) {
for i := 0; i < b.N; i++ { //b.N根据代码而改变
decode("post.json")
}
}
go test -v -cover -short -bench .
//-bench,执行基准测试
//正则表达式.匹配所有基准测试文件
//功能测试+基准测试
go test -run x -bench .
//基准测试(+名字为x的功能测试)
func unmarshal(filename string) (post Post, err error) {
jsonFile, err := os.Open(filename)
if err != nil {
fmt.Println("Error opening JSON file:", err)
return
}
defer jsonFile.Close()
jsonData, err := ioutil.ReadAll(jsonFile)
if err != nil {
fmt.Println("Error reading JSON data:", err)
return
}
json.Unmarshal(jsonData, &post)
return
}
func BenchmarkUnmarshal(b *testing.B) {
for i := 0; i < b.N; i++ {
unmarshal("post.json")
}
}
go test -run -x -bench .
8.3 HTTP测试
testing/httptest包可发送HTTP请求,获取HTTP响应。
httptest包进行HTTP测试的具体步骤:
(1)创建多路复用器;
mux := http.NewServerMux()
(2)多路复用器绑定测试的处理器;
mux.HandleFunc("/post/", handleRequest)
(3)创建记录器;
writer := httptest.NewRecorder()
(4)创建请求;
json := strings.NewReader(`{"content":"Updated post","author":"Sau Sheong"}`)
request, _ := http.NewRequest("PUT", "/post/1", json)
(5)发送请求,响应写入记录器;
mux.ServeHTTP(writer, request)
(6)检查记录器中响应结果。
if writer.Code != 200 {
t.Errorf("Response code is %v", writer.Code)
}
setup.sql
drop table if exists posts;
create table posts (
id serial primary key,
content text,
author varchar(255)
);
sql.txt
psql -h localhost -p 5433 -U gwp -d gwp -f setup.sql
data.go
package main
import (
"database/sql"
_ "github.com/lib/pq"
)
var Db *sql.DB
// connect to the Db
func init() {
var err error
Db, err = sql.Open("postgres", "port=5433 user=gwp dbname=gwp password=gwp sslmode=disable")
if err != nil {
panic(err)
}
//插入记录,用于测试
post := &Post{Content: "test", Author: "mali"}
post.create()
}
// Get a single post
func retrieve(id int) (post Post, err error) {
err = Db.QueryRow("select id, content, author from posts where id = $1", id).Scan(&post.Id, &post.Content, &post.Author)
return
}
// Create a new post
func (post *Post) create() (err error) {
statement := "insert into posts (content, author) values ($1, $2) returning id"
stmt, err := Db.Prepare(statement)
if err != nil {
return
}
defer stmt.Close()
err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id)
return
}
// Update a post
func (post *Post) update() (err error) {
_, err = Db.Exec("update posts set content = $2, author = $3 where id = $1", post.Id, post.Content, post.Author)
return
}
// Delete a post
func (post *Post) delete() (err error) {
_, err = Db.Exec("delete from posts where id = $1", post.Id)
return
}
server.go
package main
import (
"encoding/json"
"net/http"
"path"
"strconv"
)
type Post struct {
Id int `json:"id"`
Content string `json:"content"`
Author string `json:"author"`
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8080",
}
http.HandleFunc("/post/", handleRequest)
server.ListenAndServe()
}
// main handler function
func handleRequest(w http.ResponseWriter, r *http.Request) {
var err error
switch r.Method {
case "GET":
err = handleGet(w, r)
case "POST":
err = handlePost(w, r)
case "PUT":
err = handlePut(w, r)
case "DELETE":
err = handleDelete(w, r)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// Retrieve a post
// GET /post/1
func handleGet(w http.ResponseWriter, r *http.Request) (err error) {
id, err := strconv.Atoi(path.Base(r.URL.Path))
if err != nil {
return
}
post, err := retrieve(id)
if err != nil {
return
}
output, err := json.MarshalIndent(&post, "", "\t\t")
if err != nil {
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(output)
return
}
// Create a post
// POST /post/
func handlePost(w http.ResponseWriter, r *http.Request) (err error) {
len := r.ContentLength
body := make([]byte, len)
r.Body.Read(body)
var post Post
json.Unmarshal(body, &post)
err = post.create()
if err != nil {
return
}
w.WriteHeader(200)
return
}
// Update a post
// PUT /post/1
func handlePut(w http.ResponseWriter, r *http.Request) (err error) {
id, err := strconv.Atoi(path.Base(r.URL.Path))
if err != nil {
return
}
post, err := retrieve(id)
if err != nil {
return
}
len := r.ContentLength
body := make([]byte, len)
r.Body.Read(body)
json.Unmarshal(body, &post)
err = post.update()
if err != nil {
return
}
w.WriteHeader(200)
return
}
// Delete a post
// DELETE /post/1
func handleDelete(w http.ResponseWriter, r *http.Request) (err error) {
id, err := strconv.Atoi(path.Base(r.URL.Path))
if err != nil {
return
}
post, err := retrieve(id)
if err != nil {
return
}
err = post.delete()
if err != nil {
return
}
w.WriteHeader(200)
return
}
server_test.go
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestHandleGet(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/post/", handleRequest)
writer := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/post/1", nil)
mux.ServeHTTP(writer, request)
if writer.Code != 200 {
t.Errorf("Response code is %v", writer.Code)
}
var post Post
json.Unmarshal(writer.Body.Bytes(), &post)
if post.Id != 1 {
t.Errorf("Cannot retrieve JSON post")
}
}
func TestHandlePut(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/post/", handleRequest)
writer := httptest.NewRecorder()
json := strings.NewReader(`{"content":"Updated post","author":"Sau Sheong"}`)
request, _ := http.NewRequest("PUT", "/post/1", json)
mux.ServeHTTP(writer, request)
if writer.Code != 200 {
t.Errorf("Response code is %v", writer.Code)
}
}
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
)
var mux *http.ServeMux
var writer *httptest.ResponseRecorder
//setUp()和tearDown()在整个测试过程中只执行一次
//所有测试用例执行前后执行
//m.Run()调用所有测试用例
func TestMain(m *testing.M) {
setUp() //预设(setup)操作
code := m.Run()
tearDown() //拆卸(teardown)操作
os.Exit(code)
}
func setUp() {
mux = http.NewServeMux()
mux.HandleFunc("/post/", handleRequest)
writer = httptest.NewRecorder()
}
func tearDown() {
}
func TestHandleGet(t *testing.T) {
request, _ := http.NewRequest("GET", "/post/1", nil)
mux.ServeHTTP(writer, request)
if writer.Code != 200 {
t.Errorf("Response code is %v", writer.Code)
}
var post Post
json.Unmarshal(writer.Body.Bytes(), &post)
if post.Id != 1 {
t.Errorf("Cannot retrieve JSON post")
}
}
func TestHandlePut(t *testing.T) {
json := strings.NewReader(`{"content":"Updated post","author":"Sau Sheong"}`)
request, _ := http.NewRequest("PUT", "/post/1", json)
mux.ServeHTTP(writer, request)
if writer.Code != 200 {
t.Errorf("Response code is %v", writer.Code)
}
}
测试
go test .
go run server.go data.go
curl -i http://127.0.0.1:8080/post/1
8.4 测试替身以及依赖注入
测试替身(test double),模拟测试不方便使用的实际对象、结构或者函数。提高被测试代码的独立性,自动单元测试环境里常使用。
模拟邮件发送(并不真正发送邮件)或移除单元测试中对真实数据库的依赖,都需要创建测试替身。
可用依赖注入(dependency injection)设计模式实现测试替身,通过向被调用的对象、结构或者函数传入依赖关系(Go中通常是interface),然后由依赖关系代替被调用者执行实际的操作,依此来解耦软件中的多层(layer)。
handleRequest和handleGet依赖retrieve函数,后者最终依赖sql.DB,需要移除sql.DB的依赖(避免直接依赖)。
将sql.DB包含在Post结构体(实现了Text接口)中,然后通过接口Text传递到函数调用流程里,所有调用流程里都未直接依赖sql.DB。
data.go
package main
import (
"database/sql"
_ "github.com/lib/pq"
)
type Text interface {
fetch(id int) (err error)
create() (err error)
update() (err error)
delete() (err error)
}
type Post struct {
Db *sql.DB
Id int `json:"id"`
Content string `json:"content"`
Author string `json:"author"`
}
// Get a single post
func (post *Post) fetch(id int) (err error) {
err = post.Db.QueryRow("select id, content, author from posts where id = $1", id).Scan(&post.Id, &post.Content, &post.Author)
return
}
// Create a new post
func (post *Post) create() (err error) {
statement := "insert into posts (content, author) values ($1, $2) returning id"
stmt, err := post.Db.Prepare(statement)
if err != nil {
return
}
defer stmt.Close()
err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id)
return
}
// Update a post
func (post *Post) update() (err error) {
_, err = post.Db.Exec("update posts set content = $2, author = $3 where id = $1", post.Id, post.Content, post.Author)
return
}
// Delete a post
func (post *Post) delete() (err error) {
_, err = post.Db.Exec("delete from posts where id = $1", post.Id)
return
}
server.go
package main
import (
"database/sql"
"encoding/json"
_ "github.com/lib/pq"
"net/http"
"path"
"strconv"
)
func main() {
// connect to the Db
var err error
db, err := sql.Open("postgres", "port=5433 user=gwp dbname=gwp password=gwp sslmode=disable")
if err != nil {
panic(err)
}
server := http.Server{
Addr: "127.0.0.1:8080",
}
http.HandleFunc("/post/", handleRequest(&Post{Db: db}))
server.ListenAndServe()
}
// main handler function
func handleRequest(t Text) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var err error
switch r.Method {
case "GET":
err = handleGet(w, r, t)
case "POST":
err = handlePost(w, r, t)
case "PUT":
err = handlePut(w, r, t)
case "DELETE":
err = handleDelete(w, r, t)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
// Retrieve a post
// GET /post/1
func handleGet(w http.ResponseWriter, r *http.Request, post Text) (err error) {
id, err := strconv.Atoi(path.Base(r.URL.Path))
if err != nil {
return
}
err = post.fetch(id)
if err != nil {
return
}
output, err := json.MarshalIndent(post, "", "\t\t")
if err != nil {
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(output)
return
}
// Create a post
// POST /post/
func handlePost(w http.ResponseWriter, r *http.Request, post Text) (err error) {
len := r.ContentLength
body := make([]byte, len)
r.Body.Read(body)
json.Unmarshal(body, post)
err = post.create()
if err != nil {
return
}
w.WriteHeader(200)
return
}
// Update a post
// PUT /post/1
func handlePut(w http.ResponseWriter, r *http.Request, post Text) (err error) {
id, err := strconv.Atoi(path.Base(r.URL.Path))
if err != nil {
return
}
err = post.fetch(id)
if err != nil {
return
}
len := r.ContentLength
body := make([]byte, len)
r.Body.Read(body)
json.Unmarshal(body, post)
err = post.update()
if err != nil {
return
}
w.WriteHeader(200)
return
}
// Delete a post
// DELETE /post/1
func handleDelete(w http.ResponseWriter, r *http.Request, post Text) (err error) {
id, err := strconv.Atoi(path.Base(r.URL.Path))
if err != nil {
return
}
err = post.fetch(id)
if err != nil {
return
}
err = post.delete()
if err != nil {
return
}
w.WriteHeader(200)
return
}
go run server.go data.go
double.go
package main
type FakePost struct {
Id int
Content string
Author string
}
func (post *FakePost) fetch(id int) (err error) {
post.Id = id
return
}
func (post *FakePost) create() (err error) {
return
}
func (post *FakePost) update() (err error) {
return
}
func (post *FakePost) delete() (err error) {
return
}
server_test.go
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestGetPost(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/post/", handleRequest(&FakePost{}))
writer := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/post/1", nil)
mux.ServeHTTP(writer, request)
if writer.Code != 200 {
t.Errorf("Response code is %v", writer.Code)
}
var post Post
json.Unmarshal(writer.Body.Bytes(), &post)
if post.Id != 1 {
t.Errorf("Cannot retrieve JSON post")
}
}
func TestPutPost(t *testing.T) {
mux := http.NewServeMux()
post := &FakePost{}
mux.HandleFunc("/post/", handleRequest(post))
writer := httptest.NewRecorder()
json := strings.NewReader(`{"content":"Updated post","author":"Sau Sheong"}`)
request, _ := http.NewRequest("PUT", "/post/1", json)
mux.ServeHTTP(writer, request)
if writer.Code != 200 {
t.Errorf("Response code is %v", writer.Code)
}
if post.Content != "Updated post" {
t.Error("Content is not correct", post.Content)
}
}
go test .