用例 – BookSwap 应用
Go 最常见的应用场景之一是构建 Web 应用程序。因此,了解如何构建和测试 Web 应用程序非常重要。我们将在本章和接下来的章节中探索并测试 BookSwap 应用程序。
这个简单的应用程序允许用户注册、列出自己可交换的书籍。其他用户可以注册该应用程序并查看其他用户的可用书籍。然后,他们可以请求借阅其他用户的书籍。BookSwap 应用程序随后生成订单并将其发送到包装服务,以便进行包装和运输。
图 4.8 展示了 BookSwap 应用程序的概述:

BookSwap
Web 应用程序有一些简单的组件:
-
用户与 UserService 服务端点进行交互。它暴露了几个简单的端点,提供应用程序所需的所有功能:
-
GET / 返回欢迎消息并展示应用程序中的所有书籍列表。这个端点将作为应用程序的主页,显示所有可交换的书籍。为了完整性,GET /books 端点还将返回可用书籍的列表。
-
POST /users 创建一个新用户。用户将收到一个唯一的 userID 值,之后需要记住这个值以便进行后续的交互。为了简化起见,我们将不处理用户认证或任何安全问题。
-
GET /users/{id}
返回给定用户的书籍列表。这个端点将作为某个用户的个人主页。
-
-
UserService 服务依赖于 BookService。它管理 BookSwap Web 应用程序中所有可用书籍的详细信息和状态。它暴露以下端点:
-
POST /books 在 BookSwap 服务中创建一个新的书籍条目。此请求将接受包含书籍详情的 JSON 请求体。
-
POST /books/{id}?user={userId}
为特定书籍和指定用户创建一个新的请求。这个请求将创建一个新的请求,将指定的书籍发送给新用户。
-
-
BookService 依赖于外部的 PostingService,它处理邮票的创建和包装请求。一旦 PostingService 处理完订单请求,我们就可以将书籍标记为已交换,并更新其 ownerID 值。
你可以在我们的 GitHub 仓库中查看 BookSwap 应用程序的完整实现。该应用程序是使用标准库中的 net/http
包实现的。我们将在本章中探索 BookSwap Web 应用程序的相关部分,这些部分展示了我们在本章中学到的内容。
测试 BookService
我们将把书籍表示为一种非常简单的数据类型,使用 JSON 标签来格式化其内容,以便在 REST API 中显示,这些 API 提供 JSON 数据:
type Book struct {
ID string `json:"id"`
Name string `json:"name"`
Author string `json:"author"`
OwnerID string `json:"owner_id"`
Status string `json:"status"`
}
虽然 REST API 不一定需要操作 JSON 数据,但 application/json
数据格式是最常用的。书籍具有 OwnerID
和 Status
字段,后者显示书籍是否可用于交换。
BookService
是一个非常简单的服务,用于管理书籍。它需要能够通过六个简单的方法来检索和管理书籍:
// NewBookService 初始化一个 BookService,接受初始书籍列表和 PostingService 服务作为参数
func NewBookService(initial []Book, ps PostingService) *BookService
// Get 根据给定的 ID 返回一本书,如果没有找到则返回错误
func (bs *BookService) Get(id string) (*Book, error)
// Upsert 创建或更新一本书
func (bs *BookService) Upsert(b Book) Book
// List 返回所有可用书籍的列表
func (bs *BookService) List() []Book
// ListByUser 根据用户 ID 返回该用户的书籍列表
func (bs *BookService) ListByUser(userID string) []Book
// SwapBook 检查书籍是否可用,并在可能的情况下将其标记为已交换
func (bs *BookService) SwapBook(bookID, userID string) (*Book, error)
NewBookService
方法使用给定的书籍列表和 PostingService
服务初始化 BookService
。Get
方法尝试根据给定的 ID 查找一本书,如果没有找到,则返回错误。Upsert
方法创建一个新的书籍条目,或者如果给定的 ID 已经存在,则更新该条目。List
操作返回所有可供借阅的书籍。ListByUser
根据给定的用户过滤所有书籍,使我们能够为某个用户生成主页。SwapBook
是一个函数,它处理可用性检查,并在交换请求的情况下更新书籍的拥有者 ID。
BookService
会将书籍条目保存在一个以书籍 ID 为键的 map 中:
type BookService struct {
books map[string]Book
ps PostingService
}
这个 map
将方便进行查找操作,这是 BookSwap 应用程序所需要的。Get
和 List
操作预计将是最常用的,因为它们会出现在主页和个人主页上。
让我们看看如何为 BookService
的 Get
操作编写表驱动测试。我们声明一个包含两个子测试的测试:一个用于初始数量的书籍,另一个用于空的书籍 map
:
func TestGetBook(t *testing.T) {
t.Run("initial books", func(t *testing.T) {
// 在 BookService 中已有书籍
})
t.Run("empty books", func(t *testing.T) {
// BookService 中没有书籍
})
}
我们使用两个不同的子测试来处理这两种情况,因为它们需要不同的测试设置。如我们所讨论的,表驱动测试不适合需要不同设置条件的场景。我们首先创建一个示例书籍并创建一个新的 BookService
实例:
eb := db.Book{
ID: uuid.New().String(),
Name: "Existing book",
Status: db.Available.String(),
}
这个起始点将在所有测试用例中共享。注意,我们将 PostingService
服务传递为 nil
,因为这些测试不涉及它。接下来,我们在第一个子测试中实现一个表驱动测试,包含三个场景:
tests := map[string]struct {
id string
want db.Book
wantErr error
}{
"existing book": {id: eb.ID, want: eb},
"no book found": {id: "not-found", wantErr: errors.New("no book found")},
"empty id": {id: "", wantErr: errors.New("no book found")},
}
这三个测试用例分别是:查找现有书籍、查找在 BookService
中不存在的书籍、以及查找空 ID。然后,我们遍历测试用例,并根据输入和期望的结果进行断言:
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
b, err := bs.Get(tc.id)
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)
})
}
正如我们在 “表驱动测试实践” 一节中所做的那样,我们遍历测试用例的 map,首先处理错误情况。记得验证所有期望的返回值。
在第二个子测试中,名为 "empty books",我们运行了一个单独的测试,并对不同的 UUT 实例执行了所需的验证:
t.Run("empty books", func(t *testing.T) {
bs := db.NewBookService([]db.Book{})
b, err := bs.Get("id")
assert.Equal(t, errors.New("no book found"), err)
assert.Nil(t, b)
})
我们本来可以为第二个子测试也实现表驱动测试,但为了简洁起见,我们选择在这里只包含一个测试。
最后,我们使用 go test
命令运行测试,确保它们通过:
$ go test -run TestGetBook ./chapter04/db -v
=== RUN TestGetBook
--- PASS: TestGetBook (0.00s)
--- PASS: TestGetBook/initial_books (0.00s)
--- PASS: TestGetBook/initial_books/existing_book (0.00s)
--- PASS: TestGetBook/initial_books/no_book_found (0.00s)
--- PASS: TestGetBook/initial_books/empty_id (0.00s)
--- PASS: TestGetBook/empty_books (0.00s)
PASS
ok github.com/PacktPublishing/Test-Driven-Developmentin-Go/chapter04/db 0.217s
请注意,输出显示了两个不同子测试的嵌套,这使我们能够构建详细的测试层次结构。我们将在接下来的章节中继续探索和测试 BookSwap 应用程序的其他部分,因此有很多时间来进一步探索它。