GET /players/{name}
应该返回一个表示获胜总数的数字POST /players/{name}
应该为玩家赢得游戏记录一次得分,并随着每次 POST
递增快速跑通测试,为满足必要条件暂时犯错亦可。
GET
一个玩家,而且似乎很难知道 POST
在没有 GET
的情况下是否工作。GET
需要一个类似 PlayerStore
的东西来获得玩家的分数。这应该是一个接口,所以测试时我们可以创建一个简单的存根来测试代码而无需实现任何真实的存储机制。POST
,我们可以 监听 PlayerStore
的调用以确保它能正确存储玩家。我们的存储实现不会与检索相关联。PlayerServer
写一个测试函数,让它接受上面提到的两个参数。发送的请求将得到一个期望为 20 的玩家得分。Request
来发送请求,并期望监听到 handler 向 ResponseWriter
写入了什么。http.NewRequest
来创建一个请求。第一个参数是请求方法,第二个是请求路径。nil
是请求实体,不过在这个场景中不用发送请求实体。net/http/httptest
自带一个名为 ResponseRecorder
的监听器,所以我们可以用这个。它有很多有用的方法可以检查应答被写入了什么。./server_test.go:13:2: undefined: PlayerServer
PlayerServer
:Greet
函数接触到了 HTTP 服务器。我们知道了 net/http 的 ResponseWriter
也实现了 io Writer
,所以我们可以用 fmt.Fprint
发送字符串来作为 HTTP 应答。go build
把目录中所有 .go
文件编译成一个可运行的程序,然后你可以用 ./myprogram
来运行它。http.HandlerFunc
Handler
接口是为创建服务器而需要实现的。一般来说,我们通过创建 struct
来实现接口。然而,struct 的用途是用于存储数据,但是目前没有状态可存储,因此创建一个 struct 感觉不太对。HandlerFunc 类型是一个允许将普通函数用作 HTTP handler 的适配器。如果 f 是具有适当签名的函数,则 HandlerFunc(f) 是一个调用 f 的 Handler。
PlayerServer
函数,使它现在符合 Handler
。http.ListenAndServe(":5000"...)
ListenAndServe
会在 Handler
上监听一个端口。如果端口已被占用,它会返回一个 error
,所以我们在一个 if
语句中捕获出错的场景并记录下来。当然,我们需要一种存储机制来控制不同玩家的得分。在这个测试中那些值看起来很武断,这有点儿怪。
r.URL.Path
返回请求的路径,然后我们用切片语法得到 /players/
最后的斜杠后的路径。这不太靠谱,但现在起码可行。PlayerServer
GetPlayerScore
中,这就是使用接口重构的正确方法。PlayerServer
能够使用 PlayerStore
,它需要一个引用。现在是改变架构的时候了,将 PlayerServer
改成一个 struct
。Handler
接口,并把它放到已有的 handler 中。store.GetPlayerStore
来获得得分,而不是我们定义的本地函数(现在可以删除它了)。./main.go:9:58: type PlayerServer is not an expression
PlayerServer
实例,然后调用它的 ServeHTTP
方法。main.go
还是因同样的原因编译失败。PlayerStore
,我们需要创建一个存根。map
创建键/值存储是一种比较简便快捷的方式。现在让我们在测试中创建其中一个 store
并将其传给 PlayerServer
。store
的引入,代码意图现在更清晰了。我们告诉 reader,在 PlayerStore
中有数据了,当你将它用在 PlayerServer
时,你应该得到正确的应答。http://localhost:5000/players/Pepper
,你会得到一个异常的应答。PlayerStore
。go build
并访问同一个 URL 你应该得到一个 "123"
的应答。尽管这样不太对,但在我们实现数据存储前已经是最好不过的了。POST /players/{name}
的场景POST
场景让我们更接近“测试通过”,但我觉得首先解决玩家不存在的情景会更容易,因为我们已经处于这种情况。我们稍后会讨论其余的事情。StatusNotFound
但所有的测试却都通过了!StatusOK
。assertStatus
的辅助函数来提高编码效率。PlayerServer
只返回 “not found” 的问题。GET /players/{name}
。一旦这个通过,我们就可以开始测试 handler 与 store 的交互。if
语句来测试请求方法就可以了。ServeHTTP
的路由更加清晰,这意味着我们下一次存储迭代只能在 processWin
中。POST /players/{name}
时 PlayerStore
被告知要做一次获胜记录。RecordWin
方法扩展 StubPlayerStore
然后监视它的调用来实现这一点。StubPlayerStore
的代码,因为我们添加了一个新字段RecordWin
,我们需要修改 PlayerStore
接口来更新 PlayerServer
。InMemoryPlayerStore
加上那个方法。PlayerStore
有 RecordWin
方法,那我们可以在 PlayerServer
中调用它。"Bob"
并不是我们想要发送给 RecordWin
的,所以让我们进一步完善测试。winCalls
切片中有一个元素,我们可以安全地引用第一个元素并检查它是否等于 player
。processWin
接收一个 http.Request
参数来从 URL 中获取玩家的名字。这样我们就可以用正确的值调用 store
来使测试通过。PlayerStore
。没关系,通过专注 handler 我们已经确定了需要的接口,而不是妄图对它进行预先设计。InMemoryPlayerStore
编写一些测试,但在实现一种更强大的持久化存储玩家得分的方案(即数据库)之前,这只是暂时的。PlayerServer
和 InMemoryPlayerStore
编写一个集成测试来完成功能。这将让我们确保程序能正常工作,而无需直接测试 InMemoryPlayerStore
。不仅如此,当我们开始使用数据库实现 PlayerStore
时,我们可以使用相同的集成测试来测试该实现。InMemoryPlayerStore
和 PlayerServer
。response
),因为我们要尝试并获得 player
的得分。InMemoryPlayerStore
)。InMemoryPlayerStore
编写更具体的单元测试来帮我找出解决方案。InMemoryPlayerStore
结构中添加了 map[string]int
NewInMemoryPlayerStore
初始化了 store,并更新了集成测试来使用它(store := NewInMemoryPlayerStore()
)map
相关的操作main
改为使用 NewInMemoryPlayerStore()