docker在测试中的作用

郭柱明


cayley与docker

cayley是一个用go语言编写的很规范的图数据库 他的设计方式很值得我们学习 这篇文章我们简单聊一聊docker在cayley这个图数据库中的应用

docker主要在cayley中有两处使用

  • 将整个cayley作为一个应用打包为一个docker镜像
  • 在每一个涉及持久层操作的单元测试和集成测试中 使用docker生成一个一次性数据库供数据存储

第一点很常见 就是使用dockerfile的方式将应用打包成镜像 这样我们可以通过docker很简单地安装和运行cayley
image_1crovr4t31hmc14814cdg3qmtd9.png-172.6kB
但是第二点真的很少见 cayley在第二点上算是非常创新的做法 现在主要讨论第二点


docker在测试中的使用

docker的使用情景有很多种 我们可以看一下阮一峰博客上说的
image_1crp006g81526bpjoll1t61kk3m.png-24.8kB

cayley在这里正是第一种情况 使用docker提供一次性环境

因为cayley是支持多种数据库作为底层存储 如果在开发测试过程 对每一种数据都进行本地安装 本地系统环境等因素一定会让操作很麻烦 所以cayley选择在各种测试进行的时候使用docker镜像生成一个需要的数据库容器 并且将这个容器运行起来 返回这个容器运行在的IP地址和端口 这样就可以提供给测试的代码进行连接 注意整个测试并不是发生在容器内部的 仅仅是数据库在容器内部

当我们操作完需要的持久层操作之后 就可以彻底删除这个容器 也不会导致测试的数据遗留

如果现在不熟悉docker的基本使用 可以先看一下阮一峰这篇博客先了解简单的docker helloworld


cayley使用一个封装了docker remote api的第三方包go docker client生成一个数据库容器的步骤如下
生成一个数据库容器

注意 cayley是原本缺失第二步pullimage的 所以导致原来运行cayley所有的测试都会导致设计持久层的测试被skip 正就是我帮cayley修复那个bug 扬哥因此送了我一瓶红酒 commit详情在pull image from remote repository if there is not image at local machine 这是我人生第一次给比较大的开源项目贡献代码。。

下面是我看了cayley的代码后 整理的一个hello world 生成一个可用的mongo的容器 运行 并且返回mongo的IP地址和端口进行连接 因为下面我已经写了尽可能多的注释了 所以不一一解释了

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
package main

import (
"github.com/fsouza/go-dockerclient"
"time"
"log"
"fmt"
"bytes"
"runtime"
"strconv"
"net"
"math/rand"
)


//配置
type fullConfig struct {
//主要是镜像的配置
docker.Config
//主要是容器的配置
docker.HostConfig
}

//检测IP地址和端口是否可用 可连接
const wait = time.Second * 5
func waitPort(addr string) bool {
start := time.Now()
c, err := net.DialTimeout("tcp", addr, wait)
if err == nil {
c.Close()
} else if dt := time.Since(start); dt < wait {
time.Sleep(wait - dt)
}
return err == nil
}

//随机选择可用的端口
const localhost = "127.0.0.1"
func randPort() int {
const (
min = 10000
max = 30000
)
for {
port := min + rand.Intn(max-min)
c, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", localhost, port), time.Second)
if c != nil {
c.Close()
}
if err != nil {
// TODO: check for a specific error
return port
}
}
}

func main() {
//一般通过以下sock文件初始化docker client
Address := `unix:///var/run/docker.sock`

//初始化镜像和容器的配置
var conf docker.Config
port := "27017"
//镜像的名字叫mongo
conf.Image = "mongo"
conf.OpenStdin = true
conf.Tty = true
//容器暴露出27017这个端口
conf.ExposedPorts = map[docker.Port]struct{} {
"27017/tcp": {},
}
fconf := fullConfig{
Config: conf,
HostConfig: docker.HostConfig{
//端口映射 容器的27017端口映射到本地的27017端口
PortBindings: map[docker.Port][]docker.PortBinding{
"27017/tcp": {
{
HostIP: "0.0.0.0",
HostPort: port,
},
},
},
},
}

//在linux系统下 可以原生地运行docker 但是在其他系统下需要特殊处理 随机选择可用的端口进行映射
if runtime.GOOS != "linux" {
log.Println("this is not linux")
lport := strconv.Itoa(randPort())
// nothing except Linux runs Docker natively,
// so we randomize the port and expose it on Docker VM
fconf.PortBindings = map[docker.Port][]docker.PortBinding{
docker.Port(port + "/tcp"): {{
HostIP: localhost,
HostPort: lport,
}},
}
port = lport
log.Println("this is not linux env, change the port to", lport)
}

//初始化docker client
cli, err := docker.NewClient(Address)
if err != nil {
panic(err)
}

//从远程拉取指定的docker镜像 如果本地已经存在指定镜像 那么操作被跳过
var buf bytes.Buffer
if err := cli.PullImage(docker.PullImageOptions{
//通过 镜像所在组/镜像名称:镜像标签 来指定特定的镜像
Repository: "docker.io/mongo:latest",
//指定输出位置
OutputStream: &buf,
}, docker.AuthConfiguration{}); err != nil {
log.Println("pull 容器失败")
panic(err)
}
log.Println("buf", buf.String())

//从上面的镜像中生成一个docker容器
cont, err := cli.CreateContainer(docker.CreateContainerOptions{
Config: &fconf.Config,
HostConfig: &fconf.HostConfig,
})
if err != nil {
log.Println("创建容器失败")
panic(err)
}
//生成一个强制移除容器的关闭函数
closer := func() {
cli.RemoveContainer(docker.RemoveContainerOptions{
ID: cont.ID,
Force: true,
})
}
defer closer()

//启动容器
if err := cli.StartContainer(cont.ID, &fconf.HostConfig); err != nil {
log.Println("启动容器失败")
closer()
panic(err)
}

//监听容器是否启动成功
info, err := cli.InspectContainer(cont.ID)
if err != nil {
log.Println("监视容器失败")
closer()
panic(err)
}

//获取容器运行在的IP地址和端口 这里默认使用的是 bridge的网络模式
addr := info.NetworkSettings.IPAddress
addr += ":" + port

//在10机会监听数据库的连接url是否可用
ok := false
for i := 0; i < 10 && !ok; i++ {
ok = waitPort(addr)
if !ok {
time.Sleep(time.Second * 2)
}
}

if !ok {
log.Println("一直连接失败")
closer()
log.Fatal("tcp connect fail")
}
fmt.Println("addr", addr)
}

cayley将上面的常用操作都封装成函数在docker操作 这些使用docker生成一次性数据库并使用的一个例子可见mongo_test

cayley在这一点上做的很精彩 也很创新 非常值得我们学习