本文是对Go单测从零到溜系列—2.mock数据库测试 的实践
Mock Mysql
DATA-DOG/go-sqlmock 是一个实现了 sql/driver (我给这个package修过一次typo) 的mock库。
DATA-DOG/go-sqlmock 不需要建立真正的数据库连接就可以在测试中模拟任何 sql 驱动程序的行为,可以很方便的在编写单元测试时mock sql语句的执行结果
以官方README为例:
需要预先在本地建一个名为shuang的库,新建两张表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 CREATE TABLE `products` ( `id` int NOT NULL AUTO_INCREMENT, `views` int DEFAULT NULL , `ctime` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 6 DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_bin; - CREATE TABLE `product_viewers` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int DEFAULT NULL , `product_id` int DEFAULT NULL , `ctime` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 4 DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_bin;
并在products表里插入一条id=5的记录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package mainimport ( "database/sql" _ "github.com/go-sql-driver/mysql" ) func recordStats (db *sql.DB, userID, productID int64 ) (err error ) { tx, err := db.Begin() if err != nil { return } defer func () { switch err { case nil : err = tx.Commit() default : tx.Rollback() } }() if _, err = tx.Exec("UPDATE products SET views = views + 1" ); err != nil { return } if _, err = tx.Exec("INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)" , userID, productID); err != nil { return } return } func main () { db, err := sql.Open("mysql" , "root:12345678@tcp(127.0.0.1:3306)/shuang" ) if err != nil { panic (err) } defer db.Close() if err = recordStats(db, 1 , 5 ); err != nil { panic (err) } }
运行后可以发现products表的views字段每次会加1, product_viewers表也会新增一条记录
如何在不真正连接数据库进行操作的前提下,测试recordStats这个func的代码?
单测代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package mainimport ( "fmt" "testing" "github.com/DATA-DOG/go-sqlmock" ) func TestShouldUpdateStats (t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("an error '%s' was not expected when opening a stub database connection" , err) } defer db.Close() mock.ExpectBegin() mock.ExpectExec("UPDATE products" ).WillReturnResult(sqlmock.NewResult(1 , 1 )) mock.ExpectExec("INSERT INTO product_viewers" ).WithArgs(2 , 3 ).WillReturnResult(sqlmock.NewResult(1 , 1 )) mock.ExpectCommit() if err = recordStats(db, 2 , 3 ); err != nil { t.Errorf("error was not expected while updating stats: %s" , err) } if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s" , err) } } func TestShouldRollbackStatUpdatesOnFailure (t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("an error '%s' was not expected when opening a stub database connection" , err) } defer db.Close() mock.ExpectBegin() mock.ExpectExec("UPDATE products" ).WillReturnResult(sqlmock.NewResult(1 , 1 )) mock.ExpectExec("INSERT INTO product_viewers" ). WithArgs(2 , 3 ). WillReturnError(fmt.Errorf("some error" )) mock.ExpectRollback() if err = recordStats(db, 2 , 3 ); err == nil { t.Errorf("was expecting an error, but there was none" ) } if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s" , err) } }
单测中定义了一个执行成功的测试用例和一个执行失败回滚的测试用例,确保代码中的每个逻辑分支都能被测试到,提高单元测试覆盖率。
展示测试覆盖率,并生成覆盖统计文件到 count.out:
go test ./... -v -coverprofile=count.out
用go tool来分析 count.out 文件并生成想要的结果:
用 -func 生成每个函数的覆盖率
go tool cover -func=count.out
展示每一个函数单元测试的覆盖率,若100% 则测试完整,若0.0% 则没有测试
用 -html 生成 html 文件,以图形方式展示每个函数,每一行代码的覆盖率
go tool cover -html=count.out
会打开默认浏览器,图形化展示测试覆盖率
可切换当前库下的每个文件,看每一行代码是否测试执行。没有执行的显示为红色, 灰色是不需要测试的, 亮绿色是测试通过的
Mock Redis
更多参考:
golang三大基础mock大法
Golang 单元测试:有哪些误区和实践?
原文链接: https://dashen.tech/2010/03/26/Go单测之mock数据库/
版权声明: 转载请注明出处.