-
GET /players/{name}
应该返回一个表示获胜总数的数字
-
POST /players/{name}
应该为玩家赢得游戏记录一次得分,并随着每次POST
递增
-
在任何给定时间保持问题都是小问题
- 不会陷入陷阱(rabbit holes)
先有鸡还是先有蛋
GET
一个玩家,而且似乎很难知道 POST
在没有 GET 的情况下是否工作。
-
GET
需要一个类似PlayerStore
的东西来获得玩家的分数。这应该是一个接口,所以测试时我们可以创建一个简单的存根来测试代码而无需实现任何真实的存储机制。
-
对于
POST
,我们可以 监听PlayerStore
的调用以确保它能正确存储玩家。我们的存储实现不会与检索相关联。
- 为了尽快让代码可运行,我们可以先在内存中写一个非常简单的实现,然后我们可以实现一个任何喜欢的存储机制。
先写测试
func ListenAndServe(addr string, handler Handler) error
这可以启动一个 web 服务器监听在一个端口上,为每个请求创建一个 Go 例程,并用一个 handler 处理这些请求。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
这个函数期望有两个参数输入,第一个是 响应 请求的,第二个是发送给服务器的 HTTP 请求。
我们为 PlayerServer
写一个测试函数,让它接受上面提到的两个参数。发送的请求将得到一个期望为 20 的玩家得分。
t.Run("returns Pepper's score", func(t *testing.T) {
request, _ := http.NewRequest(http.MethodGet, "/players/Pepper", nil)
response := httptest.NewRecorder()
PlayerServer(response, request)
got := response.Body.String()
want := "20"
if got != want { want '%s'", got, want)
}
})
为了测试服务器,我们需要通过 Request
来发送请求,并期望监听到 handler 向 ResponseWriter
写入了什么。
-
我们用
http.NewRequest
来创建一个请求。第一个参数是请求方法,第二个是请求路径。nil
是请求实体,不过在这个场景中不用发送请求实体。
-
net/http/httptest
自带一个名为ResponseRecorder
的监听器,所以我们可以用这个。它有很多有用的方法可以检查应答被写入了什么。
尝试运行测试
./server_test.go:13:2: undefined: PlayerServer
编写最少量的代码让测试运行起来,然后检查错误输出。
PlayerServer
func PlayerServer() {}
再运行一次
./server_test.go:13:14: too many arguments in call to PlayerServer
have (*httptest.ResponseRecorder, *http.Request)
want ()
给函数添加参数:
import "net/http"
func PlayerServer(w http.ResponseWriter, r *http.Request) {
}
代码可以编译了,只是测试还是失败的。
=== RUN TestGETPlayers/returns_Pepper's_score
--- FAIL: TestGETPlayers/returns_Pepper's_score (0.00s)
server_test.go:20: got '', want '20'
编写足够的代码让它通过
在依赖注入章节中,我们通过 Greet
函数接触到了 HTTP 服务器。我们知道了 net/http 的 ResponseWriter
也实现了 io Writer
,所以我们可以用 fmt.Fprint
发送字符串来作为 HTTP 应答。
func PlayerServer(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "20")
}
现在测试应该通过了。
完成框架(scaffolding)
- 我们要写真正能用的代码,而不是为了测试而写测试,代码能用才是王道。
- 当我们重构时,可能会修改程序的结构。我们希望确保这也作为增量方法的一部分反映在程序中。
创建一个新文件,写入以下代码。
package main
import (
"log"
"net/http"
)
func main() {
handler := http.HandlerFunc(PlayerServer)
if err := http.ListenAndServe(":5000", handler); err != nil {
log.Fatalf("could not listen on port 5000 %v", err)
}
}
目前我们所有程序代码都在一个文件里,然而这不是那种把代码拆分为多个文件的大型项目的最佳实践。
通过 go build
把目录中所有 .go
文件编译成一个可运行的程序,然后你可以用 ./myprogram
来运行它。
http.HandlerFunc
Handler
接口是为创建服务器而需要实现的。一般来说,我们通过创建 struct
来实现接口。然而,struct 的用途是用于存储数据,但是目前没有状态可存储,因此创建一个 struct 感觉不太对。
HandlerFunc 类型是一个允许将普通函数用作 HTTP handler 的适配器。如果 f 是具有适当签名的函数,则 HandlerFunc(f) 是一个调用 f 的 Handler。
type HandlerFunc func(ResponseWriter, *Request)
所以我们用它来封装 PlayerServer
函数,使它现在符合 Handler
。
http.ListenAndServe(":5000"...)
ListenAndServe
会在 Handler
上监听一个端口。如果端口已被占用,它会返回一个 error
,所以我们在一个 if
语句中捕获出错的场景并记录下来。
先写测试
我们添加另一个子测试来尝试为不同的玩家获取得分,来破坏之前硬编码的实现。
t.Run("returns Floyd's score", func(t *testing.T) {
request, _ := http.NewRequest(http.MethodGet, "/players/Floyd", nil)
response := httptest.NewRecorder()
PlayerServer(response, request)
got := response.Body.String()
want := "10"
if got != want {
t.Errorf("got '%s', want '%s'", got, want)
}
})
你或许在想:
当然,我们需要一种存储机制来控制不同玩家的得分。在这个测试中那些值看起来很武断,这有点儿怪。
注意,我们只是尽量合理地小步前进,所以现在只改善硬编码的问题。
尝试运行测试
=== RUN TestGETPlayers/returns_Pepper's_score
--- PASS: TestGETPlayers/returns_Pepper's_score (0.00s)
=== RUN TestGETPlayers/returns_Floyd's_score
--- FAIL: TestGETPlayers/returns_Floyd's_score (0.00s)
server_test.go:34: got '20', want '10'
编写足够的代码让它通过
func PlayerServer(w http.ResponseWriter, r *http.Request) {
player := r.URL.Path[len("/players/"):]
if player == "Pepper" {
fmt.Fprint(w, "20")
return
}
if player == "Floyd" {
fmt.Fprint(w, "10")
return
}
}
r.URL.Path
返回请求的路径,然后我们用切片语法得到 /players/
最后的斜杠后的路径。这不太靠谱,但现在起码可行。
重构
我们可以通过将分数检索分离为函数来简化 PlayerServer
func PlayerServer(w http.ResponseWriter, r *http.Request) {
player := r.URL.Path[len("/players/"):]
fmt.Fprint(w, GetPlayerScore(player))
}
func GetPlayerScore(name string) string {
if name == "Pepper" {
return "20"
}
if name == "Floyd" {
return "10"
}
return ""
}
我们可以创建一些辅助函数来避免测试中的重复代码
func TestGETPlayers(t *testing.T) {
t.Run("returns Pepper's score", func(t *testing.T) {
request := newGetScoreRequest("Pepper")
response := httptest.NewRecorder()
PlayerServer(response, request)
assertResponseBody(t, response.Body.String(), "20")
})
t.Run("returns Floyd's score", func(t *testing.T) {
request := newGetScoreRequest("Floyd")
response := httptest.NewRecorder()
PlayerServer(response, request)
assertResponseBody(t, response.Body.String(), "10")
})
}
func newGetScoreRequest(name string) *http.Request {
req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/players/%s", name), nil)
return req
}
func assertResponseBody(t *testing.T, got, want string) {
t.Helper()
if got != want {
t.Errorf("response body is wrong, got '%s' want '%s'", got, want)
}
}
GetPlayerScore
中,这就是使用接口重构的正确方法。
type PlayerStore interface {
GetPlayerScore(name string) int
}
为了让 PlayerServer
能够使用 PlayerStore
,它需要一个引用。现在是改变架构的时候了,将 PlayerServer
改成一个 struct
。
type PlayerServer struct {
store PlayerStore
}
最后,我们通过给这个 struct 添加一个方法来实现 Handler
接口,并把它放到已有的 handler 中。
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
player := r.URL.Path[len("/players/"):]
fmt.Fprint(w, p.store.GetPlayerScore(player))
}
store.GetPlayerStore
来获得得分,而不是我们定义的本地函数(现在可以删除它了)。
type PlayerStore interface {
GetPlayerScore(name string) int
}
type PlayerServer struct {
store PlayerStore
}
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
player := r.URL.Path[len("/players/"):]
fmt.Fprint(w, p.store.GetPlayerScore(player))
}
解决问题
./main.go:9:58: type PlayerServer is not an expression
PlayerServer
实例,然后调用它的 ServeHTTP
func TestGETPlayers(t *testing.T) {
server := &PlayerServer{}
t.Run("returns Pepper's score", func(t *testing.T) {
request := newGetScoreRequest("Pepper")
response := httptest.NewRecorder()
server.ServeHTTP(response, request)
assertResponseBody(t, response.Body.String(), "20")
})
t.Run("returns Floyd's score", func(t *testing.T) {
request := newGetScoreRequest("Floyd")
response := httptest.NewRecorder()
server.ServeHTTP(response, request)
assertResponseBody(t, response.Body.String(), "10")
})
}
main.go
还是因同样的原因编译失败。
func main() {
server := &PlayerServer{}
if err := http.ListenAndServe(":5000", server); err != nil {
log.Fatalf("could not listen on port 5000 %v", err)
}
}
最后编译终于通过了,但测试还没通过
=== RUN TestGETPlayers/returns_the_Pepper's_score
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
这是因为在测试中我还没传入 PlayerStore
,我们需要创建一个存根。
type StubPlayerStore struct {
scores map[string]int
}
func (s *StubPlayerStore) GetPlayerScore(name string) int {
score := s.scores[name]
return score
}
使用 map
创建键/值存储是一种比较简便快捷的方式。现在让我们在测试中创建其中一个 store
并将其传给 PlayerServer
。
func TestGETPlayers(t *testing.T) {
store := StubPlayerStore{
map[string]int{
"Pepper": 20,
"Floyd": 10,
},
}
server := &PlayerServer{&store}
t.Run("returns Pepper's score", func(t *testing.T) {
request := newGetScoreRequest("Pepper")
response := httptest.NewRecorder()
server.ServeHTTP(response, request)
assertResponseBody(t, response.Body.String(), "20")
})
t.Run("returns Floyd's score", func(t *testing.T) {
request := newGetScoreRequest("Floyd")
response := httptest.NewRecorder()
server.ServeHTTP(response, request)
assertResponseBody(t, response.Body.String(), "10")
})
}
store
的引入,代码意图现在更清晰了。我们告诉 reader,在 PlayerStore
中有数据了,当你将它用在 PlayerServer
时,你应该得到正确的应答。
运行程序
现在测试通过了,要完成这个重构,我们要做的最后一件事是检查应用程序是否正常工作。该程序应该可以启动,但如果你尝试访问 http://localhost:5000/players/Pepper
,你会得到一个异常的应答。
原因是我们没传入 PlayerStore
。
我们要实现一个,但现在有点儿困难,因为我们还没存储有用的数据,所以我们先通过硬编码实现。
type InMemoryPlayerStore struct{}
func (i *InMemoryPlayerStore) GetPlayerScore(name string) int {
return 123
}
func main() {
server := &PlayerServer{&InMemoryPlayerStore{}}
if err := http.ListenAndServe(":5000", server); err != nil {
log.Fatalf("could not listen on port 5000 %v", err)
}
}
如果你再次运行 go build
并访问同一个 URL 你应该得到一个 "123"
的应答。尽管这样不太对,但在我们实现数据存储前已经是最好不过的了。
关于下一步该做什么,我们有几个选择
-
处理玩家不存在的场景
-
处理
POST /players/{name}
的场景
- 主应用程序能运行但实际上还不算真正能用。我们不得不手动测试才能看到问题
POST
场景让我们更接近“测试通过”,但我觉得首先解决玩家不存在的情景会更容易,因为我们已经处于这种情况。我们稍后会讨论其余的事情。
先写测试
添加一个玩家不存在的测试用例
t.Run("returns 404 on missing players", func(t *testing.T) {
request := newGetScoreRequest("Apollo")
response := httptest.NewRecorder()
server.ServeHTTP(response, request)
got := response.Code
want := http.StatusNotFound
if got != want {
t.Errorf("got status %d want %d", got, want)
}
})
尝试运行测试
=== RUN TestGETPlayers/returns_404_on_missing_players
--- FAIL: TestGETPlayers/returns_404_on_missing_players (0.00s)
server_test.go:56: got status 200 want 404
编写最少量的代码让测试运行起来,然后检查错误输出
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
player := r.URL.Path[len("/players/"):]
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, p.store.GetPlayerScore(player))
}
StatusNotFound
但所有的测试却都通过了!
StatusOK
。
func TestGETPlayers(t *testing.T) {
store := StubPlayerStore{
map[string]int{
"Pepper": 20,
"Floyd": 10,
},
}
server := &PlayerServer{&store}
t.Run("returns Pepper's score", func(t *testing.T) {
request := newGetScoreRequest("Pepper")
response := httptest.NewRecorder()
server.ServeHTTP(response, request)
assertStatus(t, response.Code, http.StatusOK)
assertResponseBody(t, response.Body.String(), "20")
})
t.Run("returns Floyd's score", func(t *testing.T) {
request := newGetScoreRequest("Floyd")
response := httptest.NewRecorder()
server.ServeHTTP(response, request)
assertStatus(t, response.Code, http.StatusOK)
assertResponseBody(t, response.Body.String(), "10")
})
t.Run("returns 404 on missing players", func(t *testing.T) {
request := newGetScoreRequest("Apollo")
response := httptest.NewRecorder()
server.ServeHTTP(response, request)
assertStatus(t, response.Code, http.StatusNotFound)
})
}
func assertStatus(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("did not get correct status, got %d, want %d", got, want)
}
}
func newGetScoreRequest(name string) *http.Request {
req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/players/%s", name), nil)
return req
}
func assertResponseBody(t *testing.T, got, want string) {
t.Helper()
if got != want {
t.Errorf("response body is wrong, got '%s' want '%s'", got, want)
}
}
assertStatus
的辅助函数来提高编码效率。
PlayerServer 只返回 “not found” 的问题。
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
player := r.URL.Path[len("/players/"):]
score := p.store.GetPlayerScore(player)
if score == 0 {
w.WriteHeader(http.StatusNotFound)
}
fmt.Fprint(w, score)
}
存储得分
现在我们可以从 store 中查询得分了,能够存储新的得分就很有意义了。
先写测试
func TestStoreWins(t *testing.T) {
store := StubPlayerStore{
map[string]int{},
}
server := &PlayerServer{&store}
t.Run("it returns accepted on POST", func(t *testing.T) {
request, _ := http.NewRequest(http.MethodPost, "/players/Pepper", nil)
response := httptest.NewRecorder()
server.ServeHTTP(response, request)
assertStatus(t, response.Code, http.StatusAccepted)
})
}
首先,我们检查使用 POST 访问指定路径时是否返回了正确的状态码。这迫使我们要实现可以接受不同类型请求的功能,以不同的方式处理 GET /players/{name}
。一旦这个通过,我们就可以开始测试 handler 与 store 的交互
尝试运行测试
=== RUN TestStoreWins/it_returns_accepted_on_POST
--- FAIL: TestStoreWins/it_returns_accepted_on_POST (0.00s)
server_test.go:70: did not get correct status, got 404, want 202
编写足够的代码让它通过
注意我们故意写错,所以用一个 if
语句来测试请求方法就可以了。
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
w.WriteHeader(http.StatusAccepted)
return
}
player := r.URL.Path[len("/players/"):]
score := p.store.GetPlayerScore(player)
if score == 0 {
w.WriteHeader(http.StatusNotFound)
}
fmt.Fprint(w, score)
}
重构
现在 handler 看起来有点儿乱了。让我们用下面的代码重写,以便更容易理解并将不同的功能拆分到新函数中
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
p.processWin(w)
case http.MethodGet:
p.showScore(w, r)
}
}
func (p *PlayerServer) showScore(w http.ResponseWriter, r *http.Request) {
player := r.URL.Path[len("/players/"):]
score := p.store.GetPlayerScore(player)
if score == 0 {
w.WriteHeader(http.StatusNotFound)
}
fmt.Fprint(w, score)
}
func (p *PlayerServer) processWin(w http.ResponseWriter) {
w.WriteHeader(http.StatusAccepted)
}
ServeHTTP
的路由更加清晰,这意味着我们下一次存储迭代只能在 processWin
中。
POST /players/{name}
时 PlayerStore
被告知要做一次获胜记录。
先写测试
我们可以通过使用新的 RecordWin
方法扩展 StubPlayerStore
然后监视它的调用来实现这一点。
type StubPlayerStore struct {
scores map[string]int
winCalls []string
}
func (s *StubPlayerStore) GetPlayerScore(name string) int {
score := s.scores[name]
return score
}
func (s *StubPlayerStore) RecordWin(name string) {
s.winCalls = append(s.winCalls, name)
}
现在在测试中扩展以检查启动的调用次数
func TestStoreWins(t *testing.T) {
store := StubPlayerStore{
map[string]int{},
}
server := &PlayerServer{&store}
t.Run("it records wins when POST", func(t *testing.T) {
request := newPostWinRequest("Pepper")
response := httptest.NewRecorder()
server.ServeHTTP(response, request)
assertStatus(t, response.Code, http.StatusAccepted)
if len(store.winCalls) != 1 {
t.Errorf("got %d calls to RecordWin want %d", len(store.winCalls), 1)
}
})
}
func newPostWinRequest(name string) *http.Request {
req, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("/players/%s", name), nil)
return req
}
尝试运行测试
./server_test.go:26:20: too few values in struct initializer
./server_test.go:65:20: too few values in struct initializer
编写最少量的代码让测试运行起来,然后检查错误输出
我们需要更新创建 StubPlayerStore
的代码,因为我们添加了一个新字段
store := StubPlayerStore{
map[string]int{},
nil,
}
--- FAIL: TestStoreWins (0.00s)
--- FAIL: TestStoreWins/it_records_wins_when_POST (0.00s)
server_test.go:80: got 0 calls to RecordWin want 1
编写足够的代码让它通过
RecordWin
,我们需要修改 PlayerStore
接口来更新 PlayerServer
type PlayerStore interface {
GetPlayerScore(name string) int
RecordWin(name string)
}
修改后程序不能编译了
./main.go:17:46: cannot use InMemoryPlayerStore literal (type *InMemoryPlayerStore) as type PlayerStore in field value:
*InMemoryPlayerStore does not implement PlayerStore (missing RecordWin method)
编译器告诉我们哪里出错了。我们给 InMemoryPlayerStore
加上那个方法。
type InMemoryPlayerStore struct{}
func (i *InMemoryPlayerStore) RecordWin(name string) {}
PlayerStore
有 RecordWin
方法,那我们可以在 PlayerServer
func (p *PlayerServer) processWin(w http.ResponseWriter) {
p.store.RecordWin("Bob")
w.WriteHeader(http.StatusAccepted)
}
运行测试,现应该通过了!显然 "Bob"
并不是我们想要发送给 RecordWin
的,所以让我们进一步完善测试。
先写测试
t.Run("it records wins on POST", func(t *testing.T) {
player := "Pepper"
request := newPostWinRequest(player)
response := httptest.NewRecorder()
server.ServeHTTP(response, request)
assertStatus(t, response.Code, http.StatusAccepted)
if len(store.winCalls) != 1 {
t.Fatalf("got %d calls to RecordWin want %d", len(store.winCalls), 1)
}
if store.winCalls[0] != player {
t.Errorf("did not store correct winner got '%s' want '%s'", store.winCalls[0], player)
}
})
现在我们知道 winCalls
切片中有一个元素,我们可以安全地引用第一个元素并检查它是否等于 player
。
尝试运行测试
=== RUN TestStoreWins/it_records_wins_on_POST
--- FAIL: TestStoreWins/it_records_wins_on_POST (0.00s)
server_test.go:86: did not store correct winner got 'Bob' want 'Pepper'
编写足够的代码让它通过
func (p *PlayerServer) processWin(w http.ResponseWriter, r *http.Request) {
player := r.URL.Path[len("/players/"):]
p.store.RecordWin(player)
w.WriteHeader(http.StatusAccepted)
}
我们让 processWin
接收一个 http.Request
参数来从 URL 中获取玩家的名字。这样我们就可以用正确的值调用 store
来使测试通过。
重构
我们可以稍微精简一下这段代码,因为我们在两个地方以相同的方式获取玩家名称
func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
player := r.URL.Path[len("/players/"):]
switch r.Method {
case http.MethodPost:
p.processWin(w, player)
case http.MethodGet:
p.showScore(w, player)
}
}
func (p *PlayerServer) showScore(w http.ResponseWriter, player string) {
score := p.store.GetPlayerScore(player)
if score == 0 {
w.WriteHeader(http.StatusNotFound)
}
fmt.Fprint(w, score)
}
func (p *PlayerServer) processWin(w http.ResponseWriter, player string) {
p.store.RecordWin(player)
w.WriteHeader(http.StatusAccepted)
}
PlayerStore
。没关系,通过专注 handler 我们已经确定了需要的接口,而不是妄图对它进行预先设计。
InMemoryPlayerStore
编写一些测试,但在实现一种更强大的持久化存储玩家得分的方案(即数据库)之前,这只是暂时的。
PlayerServer
和 InMemoryPlayerStore
编写一个集成测试来完成功能。这将让我们确保程序能正常工作,而无需直接测试 InMemoryPlayerStore
。不仅如此,当我们开始使用数据库实现 PlayerStore时,我们可以使用相同的集成测试来测试该实现。
集成测试
-
集成测试更难编写
-
测试失败时,可能很难知道原因(通常它是集成测试组件中的错误),因此可能更难修复
-
有时运行较慢(因为它们通常与“真实”组件一起使用,比如数据库)
先写测试
func TestRecordingWinsAndRetrievingThem(t *testing.T) {
store := InMemoryPlayerStore{}
server := PlayerServer{&store}
player := "Pepper"
server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player))
server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player))
server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player))
response := httptest.NewRecorder()
server.ServeHTTP(response, newGetScoreRequest(player))
assertStatus(t, response.Code, http.StatusOK)
assertResponseBody(t, response.Body.String(), "3")
}
-
我们正在尝试集成两个组件:
InMemoryPlayerStore
和PlayerServer
。
-
然后我们发起 3 个请求,为玩家记录 3 次获胜。我们并不太关心测试中的返回状态码,因为和集成得好不好无关。
- 我们真正关心的是下一个响应(所以我们用变量存储
response
),因为我们要尝试并获得player
的得分
尝试运行测试
--- FAIL: TestRecordingWinsAndRetrievingThem (0.00s)
server_integration_test.go:24: response body is wrong, got '123' want '3'
编写足够的代码让它通过
InMemoryPlayerStore
)。
InMemoryPlayerStore编写更具体的单元测试来帮我找出解决方案
func NewInMemoryPlayerStore() *InMemoryPlayerStore {
return &InMemoryPlayerStore{map[string]int{}}
}
type InMemoryPlayerStore struct{
store map[string]int
}
func (i *InMemoryPlayerStore) RecordWin(name string) {
i.store[name]++
}
func (i *InMemoryPlayerStore) GetPlayerScore(name string) int {
return i.store[name]
}
-
我们需要存储数据,所以我在
InMemoryPlayerStore
结构中添加了map[string]int
-
为方便起见,我已经让
NewInMemoryPlayerStore
初始化了 store,并更新了集成测试来使用它(store := NewInMemoryPlayerStore()
)
- 代码的其余部分只是
map
相关的操作
main
改为使用 NewInMemoryPlayerStore()
package main
import (
"log"
"net/http"
)
func main() {
server := &PlayerServer{NewInMemoryPlayerStore()}
if err := http.ListenAndServe(":5000", server); err != nil {
log.Fatalf("could not listen on port 5000 %v", err)
}
}
curl
来测试它。
-
运行几次这条命令
curl -X POST http://localhost:5000/players/Pepper
,你换成别的玩家名称也可以
-
用
curl http://localhost:5000/players/Pepper
获取玩家得分
-
选择一种存储机制(Bolt? Mongo? Postgres? File system?)
-
用一个
PostgresPlayerStore
函数来实现PlayerStore
-
通过 TDD 来确保它能正常工作
- 接入集成测试中,检查它是否依然正常工作
- 最终接入到主程序中
总结
http.Handler
-
通过实现这个接口来创建 web 服务器
-
用
http.HandlerFunc
把普通函数转化为http.Handler
-
把
httptest.NewRecorder
作为一个ResponseWriter
传进去,这样让你可以监视 handler 发送了什么响应
-
使用
http.NewRequest
构建对服务器的请求
接口,模拟和依赖注入
-
允许你以小步快速迭代的方式逐步构建系统
- 允许你开发需要存储 handler 而无需实际存储
- TDD 驱使你实现需要的接口
暂时提交有问题的代码,然后重构(然后提交到版本控制)
-
你需要将编译失败或测试失败视为一种红色警告状态,而且需要尽快摆脱它。
-
只编写必要的代码,然后重构优化代码。
-
在代码未编译或测试失败时尝试进行太多更改会让你面临陷入复杂问题的风险。
-
坚持这种小步快速迭代的方法编写测试,在处理复杂系统时,小的更改有助于提升系统可维护性。