Skip to content

Commit eff077d

Browse files
committed
第九章完成
1 parent 815cd38 commit eff077d

File tree

5 files changed

+359
-13
lines changed

5 files changed

+359
-13
lines changed

SUMMARY.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
- 5.2 math/big — 大数实现
2525
- 5.3 math/cmplx — 复数基本函数操作
2626
- 5.4 math/rand — 伪随机数生成器
27-
* 第六章 [文件系统](chapter06/06.0.md)
27+
* [第六章 文件系统](chapter06/06.0.md)
2828
- 6.1 [os — 平台无关的操作系统功能实现](chapter06/06.1.md)
2929
- 6.2 [path/filepath — 操作路径](chapter06/06.2.md)
3030
* [第七章 数据持久存储与交换](chapter07/07.0.md)
@@ -40,21 +40,21 @@
4040
- 8.4 compress/bzip2 — bzip2 压缩
4141
- 8.5 archive/tar — tar 归档访问
4242
- 8.6 archive/zip — zip 归档访问
43-
* 第九章 [测试](chapter09/09.0.md)
43+
* [第九章 测试](chapter09/09.0.md)
4444
- 9.1 [testing - 单元测试](chapter09/09.1.md)
4545
- 9.2 [testing - 基准测试](chapter09/09.2.md)
4646
- 9.3 [testing - 子测试](chapter09/09.3.md)
4747
- 9.4 [testing - 运行并验证示例](chapter09/09.4.md)
4848
- 9.5 [testing - 其他功能](chapter09/09.5.md)
4949
- 9.6 [httptest - HTTP 测试辅助工具](chapter09/09.6.md)
50-
* 第十章 [进程、线程与 goroutine](chapter10/10.0.md)
50+
* [第十章 进程、线程与 goroutine](chapter10/10.0.md)
5151
- 10.1 [创建进程](chapter10/10.1.md)
5252
- 10.2 [进程属性和控制](chapter10/10.2.md)
5353
- 10.3 [线程](chapter10/10.3.md)
5454
- 10.4 [进程间通信](chapter10/10.4.md)
5555
* 第十一章 网络通信与互联网 (Internet)
5656
* 第十二章 email
57-
* 第十三章 [应用构建 与 debug](chapter13/13.0.md)
57+
* [第十三章 应用构建 与 debug](chapter13/13.0.md)
5858
- 13.1 [flag - 命令行参数解析](chapter13/13.1.md)
5959
- 13.2 [log - 日志记录](chapter13/13.2.md)
6060
- 13.3 [expvar - 公共变量的标准化接口](chapter13/13.3.md)

chapter09/09.6.md

+149-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,155 @@
11
# httptest - HTTP 测试辅助工具 #
22

3-
httptest 包提供了进行 HTTP 测试所需的设施
3+
由于 Go 标准库的强大支持,Go 可以很容易的进行 Web 开发。为此,Go 标准库专门提供了 httptest 包专门用于进行 http Web 开发测试
44

5-
## 测试 http.Handler ##
5+
本节我们通过一个社区帖子的增删改查的例子来学习该包。
66

7+
## 简单的 Web 应用
8+
9+
我们首先构建一个简单的 Web 应用。
10+
11+
为了简单起见,数据保存在内存,并且没有考虑并发问题。
12+
13+
```go
14+
// 保存 Topic,没有考虑并发问题
15+
var TopicCache = make([]*Topic, 0, 16)
16+
17+
type Topic struct {
18+
Id int `json:"id"`
19+
Title string `json:"title"`
20+
Content string `json:"content"`
21+
CreatedAt time.Time `json:"created_at"`
22+
}
23+
```
24+
对于 Topic 的增删改查代码很简单,可以查看[完整代码](code/src/chapter09/httptest/data.go)
25+
26+
接下来,是通过 http 包来实现一个 Web 应用。
27+
28+
```go
29+
func main() {
30+
http.HandleFunc("/topic/", handleRequest)
31+
http.ListenAndServe(":2017", nil)
32+
}
33+
...
34+
```
35+
`/topic/` 开头的请求都交由 `handleRequest` 处理,它根据不同的 `Method` 执行相应的增删改查,详细代码可以查看 [server.go](code/src/chapter09/httptest/server.go)
36+
37+
准备好 Web 应用后,我们启动它。
38+
39+
> go run server.go data.go
40+
41+
通过 curl 进行简单的测试:
42+
43+
> 增:curl -i -X POST http://localhost:2017/topic/ -H 'content-type: application/json' -d '{"title":"The Go Standard Library","content":"It contains many packages."}'
44+
45+
> 查:curl -i -X GET http://localhost:2017/topic/1
46+
47+
> 改:curl -i -X PUT http://localhost:2017/topic/1 -H 'content-type: application/json' -d '{"title":"The Go Standard Library By Example","content":"It contains many packages, enjoying it."}'
48+
49+
> 删:curl -i -X DELETE http://localhost:2017/topic/1
50+
51+
## 通过 httptest 进行测试
52+
53+
上面,我们通过 curl 对我们的 Web 应用的接口进行了测试。现在,我们通过 httptest 进行测试。
54+
55+
我们先测试创建帖子,也就是测试 `handlePost` 函数。
56+
57+
```go
58+
func TestHandlePost(t *testing.T) {
59+
mux := http.NewServeMux()
60+
mux.HandleFunc("/topic/", handleRequest)
61+
62+
reader := strings.NewReader(`{"title":"The Go Standard Library","content":"It contains many packages."}`)
63+
r, _ := http.NewRequest(http.MethodPost, "/topic/", reader)
64+
65+
w := httptest.NewRecorder()
66+
67+
mux.ServeHTTP(w, r)
68+
69+
resp := w.Result()
70+
if resp.StatusCode != http.StatusOK {
71+
t.Errorf("Response code is %v", resp.StatusCode)
72+
}
73+
}
74+
```
75+
首先跟待测试代码一样,配置上路由,对 `/topic/` 的请求都交由 `handleRequest ` 处理。
76+
77+
```go
78+
mux := http.NewServeMux()
79+
mux.HandleFunc("/topic/", handleRequest)
80+
```
81+
82+
因为 `handlePost` 的签名是 `func handlePost(w http.ResponseWriter, r *http.Request) error`,为了测试它,我们必须创建 `http.ResponseWriter``http.Request` 的实例。
83+
84+
接下来的代码就是创建一个 `http.Request` 实例 和 一个 `http.ReponseWriter` 的实例。这里的关键是,`httptest` 为我们提供了一个 `http.ReponseWriter` 接口的实现结构:`httptest.ReponseRecorder`,通过它可以得到一个 `http.ReponseWriter`
85+
86+
```go
87+
reader := strings.NewReader(`{"title":"The Go Standard Library","content":"It contains many packages."}`)
88+
r, _ := http.NewRequest(http.MethodPost, "/topic/", reader)
89+
90+
w := httptest.NewRecorder()
91+
```
92+
93+
准备好之后,可以测试目标函数了。这里,我们没有直接调用 `handlePost(w, r)`,而是调用 `mux.ServeHTTP(w, r)`,实际上这里直接调用 `handlePost(w, r)` 也是可以的,但调用 `mux.ServeHTTP(w, r)` 更完整的测试了整个流程。`mux.ServeHTTP(w, r)` 最终会调用 `handlePost(w, r)`
94+
95+
最后,通过 `go test -v` 运行测试。
96+
97+
查、改和删帖子的接口测试代码类似,比如,`handleGet` 的测试代码如下:
98+
99+
```go
100+
func TestHandleGet(t *testing.T) {
101+
mux := http.NewServeMux()
102+
mux.HandleFunc("/topic/", handleRequest)
103+
104+
r, _ := http.NewRequest(http.MethodGet, "/topic/1", nil)
105+
106+
w := httptest.NewRecorder()
107+
108+
mux.ServeHTTP(w, r)
109+
110+
resp := w.Result()
111+
if resp.StatusCode != http.StatusOK {
112+
t.Errorf("Response code is %v", resp.StatusCode)
113+
}
114+
115+
topic := new(Topic)
116+
json.Unmarshal(w.Body.Bytes(), topic)
117+
if topic.Id != 1 {
118+
t.Errorf("Cannot get topic")
119+
}
120+
}
121+
```
122+
123+
*注意:因为数据没有落地存储,为了保证后面的测试正常,请将 `TestHandlePost` 放在最前面。*
124+
125+
## 测试代码改进
126+
127+
细心的朋友应该会发现,上面的测试代码有重复,比如:
128+
129+
```go
130+
mux := http.NewServeMux()
131+
mux.HandleFunc("/topic/", handleRequest)
132+
```
133+
134+
还有:`w := httptest.NewRecorder()`
135+
136+
这正好是前面学习的 `setup` 可以做的事情,因此可以使用 `TestMain` 来做重构。
137+
138+
```go
139+
var w *httptest.ResponseRecorder
140+
141+
func TestMain(m *testing.M) {
142+
http.DefaultServeMux.HandleFunc("/topic/", handleRequest)
143+
144+
w = httptest.NewRecorder()
145+
146+
os.Exit(m.Run())
147+
}
148+
```
149+
150+
# 导航 #
151+
152+
- 上一节:[testing - 其他功能](09.5.md)
153+
- [第十章 进程、线程与 goroutine](chapter10/10.0.md)
7154

8155

9-

code/src/chapter09/httptest/data.go

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"time"
6+
)
7+
8+
// 保存 Topic,没有考虑并发问题
9+
var TopicCache = make([]*Topic, 0, 16)
10+
11+
type Topic struct {
12+
Id int `json:"id"`
13+
Title string `json:"title"`
14+
Content string `json:"content"`
15+
CreatedAt time.Time `json:"created_at"`
16+
}
17+
18+
func FindTopic(id int) (*Topic, error) {
19+
if err := checkIndex(id); err != nil {
20+
return nil, err
21+
}
22+
23+
return TopicCache[id-1], nil
24+
}
25+
26+
func (t *Topic) Create() error {
27+
t.Id = len(TopicCache) + 1
28+
t.CreatedAt = time.Now()
29+
TopicCache = append(TopicCache, t)
30+
return nil
31+
}
32+
33+
func (t *Topic) Update() error {
34+
if err := checkIndex(t.Id); err != nil {
35+
return err
36+
}
37+
TopicCache[t.Id-1] = t
38+
return nil
39+
}
40+
41+
// 简单的将对应的 slice 位置置为 nil
42+
func (t *Topic) Delete() error {
43+
if err := checkIndex(t.Id); err != nil {
44+
return err
45+
}
46+
TopicCache[t.Id-1] = nil
47+
return nil
48+
}
49+
50+
func checkIndex(id int) error {
51+
if id > 0 && len(TopicCache) <= id-1 {
52+
return errors.New("The topic is not exists!")
53+
}
54+
55+
return nil
56+
}

code/src/chapter09/httptest/server.go

+101-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,113 @@
11
package main
22

33
import (
4-
"log"
4+
"encoding/json"
55
"net/http"
6+
"path"
7+
"strconv"
68
)
79

810
func main() {
9-
http.Handle("/httptest", new(MyHandler))
11+
http.HandleFunc("/topic/", handleRequest)
1012

11-
log.Fatal(http.ListenAndServe(":8080", nil))
13+
http.ListenAndServe(":2017", nil)
1214
}
1315

14-
type MyHandler func(http.ResponseWriter, *http.Request)
16+
// main handler function
17+
func handleRequest(w http.ResponseWriter, r *http.Request) {
18+
var err error
19+
switch r.Method {
20+
case http.MethodGet:
21+
err = handleGet(w, r)
22+
case http.MethodPost:
23+
err = handlePost(w, r)
24+
case http.MethodPut:
25+
err = handlePut(w, r)
26+
case http.MethodDelete:
27+
err = handleDelete(w, r)
28+
}
29+
if err != nil {
30+
http.Error(w, err.Error(), http.StatusInternalServerError)
31+
return
32+
}
33+
}
34+
35+
// 获取一个帖子
36+
// 如 GET /topic/1
37+
func handleGet(w http.ResponseWriter, r *http.Request) error {
38+
id, err := strconv.Atoi(path.Base(r.URL.Path))
39+
if err != nil {
40+
return err
41+
}
42+
topic, err := FindTopic(id)
43+
if err != nil {
44+
return err
45+
}
46+
output, err := json.MarshalIndent(&topic, "", "\t\t")
47+
if err != nil {
48+
return err
49+
}
50+
w.Header().Set("Content-Type", "application/json")
51+
w.Write(output)
52+
return nil
53+
}
54+
55+
// 增加一个帖子
56+
// POST /topic/
57+
func handlePost(w http.ResponseWriter, r *http.Request) (err error) {
58+
body := make([]byte, r.ContentLength)
59+
r.Body.Read(body)
60+
var topic = new(Topic)
61+
err = json.Unmarshal(body, &topic)
62+
if err != nil {
63+
return
64+
}
65+
66+
err = topic.Create()
67+
if err != nil {
68+
return
69+
}
70+
w.WriteHeader(http.StatusOK)
71+
return
72+
}
73+
74+
// 更新一个帖子
75+
// PUT /topic/1
76+
func handlePut(w http.ResponseWriter, r *http.Request) error {
77+
id, err := strconv.Atoi(path.Base(r.URL.Path))
78+
if err != nil {
79+
return err
80+
}
81+
topic, err := FindTopic(id)
82+
if err != nil {
83+
return err
84+
}
85+
body := make([]byte, r.ContentLength)
86+
r.Body.Read(body)
87+
json.Unmarshal(body, topic)
88+
err = topic.Update()
89+
if err != nil {
90+
return err
91+
}
92+
w.WriteHeader(http.StatusOK)
93+
return nil
94+
}
1595

16-
func (self MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
17-
self(w, r)
96+
// 删除一个帖子
97+
// DELETE /topic/1
98+
func handleDelete(w http.ResponseWriter, r *http.Request) (err error) {
99+
id, err := strconv.Atoi(path.Base(r.URL.Path))
100+
if err != nil {
101+
return
102+
}
103+
topic, err := FindTopic(id)
104+
if err != nil {
105+
return
106+
}
107+
err = topic.Delete()
108+
if err != nil {
109+
return
110+
}
111+
w.WriteHeader(http.StatusOK)
112+
return
18113
}

0 commit comments

Comments
 (0)