examples for the mongo-go-driver mock
在使用 mongo-go-driver 时,需要对mongo数据库操作模拟单元测试用例,
使用 testify 或 genmock 中的 mock 都需要实现mock接口。
而 mongo-go-driver 官方有一个 mtest 包,提供更好的实现。
mock单元测试
下面是mock具体操作单元测试示例:
MongoDB 查询操作示例
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 | import (
	"context"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
)
func getFromID(id primitive.ObjectID) (*user, error) {
	filter := bson.D{{Key: "id", Value: id}}
	var object user
	if err := userCollection.FindOne(context.Background(), filter).Decode(&object); err != nil {
		return nil, err
	}
	return &object, nil
}
 | 
 
为查询操作实现单元测试示例:
|  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
 | import (
	"testing"
	"github.com/stretchr/testify/assert"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo/integration/mtest"
)
func TestFindOne(t *testing.T) {
	mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
	defer mt.Close()
	mt.Run("success", func(mt *mtest.T) {
		userCollection = mt.Coll
		expectedUser := user{
			ID:    primitive.NewObjectID(),
			Name:  "john",
			Email: "john.doe@test.com",
		}
		mt.AddMockResponses(mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch, bson.D{
			{"_id", expectedUser.ID},
			{"name", expectedUser.Name},
			{"email", expectedUser.Email},
		}))
		userResponse, err := getFromID(expectedUser.ID)
		assert.Nil(t, err)
		assert.Equal(t, &expectedUser, userResponse)
	})
}
 | 
 
如下创建一个测试实例,测试类型指定为 mtest.Mock
| 1
 | mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
 | 
 
指定 mtest.Mock 类型时,不会创建真实数据库连接,对于查询结果需要通过CreateCursorResponse来模拟。
模拟多行响应结果集:
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 |         id1 := primitive.NewObjectID()
		id2 := primitive.NewObjectID()
		first := mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch, bson.D{
			{"_id", id1},
			{"name", "john"},
			{"email", "john.doe@test.com"},
		})
		second := mtest.CreateCursorResponse(1, "foo.bar", mtest.NextBatch, bson.D{
			{"_id", id2},
			{"name", "john"},
			{"email", "foo.bar@test.com"},
		})
		killCursors := mtest.CreateCursorResponse(0, "foo.bar", mtest.NextBatch)
		mt.AddMockResponses(first, second, killCursors)
 | 
 
更多操作 mongo-go-driver-mock
非mock单元测试
mtest.Mock类型虽然可以模拟结果集,确不能模拟执行过程,响应结果都是期望中的数据,不能体现过滤条件等操作。
这个时候就需要其他类型来实现:
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 | const (
	// Default specifies a client to the connection string in the MONGODB_URI env variable with command monitoring
	// enabled.
	Default ClientType = iota
	// Pinned specifies a client that is pinned to a single mongos in a sharded cluster.
	Pinned
	// Mock specifies a client that communicates with a mock deployment.
	Mock
	// Proxy specifies a client that proxies messages to the server and also stores parsed copies. The proxied
	// messages can be retrieved via T.GetProxiedMessages or T.GetRawProxiedMessages.
	Proxy
)
 | 
 
mtest提供4种类型选择,除Mock外的三种类型都是会创建真实连接,
这种时候在测试前需要编好测试数据,在执行测试方法前写入数据库。
通过name查询方法find
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 | func find(name string) ([]user, error) {
	filter := bson.D{{Key: "name", Value: name}}
	var users []user
	cursor, err := userCollection.Find(context.Background(), filter)
	if err != nil {
		return nil, err
	}
	if err = cursor.All(context.Background(), &users); err != nil {
		return nil, err
	}
	return users, nil
}
 | 
 
方法find的单元测试示例:
|  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
 | import (
	"testing"
	"github.com/stretchr/testify/assert"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo/integration/mtest"
    . "github.com/smartystreets/goconvey/convey"
    _ "github.com/joho/godotenv/autoload"
)
func TestFindOne(t *testing.T) {
    Convey("Find", t, func() {
        err := mtest.Setup(mtest.NewSetupOptions().SetURI(os.Getenv("MONGODB_URI")))
        assert.Nil(t, err)
	    mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Default).
           DatabaseName("test").CollectionName("test").AtlasDataLake(true))
	    defer mt.Close()
        mt.RunOpts("Find", mtest.NewOptions().ClientType(mtest.Default).
           DatabaseName("test").CollectionName("test").AtlasDataLake(true), func(mt *mtest.T) {
		    user1 := user{
		    	// ID:    primitive.NewObjectID(),
		    	Name:  "john",
		    	Email: "john.doe@test.com",
		    }
		    user2 := user{
		    	// ID:    primitive.NewObjectID(),
		    	Name:  "johnx",
		    	Email: "johnx.doe@test.com",
		    }
            ctx := mtest.Background
            res, err := mt.Coll.InsertOne(ctx, &user1)
            assert.Nil(t, err)
            id1, ok := res.InsertedID.(primitive.ObjectID)
            assert.Equal(t, ok, true)
            res, err = mt.Coll.InsertOne(ctx, &user2)
            assert.Nil(t, err)
            // id2, ok := res.InsertedID.(primitive.ObjectID)
            // assert.Equal(t, ok, true)
		    userList, err := find(user1.Name)
		    assert.Nil(t, err)
		    user1.ID = id1
		    assert.Equal(t, 1, len(userList))
		    assert.Equal(t, []user{user1}, userList)
        })
    })
}
 | 
 
通过当前测试目录.env文件中配置环境变量MONGODB_URI连接数据库。
| 1
2
 | MONGODB_URI=mongodb://mongo.example.com:27017
ATLAS_DATA_LAKE_INTEGRATION_TEST=true
 | 
 
然后通过mtest.Options指定数据库和集合,然后模拟写入2条name不同的数据,
调用find方法时,结果集中只会有1条数据返回。
值得注意的时,在非mtest.Mock下,结束时都会有清理数据库操作,
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 | func (t *T) RunOpts(name string, opts *Options, callback func(mt *T)) {
	t.T.Run(name, func(wrapped *testing.T) {
		sub := newT(wrapped, t.baseOpts, opts)
        ...
		// defer dropping all collections if the test is using a client
		defer func() {
            ...
			if sub.clientType != Mock {
				sub.ClearFailPoints()
				sub.ClearCollections()
			}
 | 
 
可以看到非 Mock 类型,会调用 ClearCollections, defer mt.Close()操作也一样。
所以mtest.Options中也可以不指定DatabaseName("test").CollectionName("test"),不指定时就会使用默认的。
| 1
2
3
4
 | const (
	// TestDb specifies the name of default test database.
	TestDb = "test"
)
 | 
 
如果不想清理数据库中的数据怎么办呢,在设置Options时,设置 AtlasDataLake(true) 即可。
ClearCollections
| 1
2
3
4
 | // ClearCollections drops all collections previously created by this test.
func (t *T) ClearCollections() {
	// Collections should not be dropped when testing against Atlas Data Lake because the data is pre-inserted.
	if !testContext.dataLake {
 | 
 
这样集合中的数据在测试结束后,就不会被清理。
也可以通过环境变量 ATLAS_DATA_LAKE_INTEGRATION_TEST 设置
| 1
2
3
4
5
6
7
 | // Setup initializes the current testing context.
// This function must only be called one time and must be called before any tests run.
func Setup(setupOpts ...*SetupOptions) error {
    ...
    testContext.dataLake = os.Getenv("ATLAS_DATA_LAKE_INTEGRATION_TEST") == "true"
 | 
 
参考