依赖于你的测试
现在我们已经很好地理解了如何重构代码,并学会了如何利用 Go 的一些最佳特性:编译器和接口。这将使得你的代码重构更加容易,并帮助你将其融入到冲刺计划中。在本节中,我们将通过几个 BookSwap
应用中的代码重构示例,来展示如何使用本章中探讨的所有技术。
自动化重构
Go 的最大优势之一就是其工具支持,IDE 的支持也不例外:
-
Google Go 团队为 Visual Studio Code 提供了一个 Go 开发扩展( https://code.visualstudio.com/docs/languages/go )
-
vim-go 插件( https://github.com/fatih/vim-go )是一个由 Go 社区维护的流行开源插件
-
JetBrains 团队创建了 GoLand( https://www.jetbrains.com/go/ ),这是一个专门用于 Go 开发的产品
所有这些 IDE 都支持我们查找给定类型的引用和使用情况,并在整个调用栈中重命名符号。这可以减少大量简单重构的繁琐工作,但你仍然需要自己做一些更改。
让我们考虑一个重构示例,将 BookService
重命名为 BookRepository
。我们可能想要更改这个名称,因为我们在第六章《BookSwap Web 应用的端到端测试》中添加了与数据库相关的功能。
首先,我们将使用 IDE 的重命名符号功能来重命名结构体:
// BookRepository 包含所有管理书籍的功能和依赖
type BookRepository struct {
DB *gorm.DB
ps PostingService
}
这将更新所有直接引用旧 BookService
的实现代码和测试代码,避免我们手动修复许多编译错误。
接下来,我们需要确保与这个结构体相关的所有方法都被正确命名。NewBookService
初始化函数也需要重命名:
// NewBookRepository 初始化一个 BookRepository,给定其依赖项
func NewBookRepository(db *gorm.DB, ps PostingService) *BookRepository {
return &BookRepository{
DB: db,
ps: ps,
}
}
重命名后的函数清晰地表明它负责创建 BookRepository
,并给出其依赖项。
我们还需要检查任何与旧名称相关的测试代码签名。由于我们希望根据测试验证的功能而不是验证的类型来命名测试,所以不需要更改任何测试名称。
最后,包含并测试这些定义的文件名也需要更改,以保持一致:
-
book_service.go
文件将变为book_repository.go
,使文件的命名与其中的代码一致 -
book_service_test.go
文件将变为book_repository_test.go
,确保测试代码和实现代码保持在一起
这就是我们在 BookSwap
应用中重命名服务所需做的所有工作。这个简单的代码重构没有要求更改任何测试,但它展示了你在 Go 代码重构过程中需要遵循的步骤,以及如何依赖你的 IDE 来处理一些更繁琐的部分。
验证重构代码
虽然重命名符号很简单,但你更常见的变更通常是修改方法签名。让我们来看一下 BookRepository
中 Get
方法签名的重构,该方法当前的签名如下:
// Get 返回给定的书籍,若没有则返回错误。
func (bs *BookRepository) Get(id string) (*Book, error)
这个方法接收一个 ID,从数据库中获取书籍,如果书籍没有找到则返回错误。这是这种功能的常见签名。
我们将把这个方法更改为接收 *Book
类型,并且只返回一个错误。这意味着从数据库中获取的书籍将会填充到 book
参数中,并在没有找到时返回一个错误。新的方法签名如下:
// Get 填充给定的书籍,若没有则返回错误。
func (bs *BookRepository) Get(b *Book) error
修改了方法签名后,接下来我们需要相应地修改我们的测试代码。book_repository_test.go
文件中 TestGetBook
的断言代码将进行修改,以适应新的方法签名:
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
var b db.Book
b.ID = tc.id
err := bs.Get(&b)
if tc.wantErr != nil {
assert.Equal(t, tc.wantErr, err)
assert.Nil(t, b)
return
}
assert.Nil(t, err)
assert.Equal(t, tc.want, b)
})
}
我们更改了测试代码,以使其能够编译,并且适应了方法的新签名。在重构过程中,应该尽量减少对测试的修改,以确保重构后的代码没有引入回归错误。
此时,测试代码会编译通过,但我们还没有完全实现新签名的方法代码。现在是时候来实现这个新的方法了:
func (bs *BookRepository) Get(b *Book) error {
if r := bs.DB.Where("id = ?", b.ID).First(&b); r.Error != nil {
return r.Error
}
return nil
}
在这个实现中,我们调整代码以读取 b *Book
参数的 ID,数据库查询结果会填充到传入的参数中。然后,根据是否找到了书籍,我们返回错误或 nil
。
任何其他调用代码也需要像我们调整测试代码一样进行相应的修改。如果遗漏了某些需要重构的代码,编译器会告诉你。例如,如果我们在 Get
方法中添加了第二个参数,但忘记在测试中做相应修改,编译器会告诉我们在测试运行时找不到期望的方法签名:
$ go test -run TestGetBook ./chapter07/db
./book_repository_test.go:34:19: not enough arguments in call to bs.Get
have (*db.Book)
want (*db.Book, string)
FAIL github.com/PacktPublishing/Test-Driven-Development-inGo/chapter07/db [build failed]
请记住,测试代码是你包的第一个外部消费者,因此对实现代码的任何更改都会首先影响你的测试。注意,这个编译错误的状态代码并没有被提交到我们的版本库中,因此你的测试输出可能和上面的示例不同。