使用Docker和Golang进行便捷的MongoDB测试


【编者的话】Docker的使用场景之一就是测试,在测试中,我们有时候会由于超时或者仅仅因为两个开发版本使用相同的数据库在同时运行而导致测试出错。本文以Golang和MongoDB为例,介绍了如何使用Docker来简化和改进单元测试。

背景
我们正在不断寻找新技术来解决开发中遇到的问题。我们一直在使用Java+Spring,然而Java 8和Spring Boot为我们带来了新的生机,并改变了单一的Java应用为微服务模式(译者注:monolithic Java applications)。而当你有API的时候,你只需一个合适的前端框架就可以替代jsp和jQuery:在我们的案例中我们选择AngularJS。两年前我们第一次使用了Angular,现在所有的项目都引入了AngularJS。

超过10年的Java 在你的灵魂深处留下了深刻的印记
这两三年来,我一直在寻找更好的东西。在这个行业中,最好的事情是你会有各种选择。我们曾经使用NodeJS构建了几个项目,并学习了Ruby的服务配置管理框架Chef。当然我们也有一些Scala项目,并了解过Clojure、Haskell和Rust语言,后来我们发现了Go。虽然我们只使用Go语言编写了几个小服务,但是却对与Go相关的语言、标准库、工具和社区而震惊。有大量的博客文章解释了为什么不同的公司选择了Go,本文不再赘述。同时如果你想学习如何编写Go,可以查阅A tour of Go,如果你喜欢阅读请查看Effective Go或观看A tour of Go视频。

负载测试
可能我需要相当长的篇幅来介绍负载测试,所有的编程语言都需要编写单元测试代码,另外还有一些需要使用TDD方法和达到100%测试覆盖率的目标的方法。动态语言需要安排更多类型的测试,可能当你经过了上百次的测试,你的应用才能达到一个稳定的状态。痛苦的是,由于有不同的开发语言,所以你的测试需要很多的准备工作:曾经几秒钟就可以完成的事情,那现在可能会需要几分钟,甚至是几十分钟才能完成。因此,你要开始仓库(数据库)的调用,并建立集成测试的数据库开发的预载和清除方法。有时候集成测试可能会失败,而这可能是由于超时或者仅仅因为两个开发版本使用相同的数据库在同时运行。

使用Golang和Docker进行的测试
Golang不会有类似的问题,有了Golang的快速构建、测试周期和一些Docker魔法的支持,你能够在几秒内启动MongoDB Docker容器并运行所有的测试。这个真的是从开始到结束只需要几秒的时间,但是第一次运行除外,因为第一次运行的时候需要下载和提供MongoDB Docker容器。

我从这里得到真正的灵感,即使一直在寻找借口来确认这是否是正确的:

3AF91CF4-74A0-4577-98C9-ECE3FFE7C014.jpg


让我们做一些可以进行Docker实验的nice的事情
我已经研究Golang+AngularJS一段时间了,而且现在是最佳时间来证明Docker是否如宣传的一样神奇。对于OS X用户来说,涉及到Docker时会有个小烦恼:它只在Linux上面运行。是的,你可以运用Boot2Docker来安装到OS X上,而Boot2Docker将在虚拟化的Linux上运行Docker。我已经通过Ubuntu来使用Vagrant作为开发环境,因此我刚刚在这上面安装了Docker。

首先,我要熟悉Camlistore的实施环境并且复制它。特别感谢Brad Fitzpartick,你通过Camlistore和Golang标准程序库来完成了出色的工作。Thanks!

可以通过story_test.go来找到实际测试。对于那些看不懂Golang的用户,我已经在最重要的代码部分添加了额外的注释。
Setup test environment
func TestStoryCreateAndGet(t *testing.T) {

// Start MongoDB Docker container
//
// One of the most powerful features in Golang
// is the ability to return multiple values from functions.
// In this we get:
// - containerID (type=ContainerID struct)
// - ip (type=string)
containerID, ip := dockertest.SetupMongoContainer(t)

// defer schedules KillRemove(t) function call to run immediatelly
// when TestStoryCreateAndGet(t) function is done,
// so you can place resource clenup code close to resource allocation
defer containerID.KillRemove(t)

app := AppContext{}

// Connect to Dockerized MongoDB
mongoSession, err := mgo.Dial(ip)

// Golang favors visible first hand error handling.
// Main idea is that Errors are not exceptional so you should handle them
if err != nil {
Error.Printf("MongoDB connection failed, with address '%s'.", Configuration.MongoUrl)
}

// close MongoDB connections when we're finished
defer mongoSession.Close()

app.mongoSession = mongoSession

// create test http server with applications route configuration
ts := httptest.NewServer(app.createRoutes())
defer ts.Close()

storyId := testCreate(ts, t) // run create test
testGet(ts, storyId, t) // run get test for created story
}

Post json document to http handler
func testCreate(ts *httptest.Server, t *testing.T) string {

postData := strings.NewReader("{\"text\":\"tekstiä\",\"subjectId\":\"k2j34\",\"subjectUrl\":\"www.fi/k2j34\"}")

// create http POST with postData JSON
res, err := http.Post(ts.URL+"/story", applicationJSON, postData)

// read http response body data
data, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Error(err)
}

id := string(data)

// verify that we got correct http status code
if res.StatusCode != http.StatusCreated {
t.Fatalf("Non-expected status code: %v\n\tbody: %v, data:%s\n", http.StatusCreated, res.StatusCode, id)
}

// verify that we got valid lenght response data
if res.ContentLength != 5 {
t.Fatalf("Non-expected content length: %v != %v\n", res.ContentLength, 5)
}
return id
}

Test that previously created story exists
func testGet(ts *httptest.Server, storyId string, t *testing.T) {

// create http GET request with correct path
res, err := http.Get(ts.URL + "/story/" + storyId)
data, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Error(err)
}

body := string(data)

// validate status code
if res.StatusCode != http.StatusOK {
t.Fatalf("Non-expected status code: %v\n\tbody: %v, data:%s\n", http.StatusCreated, res.StatusCode, body)
}

// validate that response has correct storyId
if !strings.Contains(body, "{\"storyId\":\""+storyId+"\",") {
t.Fatalf("Non-expected body content: %v", body)
}

// validate that content leght is what is should be
if res.ContentLength < 163 && res.ContentLength > 165 {
t.Fatalf("Non-expected content length: %v < %v, content:\n%v\n", res.ContentLength, 160, body)
}

}


眼见为实
因此,启动MongoDB Docker容器,将它配置到应用程序,然后用内置的测试直至创建HTTP服务器。然后,我们设置同样的路由给服务器,并对测试服务器运行两个请求,第一个来创建故事评论,另外一个来获取它。所有的数据都被存储了,并且从MongoDB中获取。那么所有这一切需要多久时间呢?

docker-testing-0d333fd6d1b21887e978d4d3110dc715.png

仅两秒以下!

docker-testing-with-race-detection-567c060af6235504c42111c3c5d33446.png

即使你运行一些条件选择器它仍只需要不到3秒 \o/

Docker是针对所有的用户,而不仅仅是Golang用户

对于那些可以使用Golang的用户,Docker也可以帮助你。它当然没有Golang那么快速,但是和使用外部的MongoDB服务器一样的快,而且没有额外的清理麻烦。毫无疑问,Docker是虚拟化业务中的游戏变化者,并且这些炒作也得到了很好的回报。这样就没有借口来针对MongoDB功能编写任何模拟测试。

原文链接:Painless MongoDB testing with Docker and Golang(翻译:吴锦晟 校对:李颖杰)

===============================================
译者介绍
吴锦晟,大连理工大学硕士研究生,就职于上海金桥信息股份有限公司技术中心。目前负责云计算、虚拟化、大数据及其信息可视化等方向的研究和应用。希望通过翻译技术文章于Dockone社区为Docker的步道做出微薄贡献。

1 个评论

OneAPM公司的Cloud Insight 集数据库监控(如 MySQL, MongoDB 等)于一身。可以在官网注册试用哦~

要回复文章请先登录注册