前置说明
本文以及接下来的文章都是来自 https://quii.gitbook.io/learn-go-with-tests/ 这个系列文章的。
主要分析说明的部分是 Build An Application
部分。
这并不是原文的翻译,而是记录一些自己的东西。混合式笔记,实现书中代码,加上我的思考
正文开始
看到标题,问题就来了,前面的文件结构都是按照我以前的认知来构建的,不过 golang 的结构,和跟以往了解的 php 有一些区别,虽然看了一些 golang 的结构,但是感觉使用起来不是很顺手。正好借着本篇文章在了解一下吧。
另外关于 TDD 的问题,我个人觉得随着经验的增长可以把每一步前进的快一点,比如最开始构造空方法,可以前进到,直接构建默认返回值的步骤。
今天的需求就是利用命令行实现一些请求的方法,而不仅仅是用 http。
哦吼,文章的第一段就是把 main 移动到 cmd/webserver 文件夹下,与我们的很相似,只要改个名就好了,展示一下我们当下的结构。另外包名文章中用的 poker,与我们的不一致,那我们就保持好我们当下的就好了。
现在开始在 cmd 下新建一个 cli 的文件夹,并且创建 main.go 文件。让其成为命令行的入口。
下面开始解决输入用户名记录获胜。从测试开始。新建 cli_test 注意,这个文件在根目录一下,而不是在 cmd 目录下。
1
2
3
4
5
6
7
8
9
|
func TestCli(t *testing.T) {
playerStore := &StubPlayerScore{}
cli := &CLI{playerStore}
cli.PlayPoker()
if len(playerStore.winCalls) != 1 {
t.Fatalf("expected a win call but didn't get any")
}
}
|
可以看到,与我们前面的测试是很相似的,不过就是初始化不同了,这边初始化的是 CLI,以前的是 Server,这里是 PlayPoker,以前是 ServerHTTP。而且,参数也是一致的,都是 store。目前是会报错的,因为没有 CLI,准备实现它。
1
2
3
4
5
6
7
8
|
// CLI.go
type CLI struct {
playerStore PlayerStore
}
func (c *CLI) PlayPoker() {
}
|
注意这个文件是在根目录下创建的。目前测试不会报错,但是测试会不通过,因为没有任何返回。
1
2
3
|
func (c *CLI) PlayPoker() {
c.playerStore.RecordWin("Cleo")
}
|
优化代码,现在测试可以通过了,但是无法通过输入的方式,传入用户名。所以接下来就要处理用户输入了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
type CLI struct {
playerStore PlayerStore
in io.Reader
}
func TestCli(t *testing.T) {
in := strings.NewReader("Chris wins\n")
playerStore := &StubPlayerScore{}
cli := &CLI{playerStore, in}
cli.PlayPoker()
if len(playerStore.winCalls) != 1 {
t.Fatalf("expected a win call but didn't get any")
}
got := playerStore.winCalls[0]
want := "Chirs"
if got != want {
t.Errorf("didn't record correct winner, got %q, want %q", got, want)
}
}
|
增加了一个 reader 来处理用户输入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
type CLI struct {
playerStore PlayerStore
in io.Reader
}
func (c *CLI) PlayPoker() {
reader := bufio.NewScanner(c.in)
reader.Scan()
c.playerStore.RecordWin(extractWinner(reader.Text()))
}
func extractWinner(userInput string) string {
return strings.Replace(userInput, " wins", "", 1)
}
|
这是优化后的代码,增加了一个 reader 来处理用户输入。为什么用 io.Reader 呢,这是个通用的接口,os.Stdin 也实现了这个接口。测试的时候用 strings.NewReader 来生成 reader 来测试。最后的 extractWinner 就是过滤一些文本,现在执行测试,可以通过了。另外 bufio,这个需要看下文档了。因为这个 io 实现了一些后续需要用到的东西。Scan 方法读取一行,Text 方法返回读取到的文本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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 := go_http_application_with_tdd.NewFileSystemPlayerStore(db)
if err != nil {
log.Fatalf("problem creating file system player store, %v", err)
}
game := go_http_application_with_tdd.CLI{store, os.Stdin}
game.PlayPoker()
}
|
现在处理运行部分,不过目前会报错,因为 store 和 in 都是私有的,要解决掉,后续文章讲述了如何更早的发现这个问题。
将测试文件的包名改为 包名_test 。这样就只能可以访问导出的公开的东西了。修改完包名后,就很多报错了。因为很多测试的数据都是在 _test 文件中定义的。创建一个 testing 文件用来保存我们的测试数据。把方法都独立出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
func TestCli(t *testing.T) {
t.Run("record chris win from user input", func(t *testing.T) {
in := strings.NewReader("Chris wins\n")
playerStore := &go_http_application_with_tdd.StubPlayerScore{}
cli := &go_http_application_with_tdd.CLI{playerStore, in}
cli.PlayPoker()
go_http_application_with_tdd.AssertPlayerWin(t, playerStore, "Chris")
})
t.Run("record cleo win from user input", func(t *testing.T) {
in := strings.NewReader("Cleo wins\n")
playerStore := &go_http_application_with_tdd.StubPlayerScore{}
cli := &go_http_application_with_tdd.CLI{playerStore, in}
cli.PlayPoker()
go_http_application_with_tdd.AssertPlayerWin(t, playerStore, "Cleo")
})
}
|
修改测试代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
type CLI struct {
playerStore PlayerStore
in *bufio.Scanner
}
func NewCLI(store PlayerStore, in io.Reader) *CLI {
return &CLI{
playerStore: store,
in: bufio.NewScanner(in),
}
}
func (c *CLI) PlayPoker() {
userInput := c.readline()
c.playerStore.RecordWin(extractWinner(userInput))
}
func extractWinner(userInput string) string {
return strings.Replace(userInput, " wins", "", 1)
}
func (c *CLI) readline() string {
c.in.Scan()
return c.in.Text()
}
|
通过本篇文章,我们了解了 New 方法的作用,这个方法,可以是我们的属性不用暴露出去,保持封装。
现在应该都 ok 了,不过两个 main 里面。是有重复的,那么就把这个重复的独立出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func FileSystemPlayerStoreFromFile(path string) (*FileSystemPlayerStore, func(), error) {
db, err := os.OpenFile(path, os.O_RDWR | os.O_CREATE, 0666)
if err != nil {
return nil, nil, fmt.Errorf("Problem opening %s %v", path, err)
}
closeFunc := func() {
db.Close()
}
store, err := NewFileSystemPlayerStore(db)
if err != nil {
return nil, nil, fmt.Errorf("problem creating file system player store, %v", err)
}
return store, closeFunc, nil
}
|
然后修改两个 main 的调用就 ok 了。
总结
今天了解了 cli,以及如何更好的测试,还有关于包结构的问题。这篇文章让我有了很多的思考。但是还没法写出来。先把要总结的问题或者思考列出来吧,以后再总结
- 合理的总结包结构
- 测试的时候创建 testing 文件存放所有的测试方法,不污染其他方法。
- 测试包的包名 要 包名_test 这样可以测试公开的属性,不会让未暴露的属性被使用,模拟真正的调用
- New 方法就是方式 struct 的属性被暴露出来,保持合适的封装性。