为更好地进行集成测试而开发的Go Package

209 阅读4分钟

在为与数据存储交互的代码编写测试时,我们通常会面临一个两难的问题。

  • 我们应该模拟对数据存储的调用
  • 我们应该使用真实的数据存储来编写集成测试吗?

为了清楚起见,当我说与数据存储交互时,我指的是我们实际上是在实现具体的 存储库直接与数据存储对话,而不是使用上述存储库的应用服务类型。

嘲弄数据存储的调用

我们有不同的方法来编写我们的测试,这取决于我们使用的数据存储,例如,如果我们正在测试数据库调用,恰好使用database/sql ,那么导入一个像github.com/DATA-DOG/go…的包就可以了。

在没有用于模拟调用的事实包的情况下,我们可以定义我们自己的接口类型,恰好定义我们在代码中使用的具体调用,例如,如果我们计划模拟memcached调用,并且 github.com/bradfitz/gomemcache它正在被使用,那么像下面这样的方法就可以了。

// MemcacheClient defines the methods required by our Memcached implementation.
type MemcacheClient interface {
	Get(string) (*memcache.Item, error)
	Set(*memcache.Item) error
}

// AdapterMemcached uses a mocked client for testing.
type AdapterMemcached struct {
	client MemcacheClient
}

其中AdapterMemcached 可以接收真正的memcache.Client ,以及一个模拟我们需要的方法的类型,允许我们成功地编写测试,如。

func TestAdapterMemcached(t *testing.T) {
	// TODO: Test sad/unhappy-paths cases

	mock := mockingtesting.FakeMemcacheClient{}
	mock.GetReturns(&memcache.Item{
		Value: func() []byte {
			var b bytes.Buffer
			_ = gob.NewEncoder(&b).Encode("value")
			return b.Bytes()
		}(),
	}, nil)

	c := mocking.NewAdapterMemcached(&mock)

	if err := c.Set("key", "value"); err != nil {
		t.Fatalf("expected no error, got %s", err)
	}

	value, err := c.Get("key")
	if err != nil {
		t.Fatalf("expected no error, got %s", err)
	}

	if value != "value" {
		t.Fatalf("expected `value`, got %s", value)
	}
}

编写集成测试

嘲弄数据存储的调用对于测试来说绝对是有效的,它允许我们更专注于我们的业务逻辑,然而代价是缺乏集成测试,直到代码被部署到实际环境中。

在Docker之前,模拟数据存储是理想的解决方案,但现在设置一个本地容器运行具体的数据存储版本而不与本地环境发生冲突真的很简单,使用一个真正的数据存储并针对它们运行这些测试很便宜

一个简单的解决方案通常是在执行测试套件之前提前运行容器,然而由于github.com/ory/dockert…,在每个测试案例之前实际运行容器很容易。

ory/dockertest 我们使用Docker来运行和管理容器,它的工作方式是使用其API在幕后进行交互,以正确设置我们需要的任何容器,在我们的例子中,我们将需要 ,所以实现一个像下面这样的函数应该是可行的。memcached:1.6.9-alpine

func newClient(tb testing.TB) *memcache.Client {
	pool, err := dockertest.NewPool("")
	if err != nil {
		tb.Fatalf("Could not instantiate docker pool: %s", err)
	}

	pool.MaxWait = 2 * time.Second

	// 1. Define configuration options for the container to run.

	resource, err := pool.RunWithOptions(&dockertest.RunOptions{
		Repository: "memcached",
		Tag:        "1.6.6-alpine",
	}, func(config *docker.HostConfig) {
		config.AutoRemove = true
		config.RestartPolicy = docker.RestartPolicy{
			Name: "no",
		}
	})

	if err != nil {
		tb.Fatalf("Could not run container: %s", err)
	}

	addr := fmt.Sprintf("%s:11211", resource.Container.NetworkSettings.IPAddress)
	if runtime.GOOS == "darwin" { // XXX: network layer is different on Mac
		addr = net.JoinHostPort(resource.GetBoundIP("11211/tcp"), resource.GetPort("11211/tcp"))
	}

	// 2. Wait until the container is available and instantiate the actual client
	//    the value set above in `pool.MaxWait` determines how long it should wait.

	if err := pool.Retry(func() error {
		var ss memcache.ServerList
		if err := ss.SetServers(addr); err != nil {
			return err
		}

		return memcache.NewFromSelector(&ss).Ping()
	}); err != nil {
		tb.Fatalf("Could not connect to memcached: %s", err)
	}

	tb.Cleanup(func() {
		// 3. Get rid of the containers previously launched.

		if err := pool.Purge(resource); err != nil {
			tb.Fatalf("Could not purge container: %v", err)
		}
	})

	return memcache.New(addr)
}

这将为每个测试套件或测试案例运行一个新的容器(取决于我们计划如何运行我们的测试),通过该初始化,我们将能够实际运行一个memcached docker容器,以便在我们的测试中使用它,也涵盖了需要memcached.Client 的类型。

func TestConcreteMemcached(t *testing.T) {
	// TODO: Test sad/unhappy-paths cases

	client := newClient(t)

	c := mocking.NewConcreteMemcached(client)

	if err := c.Set("concrete-key", "value"); err != nil {
		t.Fatalf("expected no error, got %s", err)
	}

	value, err := c.Get("concrete-key")
	if err != nil {
		t.Fatalf("expected no error, got %s", err)
	}

	if value != "value" {
		t.Fatalf("expected `value`, got %s", value)
	}
}

结论

在Go中使用数据存储时,没有任何借口不使用github.com/ory/dockert…ory/dockertest ,简化了集成测试,减少了运行我们的测试时需要的依赖性,因为在这些情况下只需要Docker。

然而,我们应该小心,不要过度使用它,我们需要记住,尽管我们可以选择为每个子测试创建一个容器,但最终可能不是最好的主意。我们应该测量我们的测试的持续时间,以确定我们的套件所需的容器的最佳数量,我的建议是开始时为每个测试用例使用一个,根据我们需要的隔离,也许增加该值以匹配子测试的数量。

最后,ory/dockertest 是必须的,强烈推荐