内容简介:对DB交互代码进行单元测试并不容易,当涉及到诸如从理论上讲,我们可以使用强大的模拟工具)来模拟
概述
对DB交互代码进行单元测试并不容易,当涉及到诸如 GORM 之类的ORM库时,这将变得更加困难。
从理论上讲,我们可以使用强大的模拟工具)来模拟 database/sql/driver
的所有接口(例如Conn和Driver)。但是,即使在GoMock的帮助下,我们仍然需要大量的手工工作来完成这种测试。
好消息是 Sqlmock 可以解决上述问题。正如其官方网站所宣布的那样,它是一个“用于golang的 SQL 模拟驱动程序,用于测试数据库交互。”
本文将向您展示如何使用Sqlmock对一个简单的博客应用程序进行单元测试。该应用程序以PostgreSQL为例,并使用GORM简化了O-R映射。
我们将使用BDD测试框架 Ginkgo 编写测试用例,但是您可以更改为您喜欢的任何其他测试库。
我们的博客应用程序将包含一个博客数据model和一个用于处理数据库操作的 repository
结构。
定义GORM数据Model和Repository
首先定义博客数据模型Model和Repository结构
// modle.go import "github.com/lib/pq" ... type Blog struct { ID uint Title string Content string Tags pq.StringArray // string array for tags CreatedAt time.Time } // repository.go import "github.com/jinzhu/gorm" ... type Repository struct { db *gorm.DB } func (p *Repository) ListAll() ([]*Blog, error) { var l []*Blog err := p.db.Find(&l).Error return l, err } func (p *Repository) Load(id uint) (*Blog, error) { blog := &Blog{} err := p.db.Where(`id = ?`, id).First(blog).Error return blog, err } ...
Tips: 注意 Blog.Tags
的类型是 pq.StringArray
,它表示PostgreSQL中的字符串数组。
我们的 Repository
结构非常简单。它只有 gorm.DB
一个字段,并且所有数据库操作都取决于此字段。为了简洁起见,我省略了一些代码。除了 Load
和 ListAll
之外, Repository
结构中还声明了其他几种方法,例如 Save
, Delete
, SearchByTitle
等。这些方法将在本文后面解释。
设置测试用例
import ( ... . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/DATA-DOG/go-sqlmock" "github.com/jinzhu/gorm" ) var _ = Describe("Repository", func() { var repository *Repository var mock sqlmock.Sqlmock BeforeEach(func() { var db *sql.DB var err error db, mock, err = sqlmock.New() // mock sql.DB Expect(err).ShouldNot(HaveOccurred()) gdb, err := gorm.Open("postgres", db) // open gorm db Expect(err).ShouldNot(HaveOccurred()) repository = &Repository{db: gdb} }) AfterEach(func() { err := mock.ExpectationsWereMet() // make sure all expectations were met Expect(err).ShouldNot(HaveOccurred()) }) It("test something", func(){ ... }) })
要将Sqlmock与GORM一起使用,我们需要在 BeforeEach中
进行一些准备,以确保每个测试规范都可以获取一个新的Repository实例,然后在 AfterEach
中断言预期的case。
在 BeforeEach
中,可以通过三个步骤来设置此测试用例:
- 使用
sqlmock.New()
创建*sql.DB
的模拟实例和模拟控制器 - 通过使用
gorm.Open("postgres", db)
来打开一个GORM(使用PostgreSQL) - 创建一个
Repository
实例
在 AfterEach
中,我们调用 mock.ExpectationsWereMet()
以确保满足所有期望。
现在,让我们从最简单的场景开始编写规范。
测试 ListAll 方法
// repository.go ... func (p *Repository) ListAll() ([]*Blog, error) { var l []*Blog err := p.db.Find(&l).Error return l, err } ... // repository_test.go ... Context("list all", func() { It("empty", func() { const sqlSelectAll = `SELECT * FROM "blogs"` mock.ExpectQuery(sqlSelectAll). WillReturnRows(sqlmock.NewRows(nil)) l, err := repository.ListAll() Expect(err).ShouldNot(HaveOccurred()) Expect(l).Should(BeEmpty()) }) }) ...
如上面的代码片段所示, ListAll
在DB中查找所有记录,并将它们映射到 []*Blog
。
测试规范比较直接。我们将预期查询设置为 SELECT * FROM "blogs"
,并返回一个空结果集。
然后运行所有测试:
➜ ginkgo Running Suite: Pg Suite ======================= Random Seed: 1585542357 Will run 8 of 8 specs (/Users/dche423/dbtest/pg/repository.go:24) [2020-03-30 12:26:01] Query: could not match actual sql: "SELECT * FROM "blogs"" with expected regexp "SELECT * FROM "blogs"" • Failure [0.001 seconds] Repository /Users/dche423/dbtest/pg/repository_test.go:16 list all /Users/dche423/dbtest/pg/repository_test.go:37 empty [It] /Users/dche423/dbtest/pg/repository_test.go:38 ... Test Suite Failed ➜
您可能会对这个简单的测试用例失败感到惊讶。但是控制台日志为我们提供了线索:“could not match actual sql with expected regexp.(翻译过来就是:无法将实际的sql与预期的regexp相匹配。)”
事实证明Sqlmock使用 sqlmock.QueryMatcherRegex
作为默认SQL匹配器。在这种情况下,方法 sqlmock.ExpectQuery
将正则表达式字符串作为其参数,而不是纯SQL字符串。
我们有两种选择来解决此问题:
- 使用
regexp.QuoteMeta
方法转义SQL字符串中的所有正则表达式元字符。因此我们可以将ExcectQuery
更改为mock.ExpectQuery(regexp.QuoteMeta(sqlSelectAll))...
。 - 更改默认的SQL匹配器。创建模拟实例时,我们可以提供匹配器选项:
sqlmock.New(**sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)**)
通常,正则表达式匹配器比相等匹配器更灵活(这就是Sqlmock将其用作默认值的原因)。
提示:默认情况下,Sqlmock将SQL与正则表达式匹配。
接下来,让我们测试将单个数据库记录加载到数据模型中的方法。
测试Load方法
// repository.go func (p *Repository) Load(id uint) (*Blog, error) { blog := &Blog{} err := p.db.Where(`id = ?`, id).First(blog).Error return blog, err } ... // repository_test.go Context("load", func() { It("found", func() { blog := &Blog{ ID: 1, Title: "post", ... } rows := sqlmock. NewRows([]string{"id", "title", "content", "tags", "created_at"}). AddRow(blog.ID, blog.Title, blog.Content, blog.Tags, blog.CreatedAt) const sqlSelectOne = `SELECT * FROM "blogs" WHERE (id = $1) ORDER BY "blogs"."id" ASC LIMIT 1` mock.ExpectQuery(regexp.QuoteMeta(sqlSelectOne)).WithArgs(blog.ID).WillReturnRows(rows) dbBlog, err := repository.Load(blog.ID) Expect(err).ShouldNot(HaveOccurred()) Expect(dbBlog).Should(Equal(blog)) }) It("not found", func() { // ignore sql match mock.ExpectQuery(`.+`).WillReturnRows(sqlmock.NewRows(nil)) _, err := repository.Load(1) Expect(err).Should(Equal(gorm.ErrRecordNotFound)) }) }) ...
Load
方法将博客ID作为参数,然后查找具有该ID的第一条记录。
我们将测试此方法的两种情况。
在第一个规范(名为“ found”)中,我们构建了一个博客实例并将其转换为 sql.Row
。然后,我们调用 ExpectQuery
定义期望。在本规范的最后,我们断言所加载的博客实例等于原始实例。
注意:如果不确定GORM将产生什么SQL,可以使用 gorm.DB
的 Debug()
方法打开调试标志。
其他规范涵盖“not found”方案。它还演示了当我们不关心SQL输入(我们使用 .+
作为可以匹配任何内容的输入字符串)时,如何使用正则表达式简化SQL匹配。
在这种情况下,我们关心的是,当 Load
方法找不到博客时,应该返回 gorm.ErrRecordNotFound
错误。
提示:使用正则表达式可以简化SQL匹配。
在下一部分中,我们将进行单元测试以使用GORM插入记录,这是最棘手的部分。
测试 Save 方法
// repository.go ... func (p *Repository) Save(blog *Blog) error { return p.db.Save(blog).Error } // repository_test.go ... Context("save", func() { var blog *Blog BeforeEach(func() { blog = &Blog{ Title: "post", Content: "hello", Tags: pq.StringArray{"a", "b"}, CreatedAt: time.Now(), } }) It("insert", func() { // gorm use query instead of exec // https://github.com/DATA-DOG/go-sqlmock/issues/118 const sqlInsert = ` INSERT INTO "blogs" ("title","content","tags","created_at") VALUES ($1,$2,$3,$4) RETURNING "blogs"."id"` const newId = 1 mock.ExpectBegin() // begin transaction mock.ExpectQuery(regexp.QuoteMeta(sqlInsert)). WithArgs(blog.Title, blog.Content, blog.Tags, blog.CreatedAt). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(newId)) mock.ExpectCommit() // commit transaction Expect(blog.ID).Should(BeZero()) err := repository.Save(blog) Expect(err).ShouldNot(HaveOccurred()) Expect(blog.ID).Should(BeEquivalentTo(newId)) }) It("update", func() { ... }) })
当数据Model具有主键时, Save
方法将更新数据库记录。当没有记录时,该方法会将新记录插入数据库。
上面的代码段显示了后一种情况。
我们创建一个新的博客实例,而不设置其主键。然后,使用 mock.ExpectQuery
定义期望。事务在查询之前启动,并在查询之后提交。
通常,非查询SQL期望值(例如,插入/更新)应由 mock.ExpectExec
定义,但这是一种特殊情况。由于某些原因,GROM使用 QueryRow
而不是 Exec
来表示 postgres
方言(有关更多详细信息,请参阅 此问题 )。
最后,我们使用 Expect(blog.ID).Should(BeEquivalentTo(*newId*))
断言 blog.ID
是在 Save
方法之后设置的。
提示:如果您使用的是PostgreSQL,请对GORM模型插入使用 mock.ExpectQuery
。
您可能建议不必对简单的“插入/更新”操作进行单元测试。实际上,是的,没有必要。我们要向您展示的是,GORM可能会执行一些您之前没有注意到的隐式操作。
结论
Sqlmock是对DB交互式代码进行单元测试的好工具,但是在使用GORM和PostgreSQL时有一些陷阱。
在本文中,我们构建了一个简单的博客应用程序,并使用Sqlmock对它进行了单元测试。我相信您可以在此示例的帮助下开始单元测试。
有关完整的源代码,请访问 这个仓库 。
文章来源: https://1024casts.com/topics/R9re7QDaq8MnJoaXRZxdljbNA5BwoK
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 使用 Gomock 进行单元测试
- 使用JUnit进行单元测试
- 使用Jest进行React单元测试
- Jest & enzyme 进行react单元测试
- 如何实现插入排序以及进行单元格测试
- 如何对 Jenkins 共享库进行单元测试
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。