《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 .

8.5 第三方测试库

8.5.1 Gocheck测试包

8.5.2 Ginkgo测试框架

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值