如何使用 Sqlmock 对 GORM 应用进行单元测试 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
qloog

如何使用 Sqlmock 对 GORM 应用进行单元测试

  •  
  •   qloog 2020 年 4 月 4 日 3443 次点击
    这是一个创建于 2211 天前的主题,其中的信息可能已经有所发展或是发生改变。

    tools.jpeg

    概述

    对 DB 交互代码进行单元测试并不容易,当涉及到诸如GORM之类的 ORM 库时,这将变得更加困难。

    从理论上讲,我们可以使用强大的模拟工具[GoMock](GoMock)来模拟 database/sql/driver 的所有接口(例如 Conn 和 Driver)。但是,即使在 GoMock 的帮助下,我们仍然需要大量的手工工作来完成这种测试。

    好消息是Sqlmock可以解决上述问题。正如其官方网站所宣布的那样,它是一个“用于 golang 的 SQL 模拟驱动程序,用于测试数据库交互。”

    本文将向您展示如何使用 Sqlmock 对一个简单的博客应用程序进行单元测试。该应用程序以 PostgreSQL 为例,并使用 GORM 简化了 O-R 映射。

    我们将使用 BDD 测试框架Ginkgo编写测试用例,但是您可以更改为您喜欢的任何其他测试库。

    我们的博客应用程序将包含一个博客数据 model 和一个用于处理数据库操作的repository 结构。

    blog-repo.png

    定义 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 一个字段,并且所有数据库操作都取决于此字段。为了简洁起见,我省略了一些代码。除了LoadListAll 之外,Repository 结构中还声明了其他几种方法,例如 SaveDeleteSearchByTitle 等。这些方法将在本文后面解释。

    设置测试用例

    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 中,可以通过三个步骤来设置此测试用例:

    1. 使用 sqlmock.New() 创建 *sql.DB 的模拟实例和模拟控制器
    2. 通过使用 gorm.Open("postgres", db) 来打开一个 GORM(使用 PostgreSQL)
    3. 创建一个 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 字符串。

    我们有两种选择来解决此问题:

    1. 使用 regexp.QuoteMeta 方法转义 SQL 字符串中的所有正则表达式元字符。因此我们可以将 ExcectQuery 更改为 mock.ExpectQuery(regexp.QuoteMeta(sqlSelectAll))...
    2. 更改默认的 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).irst(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.DBDebug() 方法打开调试标志。

    其他规范涵盖“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
    文章出处:1024 课堂

    5 条回复    2020-04-04 15:43:47 +08:00
    mcfog
        1
    mcfog  
       2020 年 4 月 4 日   1
    看在没二维码只有个链接的份上回一下,这是经典的没搞明白什么是单元测试,测试有哪些类型和目标而导致的无效瞎折腾
    ahmcsxcc
        2
    ahmcsxcc  
       2020 年 4 月 4 日
    @mcfog #1 能详细说下吗, 我感觉这个写的挺不错的, 你这么一说我迷茫了
    qloog
        3
    qloog &nsp;
    OP
       2020 年 4 月 4 日
    @mcfog 来详细说说呗,针对数据库的单元测试
    mcfog
        4
    mcfog  
       2020 年 4 月 4 日
    @ahmcsxcc 去了解一下静态分析,单元测试,集成测试,系统测试,验收测试的概念和区别,想想各自的目的,然后分析一下主楼写的这些属于哪种,提示:绝对不是单元测试

    @qloog 数据库不是单元测试的有效目标,硬要说的话,你可以去读 MySQL 源码里的测试,或者 GORM 里面的测试,看看是不是你想要的
    qloog
        5
    qloog  
    OP
       2020 年 4 月 4 日
    @mcfog [数据库不是单元测试的有效目标] ,说的也没错。
    GORM 里面的测试,我去看看,另外本文只是对如果想要测试数据库的提供的一种测试方式。感觉你把问题有点放大了。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2624 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 68ms UTC 15:30 PVG 23:30 LAX 08:30 JFK 11:30
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86