模拟一个Go数据库SDK

huangapple go评论94阅读模式
英文:

Mocking a Go database SDK

问题

我正在尝试为Go Flex SDK for Google Cloud Datastore创建一个测试模拟包装器。虽然我目前可以在单独的终端窗口中成功运行本地主机模拟器,使用以下命令:

gcloud beta emulators datastore start --no-store-on-disk

但我希望创建一个模拟数据库模拟器,它作为测试过程的一部分运行(而不是使用上述的exec命令),以便我可以并行运行多个测试,每个测试都有自己的数据库模拟器。

我在Google SDK中遇到了一个问题,它没有实现我的接口。

我的包装器包含以下代码:

package google

import (
	"context"

	"cloud.google.com/go/datastore"
)

type (
	// Datastore is a wrapper for the Google Cloud Datastore Client.
	Datastore datastore.Client

	// Datastorer represents things that can operate like a datastore.Client.
	Datastorer interface {
		Delete(context.Context, *datastore.Key) error
		Get(context.Context, *datastore.Key, interface{}) error
		GetAll(context.Context, *datastore.Query, interface{}) ([]*datastore.Key, error)
		Put(context.Context, *datastore.Key, interface{}) (*datastore.Key, error)
		PutMulti(context.Context, []*datastore.Key, interface{}) ([]*datastore.Key, error)
		RunInTransaction(context.Context, func(Transactioner) error, ...datastore.TransactionOption) (*datastore.Commit, error)
	}

	// Transactioner represents things that can operate like a datastore.Transaction.
	Transactioner interface {
		Commit() (*datastore.Commit, error)
		Delete(*datastore.Key) error
		DeleteMulti([]*datastore.Key) error
		Get(*datastore.Key, interface{}) error
		GetMulti([]*datastore.Key, interface{}) error
		Put(*datastore.Key, interface{}) (*datastore.PendingKey, error)
		PutMulti([]*datastore.Key, interface{}) ([]*datastore.PendingKey, error)
		Rollback() error
	}
)

// Delete deletes the entity for the given key.
func (d *Datastore) Delete(ctx context.Context, key *datastore.Key) error {
	return (*datastore.Client)(d).Delete(ctx, key)
}

// Get retrieves the entity for the given key.
func (d *Datastore) Get(ctx context.Context, key *datastore.Key, dst interface{}) error {
	return (*datastore.Client)(d).Get(ctx, key, dst)
}

// GetAll retrieves all entities for the given query.
func (d *Datastore) GetAll(ctx context.Context, q *datastore.Query, dst interface{}) ([]*datastore.Key, error) {
	return (*datastore.Client)(d).GetAll(ctx, q, dst)
}

// Put stores an entity for the given key.
func (d *Datastore) Put(ctx context.Context, key *datastore.Key, src interface{}) (*datastore.Key, error) {
	return (*datastore.Client)(d).Put(ctx, key, src)
}

// PutMulti is a batch version of Put.
func (d *Datastore) PutMulti(ctx context.Context, keys []*datastore.Key, src interface{}) ([]*datastore.Key, error) {
	return (*datastore.Client)(d).PutMulti(ctx, keys, src)
}

// RunInTransaction runs the given function in a transaction.
func (d *Datastore) RunInTransaction(ctx context.Context, f func(tx Transactioner) error, opts ...datastore.TransactionOption) (*datastore.Commit, error) {
	return (*datastore.Client)(d).RunInTransaction(ctx, func(t *datastore.Transaction) error {
		return f(t)
	}, opts...)
}

请注意,这些接口并没有模拟完整的SDK。我只包含了我在代码中实际调用的函数。以后如果需要,我会添加新的函数。

当我尝试将*datastore.Client的实例用作Datastorer时,我会收到以下错误:

cannot use client (type *"cloud.google.com/go/datastore".Client) as type Datastorer in field value:
*"cloud.google.com/go/datastore".Client does not implement Datastorer (wrong type for RunInTransaction method)
have RunInTransaction(context.Context, func(*"cloud.google.com/go/datastore".Transaction) error, ..."cloud.google.com/go/datastore".TransactionOption) (*"cloud.google.com/go/datastore".Commit, error)
want RunInTransaction(context.Context, func(Transactioner) error, ..."cloud.google.com/go/datastore".TransactionOption) (*"cloud.google.com/go/datastore".Commit, error)

因为*datastore.Client需要一个接受func(*datastore.Transaction) error的函数,而我的接口需要一个接受func(Transactioner) error的函数。

有没有办法更改它以使其编译通过?

如果我能让它工作,我计划创建实现我的DatastorerTransactioner接口的类型,并使用映射来模拟真实的数据库。至于事务,对于测试,如果需要,我可以使用sync.Mutex,但由于每个测试都是单线程的,并且将获得自己的数据库对象,我可能不需要锁定它们。

英文:

I am attempting to create a wrapper for test emulating around the Go Flex SDK for Google Cloud Datastore. While I am currently successfully running the localhost emulator using

gcloud beta emulators datastore start --no-store-on-disk

in a separate terminal from my testing window, I would prefer to create a mock database emulator that runs as part of the test process itself (without execing the above) so that I can run multiple tests in parallel, each with its own database emulator.

I have run into a problem with the Google SDK not implementing my interface.

My wrapper contains this code:

package google
import (
"context"
"cloud.google.com/go/datastore"
)
type (
// Datastore is a wrapper for the Google Cloud Datastore Client.
Datastore datastore.Client
// Datastorer represents things that can operate like a datastore.Client.
Datastorer interface {
Delete(context.Context, *datastore.Key) error
Get(context.Context, *datastore.Key, interface{}) error
GetAll(context.Context, *datastore.Query, interface{}) ([]*datastore.Key, error)
Put(context.Context, *datastore.Key, interface{}) (*datastore.Key, error)
PutMulti(context.Context, []*datastore.Key, interface{}) ([]*datastore.Key, error)
RunInTransaction(context.Context, func(Transactioner) error, ...datastore.TransactionOption) (*datastore.Commit, error)
}
// Transactioner represents things that can operate like a datastore.Transaction.
Transactioner interface {
Commit() (*datastore.Commit, error)
Delete(*datastore.Key) error
DeleteMulti([]*datastore.Key) error
Get(*datastore.Key, interface{}) error
GetMulti([]*datastore.Key, interface{}) error
Put(*datastore.Key, interface{}) (*datastore.PendingKey, error)
PutMulti([]*datastore.Key, interface{}) ([]*datastore.PendingKey, error)
Rollback() error
}
)
// Delete deletes the entity for the given key.
func (d *Datastore) Delete(ctx context.Context, key *datastore.Key) error {
return (*datastore.Client)(d).Delete(ctx, key)
}
// Get retrieves the entity for the given key.
func (d *Datastore) Get(ctx context.Context, key *datastore.Key, dst interface{}) error {
return (*datastore.Client)(d).Get(ctx, key, dst)
}
// GetAll retrieves all entities for the given query.
func (d *Datastore) GetAll(ctx context.Context, q *datastore.Query, dst interface{}) ([]*datastore.Key, error) {
return (*datastore.Client)(d).GetAll(ctx, q, dst)
}
// Put stores an entity for the given key.
func (d *Datastore) Put(ctx context.Context, key *datastore.Key, src interface{}) (*datastore.Key, error) {
return (*datastore.Client)(d).Put(ctx, key, src)
}
// PutMulti is a batch version of Put.
func (d *Datastore) PutMulti(ctx context.Context, keys []*datastore.Key, src interface{}) ([]*datastore.Key, error) {
return (*datastore.Client)(d).PutMulti(ctx, keys, src)
}
// RunInTransaction runs the given function in a transaction.
func (d *Datastore) RunInTransaction(ctx context.Context, f func(tx Transactioner) error, opts ...datastore.TransactionOption) (*datastore.Commit, error) {
return (*datastore.Client)(d).RunInTransaction(ctx, func(t *datastore.Transaction) error {
return f(t)
}, opts...)
}

Note that these interfaces do not emulate the complete SDK. I am only including functions that I actually call in my code. I'll add new ones as needed later.

When I try to use an instance of *datastore.Client as a Datastorer, I get the following error:

cannot use client (type *"cloud.google.com/go/datastore".Client) as type Datastorer in field value:
*"cloud.google.com/go/datastore".Client does not implement Datastorer (wrong type for RunInTransaction method)
have RunInTransaction(context.Context, func(*"cloud.google.com/go/datastore".Transaction) error, ..."cloud.google.com/go/datastore".TransactionOption) (*"cloud.google.com/go/datastore".Commit, error)
want RunInTransaction(context.Context, func(Transactioner) error, ..."cloud.google.com/go/datastore".TransactionOption) (*"cloud.google.com/go/datastore".Commit, error)

because *datastore.Client requires a function that takes a func(*datastore.Transaction) error and my interface wants a func(Transactioner) error.

Is there any way to change this so that it compiles?

If I can get it working, I plan to create types that implement my Datastorer and Transactioner interfaces and use maps to mock the real database. As far as tranactions go, for testing I can use sync.Mutex if I need them, but since each test is a single thread and will get its own database object, I may not need to lock them.

答案1

得分: 3

我已经使用以下代码使其编译通过:

package google

import (
	"context"

	"cloud.google.com/go/datastore"
)

type (
	// Datastore是Google Cloud Datastore Client的包装器。
	Datastore struct {
		*datastore.Client
	}

	// Datastorer表示可以像datastore.Client一样操作的东西。
	Datastorer interface {
		Delete(context.Context, *datastore.Key) error
		Get(context.Context, *datastore.Key, interface{}) error
		GetAll(context.Context, interface{}, interface{}) ([]*datastore.Key, error)
		Put(context.Context, *datastore.Key, interface{}) (*datastore.Key, error)
		PutMulti(context.Context, []*datastore.Key, interface{}) ([]*datastore.Key, error)
		RunInTransaction(context.Context, func(Transactioner) error, ...datastore.TransactionOption) (*datastore.Commit, error)
	}

	// Querier表示可以像datastore.Query一样操作的东西。
	Querier interface {
		Filter(string, interface{}) Querier
	}

	// Transactioner表示可以像datastore.Transaction一样操作的东西。
	Transactioner interface {
		Commit() (*datastore.Commit, error)
		Delete(*datastore.Key) error
		DeleteMulti([]*datastore.Key) error
		Get(*datastore.Key, interface{}) error
		GetMulti([]*datastore.Key, interface{}) error
		Put(*datastore.Key, interface{}) (*datastore.PendingKey, error)
		PutMulti([]*datastore.Key, interface{}) ([]*datastore.PendingKey, error)
		Rollback() error
	}
)

// Delete删除给定键的实体。
func (d *Datastore) Delete(ctx context.Context, key *datastore.Key) error {
	return d.Client.Delete(ctx, key)
}

// Get检索给定键的实体。
func (d *Datastore) Get(ctx context.Context, key *datastore.Key, dst interface{}) error {
	return d.Client.Get(ctx, key, dst)
}

// GetAll检索给定查询的所有实体。
func (d *Datastore) GetAll(ctx context.Context, q interface{}, dst interface{}) ([]*datastore.Key, error) {
	return d.Client.GetAll(ctx, q.(*datastore.Query), dst)
}

// Put存储给定键的实体。
func (d *Datastore) Put(ctx context.Context, key *datastore.Key, src interface{}) (*datastore.Key, error) {
	return d.Client.Put(ctx, key, src)
}

// PutMulti是Put的批量版本。
func (d *Datastore) PutMulti(ctx context.Context, keys []*datastore.Key, src interface{}) ([]*datastore.Key, error) {
	return d.Client.PutMulti(ctx, keys, src)
}

// RunInTransaction在事务中运行给定的函数。
func (d *Datastore) RunInTransaction(ctx context.Context, f func(tx Transactioner) error, opts ...datastore.TransactionOption) (*datastore.Commit, error) {
	return d.Client.RunInTransaction(ctx, func(t *datastore.Transaction) error {
		return f(t)
	}, opts...)
}

我将Datastore更改为包含datastore.Clientstruct,并添加了一个名为Querier的新接口,其中包含我从datastore.Query中使用的函数。我还更新了GetAll,使其接受一个interface{}而不是*datastore.Query,然后将其类型断言为*datastore.Query。我不能让它接受一个Querier,因为我无法传递类型为*datastore.Query的变量,因为它们不满足Querier接口(Filter返回的是Querier而不是*datastore.Query)。

所有使用在单独进程中运行的模拟器的现有测试都通过了。

更新

我将Datastore更改为:

Datastore datastore.Client

并在datastore.Query周围添加了一个包装器Query

Query datastore.Query

现在,Datastorer接口包含:

GetAll(context.Context, Querier, interface{}) ([]*datastore.Key, error)

并且GetAll函数定义为:

func (d *Datastore) GetAll(ctx context.Context, q Querier, dst interface{}) ([]*datastore.Key, error) {
	return (*datastore.Client)(d).GetAll(ctx, (*datastore.Query)(q.(*Query)), dst)
}

Query.Filter定义为:

func (q *Query) Filter(filterStr string, value interface{}) Querier {
	return (*Query)((*datastore.Query)(q).Filter(filterStr, value))
}

在调用代码中,我使用:

q := datastore.NewQuery(entity).Filter("Deleted =", false)
_, err := r.client.GetAll(ctx, (*Query)(q), data)

这样编译通过,并且所有测试都通过了。

英文:

I've gotten it to compile by using this code:

package google
import (
"context"
"cloud.google.com/go/datastore"
)
type (
// Datastore is a wrapper for the Google Cloud Datastore Client.
Datastore struct {
*datastore.Client
}
// Datastorer represents things that can operate like a datastore.Client.
Datastorer interface {
Delete(context.Context, *datastore.Key) error
Get(context.Context, *datastore.Key, interface{}) error
GetAll(context.Context, interface{}, interface{}) ([]*datastore.Key, error)
Put(context.Context, *datastore.Key, interface{}) (*datastore.Key, error)
PutMulti(context.Context, []*datastore.Key, interface{}) ([]*datastore.Key, error)
RunInTransaction(context.Context, func(Transactioner) error, ...datastore.TransactionOption) (*datastore.Commit, error)
}
// Querier represents things that can operate like a datastore.Query.
Querier interface {
Filter(string, interface{}) Querier
}
// Transactioner represents things that can operate like a datastore.Transaction.
Transactioner interface {
Commit() (*datastore.Commit, error)
Delete(*datastore.Key) error
DeleteMulti([]*datastore.Key) error
Get(*datastore.Key, interface{}) error
GetMulti([]*datastore.Key, interface{}) error
Put(*datastore.Key, interface{}) (*datastore.PendingKey, error)
PutMulti([]*datastore.Key, interface{}) ([]*datastore.PendingKey, error)
Rollback() error
}
)
// Delete deletes the entity for the given key.
func (d *Datastore) Delete(ctx context.Context, key *datastore.Key) error {
return d.Client.Delete(ctx, key)
}
// Get retrieves the entity for the given key.
func (d *Datastore) Get(ctx context.Context, key *datastore.Key, dst interface{}) error {
return d.Client.Get(ctx, key, dst)
}
// GetAll retrieves all entities for the given query.
func (d *Datastore) GetAll(ctx context.Context, q interface{}, dst interface{}) ([]*datastore.Key, error) {
return d.Client.GetAll(ctx, q.(*datastore.Query), dst)
}
// Put stores an entity for the given key.
func (d *Datastore) Put(ctx context.Context, key *datastore.Key, src interface{}) (*datastore.Key, error) {
return d.Client.Put(ctx, key, src)
}
// PutMulti is a batch version of Put.
func (d *Datastore) PutMulti(ctx context.Context, keys []*datastore.Key, src interface{}) ([]*datastore.Key, error) {
return d.Client.PutMulti(ctx, keys, src)
}
// RunInTransaction runs the given function in a transaction.
func (d *Datastore) RunInTransaction(ctx context.Context, f func(tx Transactioner) error, opts ...datastore.TransactionOption) (*datastore.Commit, error) {
return d.Client.RunInTransaction(ctx, func(t *datastore.Transaction) error {
return f(t)
}, opts...)
}

I changed DataStore to a struct containing the datastore.Client and added a new interface Querier that contains the functions that I am using from datastore.Query. I also updated GetAll to accept an interface{} instead of a *datastore.Query and then type-assert it to be a *datastore.Query. I cannot have it accept a Querier because then I cannot pass variables of type *datastore.Query because they do not satisfy the Querier interface (Filter returns a Querier instead of a *datastore.Query).

All existing tests using the emulator running in a separate process are passing.

UPDATE:

I changed Datastore to

Datastore datastore.Client

and added a wrapper Query around datastore.Query:

Query datastore.Query

Now, the Datastorer interface contains

GetAll(context.Context, Querier, interface{}) ([]*datastore.Key, error)

and the GetAll function is defined as

func (d *Datastore) GetAll(ctx context.Context, q Querier, dst interface{}) ([]*datastore.Key, error) {
return (*datastore.Client)(d).GetAll(ctx, (*datastore.Query)(q.(*Query)), dst)
}

and Query.Filter is defined as

func (q *Query) Filter(filterStr string, value interface{}) Querier {
return (*Query)((*datastore.Query)(q).Filter(filterStr, value))
}

In calling code, I use

q := datastore.NewQuery(entity).Filter("Deleted =", false)
_, err := r.client.GetAll(ctx, (*Query)(q), data)

This compiles and all tests are passing.

答案2

得分: 1

我知道这个问题很久以前就被问过了,但是如果有人仍然想知道如何模拟Google Datastore Client和Transaction,这里是我成功实现的一个示例。

type Client interface {
	Get(ctx context.Context, key *datastore.Key, dst interface{}) (err error)
	NewTransaction(ctx context.Context, opts ...datastore.TransactionOption) (t Transaction, err error)
	Put(ctx context.Context, key *datastore.Key, src interface{}) (*datastore.Key, error)
}

type Transaction interface {
	Commit() (c *datastore.Commit, err error)
	Rollback() (err error)
	Get(key *datastore.Key, dst interface{}) (err error)
	Put(key *datastore.Key, src interface{}) (*datastore.PendingKey, error)
	Delete(key *datastore.Key) error
}

type Datastore struct {
	*datastore.Client
}

func (d *Datastore) NewTransaction(ctx context.Context, opts ...datastore.TransactionOption) (t Transaction, err error) {
	return d.Client.NewTransaction(ctx, opts...)
}

当然,如果你使用不同的方法来操作Datastore,你需要自己实现它们。

在我的测试中,我现在可以像这样模拟Datastore Client和Transaction:

mockedClient := mock_gcloud.NewMockClient(ctrl)
mockedTransaction := mock_gcloud.NewMockTransaction(ctrl)
...
mockedClient.EXPECT().NewTransaction(context.Background()).Return(mockedTransaction, nil).Times(1)

就是这样。

英文:

I know that question has been asked a long time ago but in case one still may wondering how to Mock Google Datastore Client and Transaction here is a snapshot of how I got it to work.

type Client interface {
Get(ctx context.Context, key *datastore.Key, dst interface{}) (err error)
NewTransaction(ctx context.Context, opts ...datastore.TransactionOption) (t Transaction, err error)
Put(ctx context.Context, key *datastore.Key, src interface{}) (*datastore.Key, error)
}
type Transaction interface {
Commit() (c *datastore.Commit, err error)
Rollback() (err error)
Get(key *datastore.Key, dst interface{}) (err error)
Put(key *datastore.Key, src interface{}) (*datastore.PendingKey, error)
Delete(key *datastore.Key) error
}
type Datastore struct {
*datastore.Client
}
func (d *Datastore) NewTransaction(ctx context.Context, opts ...datastore.TransactionOption) (t Transaction, err error) {
return d.Client.NewTransaction(ctx, opts...)
}

Of course, If you are using different method against Datastore it's up to you to implement them.

In my tests, I can now Mock Datastore Client and Transaction like :

mockedClient := mock_gcloud.NewMockClient(ctrl)
mockedTransaction := mock_gcloud.NewMockTransaction(ctrl)
...
mockedClient.EXPECT().NewTransaction(context.Background()).Return(mockedTransaction, nil).Times(1)

Et voilà.

huangapple
  • 本文由 发表于 2017年8月29日 22:07:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/45941260.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定