现在一些测试通过了,我们应该把它嵌入到 main 中。记住,我们应该尽可能快地使用完全集成的工作软件。
在 main.go 中添加以下内容并运行它。(你可能必须调整第二个依赖项的路径来适配你的环境)
package main
import (
"fmt"
"github.com/quii/learn-go-with-tests/command-line/v3"
"log"
"os"
)
const dbFileName = "game.db.json"
func main() {
fmt.Println("Let's play poker")
fmt.Println("Type {Name} wins to record a win")
db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Fatalf("problem opening %s %v", dbFileName, err)
}
store, err := poker.NewFileSystemPlayerStore(db)
if err != nil {
log.Fatalf("problem creating file system player store, %v ", err)
}
game := poker.CLI{store, os.Stdin}
game.PlayPoker()
}
你应该会得到一个报错:
command-line/v3/cmd/cli/main.go:32:25: implicit assignment of unexported field 'playerStore' in poker.CLI literal
command-line/v3/cmd/cli/main.go:32:34: implicit assignment of unexported field 'in' in poker.CLI literal
这是因为我们试图在 CLI 中分配 playerStore 和 in 字段。这些是未导出的(私有)字段。我们可以在测试代码中执行此操作,因为测试与 CLI(poker)在同一个包中。但是 main 是在 main 包中,所以它没有访问权限。
我们现在遇到了关于包装设计的更多问题。为了测试,我们创建了未导出的存根和辅助函数,这些函数在 CLI_test 中不再可用,因为辅助函数是在 poker 包中的 _test.go 文件中定义的。
我们想让存根和辅助函数都公开吗?
这是一个主观的讨论。有人可能会争辩说,你不能为方便测试而污染 API。
在 Mitchell Hashimoto 的演示文稿 “Advanced Testing with Go” 中描述了 HashiCorp 如何提倡这样做以便用户可以在此基础上编写测试而无需重新发明轮子。在我们的例子中,这意味着任何使用 poker 包的人如果希望使用我们的代码,就不必创建自己的存根 PlayerStore。
我在其它项目中使用过这种技术,事实证明,在用户与软件包集成时可以非常有效地节省时间。
让我们创建一个叫 testing.go 的文件,并把存根和辅助函数放进去。
package poker
import "testing"
type StubPlayerStore struct {
scores map[string]int
winCalls []string
league []Player
}
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 (s *StubPlayerStore) GetLeague() League {
return s.league
}
func AssertPlayerWin(t *testing.T, store *StubPlayerStore, winner string) {
t.Helper()
if len(store.winCalls) != 1 {
t.Fatalf("got %d calls to RecordWin want %d", len(store.winCalls), 1)
}
if store.winCalls[0] != winner {
t.Errorf("did not store correct winner got '%s' want '%s'", store.winCalls[0], winner)
}
}
// todo for you - the rest of the helpers
func TestCLI(t *testing.T) {
t.Run("record chris win from user input", func(t *testing.T) {
in := strings.NewReader("Chris wins\n")
playerStore := &poker.StubPlayerStore{}
cli := &poker.CLI{playerStore, in}
cli.PlayPoker()
poker.AssertPlayerWin(t, playerStore, "Chris")
})
t.Run("record cleo win from user input", func(t *testing.T) {
in := strings.NewReader("Cleo wins\n")
playerStore := &poker.StubPlayerStore{}
cli := &poker.CLI{playerStore, in}
cli.PlayPoker()
poker.AssertPlayerWin(t, playerStore, "Cleo")
})
}
你会遇到和 main 一样的问题:
./CLI_test.go:15:26: implicit assignment of unexported field 'playerStore' in poker.CLI literal
./CLI_test.go:15:39: implicit assignment of unexported field 'in' in poker.CLI literal
./CLI_test.go:25:26: implicit assignment of unexported field 'playerStore' in poker.CLI literal
./CLI_test.go:25:39: implicit assignment of unexported field 'in' in poker.CLI literal
解决这个问题最简单的办法是创建一个构造函数,就像我们对其它类型一样:
func NewCLI(store PlayerStore, in io.Reader) *CLI {
return &CLI{
playerStore: store,
in: in,
}
}