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

  一、背景

  我们正在不断寻找新技术来解决开发中遇到的问题。我们一直在使用Java+Spring,但是Java 8与Spring Boot为我们带来了新的生机,不同于mysql,并且改变了单一的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容器。

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

  

图1

  让我们做一些可以进行Docker实验的nice的事情

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

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

  可以通过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中获取。那么所有这一切需要多久时间呢?如图2所示

  

图2

  仅两秒以下!就有了图3所示的结果

  

图3

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

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

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

  原文链接:Painless MongoDB testing with Docker and Golang

  以后我们会更新一些mysql的文章,关心mysql的朋友敬请期待。