/league
的访问地址应该按赢的次数排序返回玩家列表,她也很不满意。PlayerStore
的抽象。InMemoryPlayerStore
,以便在开发新的存储实现时还能通过集成测试。一旦我们确信新实现足以通过集成测试,我们会替换然后删除 InMemoryPlayerStore
。io.Reader
)、写入数据(io.Writer
)的接口,以及如何使用标准库来测试这些函数,而不必使用真正的文件。PlayerStore
,因此我们调用需要实现的方法来编写测试。我们将从 GetLeague
开始。strings.NewReader
会返回一个 Reader
,这是我们的 FileSystemStore
函数中用来读取数据的。在 main
中我们将打开一个文件,它也是一个 Reader
。FileSystemStore
Reader
参数,并且 GetLeague
函数还没有定义。reader
中读取了 JSON 数据league.go
的新文件,输入以下代码。server_test.go
的辅助函数 getLeagueFromResponse
中调用这个函数io.Reader
是如何定义的。Reader
已经到了结尾,没什么可读的了。我们需要一种方法让它回到开始位置。FileSystemStore
来替代这个接口吗?string.NewReader
也实现了 ReadSeeker
,所以我们不需要做任何其他的改变。GetPlayerScore
。./FileSystemStore_test.go:38:15: store.GetPlayerScore undefined (type FileSystemPlayerStore has no field or method GetPlayerScore)
league
寻找玩家并返回他们的得分RecordWin
来记录得分。Writer
,但我们已经有了 ReadSeeker
。我们可能有两个依赖项,但是标准库已经为我们提供了一个接口 ReadWriteSeeker
,我们需要对文件做的处理它都可以满足。strings.Reader
没有实现 ReadWriteSeeker
并不奇怪,这时我们该怎么办呢?*os.File
实现 ReadWriteSeeker
。好处是它变得更像集成测试,我们真的是从文件系统中读取和写入,所以我们对此更有信心。缺点是我们更喜欢单元测试,因为它们更快而且通常更简单。我们还需要做更多关于创建临时文件的工作,然后确保在测试之后删除它们。os.File
替换 strings.Reader
来使其他测试编译通过。ReadWriteSeeker
(文件),而且还返回一个函数。我们需要确保在测试完成后删除该文件。我们不希望将文件的细节泄露到测试中,因为它很容易出错,对读者来说也没什么意思。通过返回 removeFile
函数,我们可以处理辅助函数中的细节,调用者只需运行 deferred cleanDatabase()
。./FileSystemStore_test.go:67:8: store.RecordWin undefined (type FileSystemPlayerStore has no field or method RecordWin)
league[i].Wins++
而不是 player.Wins++
。i
)和该索引中的元素的副本。更改副本 Wins
的值不会对我们迭代的 league
产生任何影响。因此,我们需要通过使用 league[i]
来获取对实际值的引用,然后更改该值。GetPlayerScore
和 RecordWin
中,我们遍历 []Player
,按名称查找 player
。FileSystemStore
的内部重构这个公共代码,但对我来说,它可能还有用,我们可以将其提升为新的类型。到目前为止,操作「League」都是用 []Player
,但我们可以创造一种新的类型 League
。这使其他开发人员更容易理解,然后我们可以将有用的方法附加到该类型上供我们使用。league.go
添加一下代码League
的人都可以很容易找到给定的玩家。PlayerStore
接口以返回 League
而不是 []Player
。试着重新运行测试,你会遇到编译问题,因为我们修改了接口。但是这很容易修复,只要将返回类型从 []Player
改为 League
就行了。FileSystemStore
的方法。League
中找到其他可以被重构的功能。nil
的情况因为它找不到 player
。Store
。这将使我们对软件的工作更有信心,然后我们可以删除冗余的 InMemoryPlayerStore
。TestRecordingWinsAndRetrievingThem
中,替换之前的记录。InMemoryPlayerStore
了。main.go
现在会出现编译问题,这将促使我们现在在「真实」代码中使用我们的新存储。os.OpenFile
允许你定义打开文件的权限,在我们的例子中,O_RDWR
意味着我们想要读写权限,os.O_CREATE
是指如果文件不存在,则创建该文件。GetLeague()
或 GetPlayerScore()
时,我们就从头读取该文件,并将其解析为 JSON。我们不应该这样做,因为 FileSystemStore
完全负责 league 的状态。我们只是希望在开始时使用该文件来获取当前状态,并在数据更改时更新它。FileSystemStore
中,以便在读取中使用。f.league
。FileSystemPlayerStore
,因此只需通过调用我们新的构造函数来修复它们。Recordwin
时,我们返回到文件的开头,然后写入新的数据,但是如果新的数据比之前的数据要小怎么办?Tape
。创建一个包含以下内容的新文件Write
,因为它封装了 Seek
部分。这意味着我们的 FileSystemStore
可以只具有对 Writer
的引用。Tape
RecordWin
中删除 Seek
调用来获得我们想要的惊人回报。是的,这感觉并不多,但至少这意味着如果我们做任何其它类型的写入操作,我们可以依赖 write
来表达我们对它的需求。此外,它现在将允许我们分别测试可能存在问题的代码并修复它。tape_test.go
中:tape
来写,再读一遍,看看文件里有什么。os.File
文件有一个 truncate 函数,可以让我们有效地清空文件。我们应该能够调用它来得到我们想要的功能。tape
为以下内容io.ReadWriteSeeker
类型但是我们传入 *os.File
的地方失败。你现在应该可以自己修复这些问题了,但是如果你遇到困难,请检查源代码。TestTape_Write
的测试就应该通过了!RecordWin
中,我们有行 json.NewEncoder(f.database).Encode(f.league)
。RecordWin
中使用它。io.Reader
开始编写代码。因为那是对我们新的 PlayerStore
进行单元测试最简单的方法。当我们开发代码时,我们转而使用 io.ReadWriter
然后是 io.ReadWriteSeeker
。然后我们发现,除了 *os.File
之外,标准库中没有任何实际实现的东西。我们本来决定编写自己的或者使用开源的库,但是仅仅为测试使用临时文件就显得很实用了。Truncate
,它也在 *os.File
中。我们可以选择创建自己的接口实现这些需求。*os.File
之外的任何类型都是不现实的。所以我们不需要接口给我们的多态性。FileSystemStore.go
。我们在构造函数中有 league, _:= NewLeague(f.database)
。NewLeague
无法从我们提供的io.Reader
中解析 league,它会返回一个错误。