examples for the mongo-go-driver mock

在使用 mongo-go-driver 时,需要对mongo数据库操作模拟单元测试用例,

使用 testifygenmock 中的 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"

参考