依赖于你的测试

现在我们已经很好地理解了如何重构代码,并学会了如何利用 Go 的一些最佳特性:编译器和接口。这将使得你的代码重构更加容易,并帮助你将其融入到冲刺计划中。在本节中,我们将通过几个 BookSwap 应用中的代码重构示例,来展示如何使用本章中探讨的所有技术。

自动化重构

Go 的最大优势之一就是其工具支持,IDE 的支持也不例外:

所有这些 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 来处理一些更繁琐的部分。

验证重构代码

虽然重命名符号很简单,但你更常见的变更通常是修改方法签名。让我们来看一下 BookRepositoryGet 方法签名的重构,该方法当前的签名如下:

// 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]

请记住,测试代码是你包的第一个外部消费者,因此对实现代码的任何更改都会首先影响你的测试。注意,这个编译错误的状态代码并没有被提交到我们的版本库中,因此你的测试输出可能和上面的示例不同。