Preface
Recently, in practice, some of the ways to test external interface calls using gock Mock in a table-driven way. And how to mock test GORM, after learning the basics of this article, I will write a separate article for you to introduce.
This is the third chapter of the Go language unit test series, which introduces how to use itgo-sqlmock
andminiredis
ToolsMySQL
andRedis
ofmock
test.
In the previous article"Go Unit Test-Simulate Service Request and Interface Return"In this article, we introduce how to use httptest and gock tools for network testing.
In addition to network dependencies, we often use various databases in development, such as the common MySQL and Redis. This article will give examples to demonstrate how to mock MySQL and Redis when writing unit tests.
go-sqlmock
sqlmock is an implementationsql/driver
mock library. It does not require a real database connection to simulate the behavior of any sql driver in tests. Using it can be convenient to execute the result of the mock sql statement when writing unit tests.
Install
go get /DATA-DOG/go-sqlmock
Example of usage
The one used here isgo-sqlmock
Basic sample code provided in the official documentation. In the following code, we implement arecordStats
Functions are used to record relevant data generated by users when browsing products. The specific implementation function is to perform the following two SQL operations in a transaction:
- +1 view times of the current product in the table
- exist
product_viewers
The user ID of the current product is recorded in the table
// package main import "database/sql" // recordStats Record user browsing product informationfunc recordStats(db *, userID, productID int64) (err error) { // Start the transaction // Operate two tables of views and product_viewers tx, err := () if err != nil { return } defer func() { switch err { case nil: err = () default: () } }() // Update the products table if _, err = ("UPDATE products SET views = views + 1"); err != nil { return } // Insert a data into the product_viewers table if _, err = ( "INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)", userID, productID); err != nil { return } return } func main() { // Note: There is no real connection required during the test process db, err := ("mysql", "root@/blog") if err != nil { panic(err) } defer () // Users with userID 1 have browsed products with productID 5 if err = recordStats(db, 1 /*some user id*/, 5 /*some product id*/); err != nil { panic(err) } }
Now we need torecordStats
Functions write unit tests, but do not want to connect to real databases for testing during the test. At this time, we can use it like in the following example codesqlmock
Tools to mock database operations.
package main import ( "fmt" "testing" "/DATA-DOG/go-sqlmock" ) // TestShouldUpdateStats Test case for successful execution of sqlfunc TestShouldUpdateStats(t *) { // mock a * object, no need to connect to the real database db, mock, err := () if err != nil { ("an error '%s' was not expected when opening a stub database connection", err) } defer () // The return result when mock executes the specified SQL statement () ("UPDATE products").WillReturnResult((1, 1)) ("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult((1, 1)) () // Pass the mock DB object into our function if err = recordStats(db, 2, 3); err != nil { ("error was not expected while updating stats: %s", err) } // Ensure that all expected results are met if err := (); err != nil { ("there were unfulfilled expectations: %s", err) } } // TestShouldRollbackStatUpdatesOnFailure Test case for rollback failed to sql executionfunc TestShouldRollbackStatUpdatesOnFailure(t *) { db, mock, err := () if err != nil { ("an error '%s' was not expected when opening a stub database connection", err) } defer () () ("UPDATE products").WillReturnResult((1, 1)) ("INSERT INTO product_viewers"). WithArgs(2, 3). WillReturnError(("some error")) () // now we execute our method if err = recordStats(db, 2, 3); err == nil { ("was expecting an error, but there was none") } // we make sure that all expectations were met if err := (); err != nil { ("there were unfulfilled expectations: %s", err) } }
In the above code, a test case with successful execution and a test case with failed execution rollback are defined, ensuring that every logical branch in our code can be tested, improving unit test coverage while also ensuring the robustness of the code.
Perform unit tests and see the final test results.
❯ go test -v
=== RUN TestShouldUpdateStats
--- PASS: TestShouldUpdateStats (0.00s)
=== RUN TestShouldRollbackStatUpdatesOnFailure
--- PASS: TestShouldRollbackStatUpdatesOnFailure (0.00s)
PASS
ok golang-unit-test-demo/sqlmock_demo 0.011s
You can see that the results of both test cases are in line with expectations and the unit test passes.
In many scenarios where ORM tools are used, you can also use itgo-sqlmock
The library mock database operation is tested.
miniredis
In addition to using MySQL frequently, Redis is also used frequently in daily development. In the next section, we will learn how to mock Redis in unit tests together.
miniredis is a pure Go implementation of redis server for unit testing. It is a simple and easy to use, memory-based redis alternative, with a real TCP interface that you can think of as a redis versionnet/http/httptest
。
We can use it to mock the Redis operation when we write unit tests for some code that contains Redis operations.
Install
go get /alicebob/miniredis/v2
Example of usage
Here/go-redis/redis
As an example, a library has written a Redis operation containing several Redis operations.DoSomethingWithRedis
function.
// redis_op.go package miniredis_demo import ( "context" "/go-redis/redis/v8" // Pay attention to import the version "strings" "time" ) const ( KeyValidWebsite = "app:valid:website:list" ) func DoSomethingWithRedis(rdb *, key string) bool { // This can be some logic for redis operations ctx := () if !(ctx, KeyValidWebsite, key).Val() { return false } val, err := (ctx, key).Result() if err != nil { return false } if !(val, "https://") { val = "https://" + val } //Settings blog key five seconds expired if err := (ctx, "blog", val, 5*).Err(); err != nil { return false } return true }
The following code is what I useminiredis
The library isDoSomethingWithRedis
Unit test code written by function, whereminiredis
Not only supports the commonly used Redis operations of mock, it also provides many practical help functions, such as checking whether the value of the key is equal to the expected one.()
and help check key expiration time()
。
// redis_op_test.go package miniredis_demo import ( "/alicebob/miniredis/v2" "/go-redis/redis/v8" "testing" "time" ) func TestDoSomethingWithRedis(t *) { // mock a redis server s, err := () if err != nil { panic(err) } defer () // Prepare data ("q1mi", "") (KeyValidWebsite, "q1mi") // Redis server connecting to mock rdb := (&{ Addr: (), // mock redis server's address }) // Call the function ok := DoSomethingWithRedis(rdb, "q1mi") if !ok { () } // You can manually check whether the values in redis are compounded with expectations if got, err := ("blog"); err != nil || got != "" { ("'blog' has the wrong value") } // You can also use the help tool to check (t, "blog", "") // Expired inspection (5 * ) // Fast forward 5 seconds if ("blog") { ("'blog' should not have existed anymore") } }
Execute the test and view the unit test results:
❯ go test -v
=== RUN ;TestDoSomethingWithRedis
--- PASS: TestDoSomethingWithRedis (0.00s)
PASS
ok golang-unit-test-demo/miniredis_demo 0.052s
miniredis
Basically, most Redis commands are supported, and you can learn more about the usage by checking the documentation.
Of course, except for usingminiredis
In addition to building a local redis server, you can also use various pile driving tools to pile specific methods. Which mock method is used when writing unit tests still depends on the actual situation.
Summarize
How to handle database dependencies when writing unit tests for code in daily work development is the most common problem, this article describes how to use itgo-sqlmock
andminiredis
Tool mock-related dependencies.
Next, we will go further and introduce in detail how to implement the mock interface when writing unit tests. For more information about Go database CRUD Mock testing, please pay attention to my other related articles!