Z.S.K.'s Records

bash的自动化测试框架-bats

今天在kubernetes中部署mongodb-rs时,发现其helm模板中使用到了一个bats,搜索了一下发现这是一个对bash领域的自动化测试框架,简单学习一下.

使用

mongodb-rs的部署不在这里详述了,直接来到使用到bats的地方,

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
---
# Source: mongodb-replicaset/templates/tests/mongodb-up-test-pod.yaml
apiVersion: v1
kind: Pod
metadata:
labels:
app: mongodb-replicaset
chart: mongodb-replicaset-3.17.0
heritage: Helm
release: mongodb-rs
name: mongodb-rs-mongodb-replicaset-test
namespace: infra
annotations:
"helm.sh/hook": test-success
spec:
initContainers:
- name: test-framework
image: dduportal/bats:0.4.0
command:
- bash
- -c
- |
set -ex
# copy bats to tools dir
cp -R /usr/local/libexec/ /tools/bats/
volumeMounts:
- name: tools
mountPath: /tools
containers:
- name: mongo
image: "mongo:4.2"
command:
- /tools/bats/bats
- -t
- /tests/mongodb-up-test.sh
env:
- name: FULL_NAME
value: mongodb-rs-mongodb-replicaset
- name: NAMESPACE
value: infra
- name: REPLICAS
value: "3"
- name: AUTH
value: "true"
- name: ADMIN_USER
valueFrom:
secretKeyRef:
name: "mongodb-rs-mongodb-replicaset-admin"
key: user
- name: ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: "mongodb-rs-mongodb-replicaset-admin"
key: password
volumeMounts:
- name: tools
mountPath: /tools
- name: tests
mountPath: /tests
volumes:
- name: tools
emptyDir: {}
- name: tests
configMap:
name: mongodb-rs-mongodb-replicaset-tests
restartPolicy: Never # 指定pod的启动命令为Never,表示只运行一次.

首先看到initContainers执行了cp -R /usr/local/libexec/ /tools/bats/,从bats github中可以看到,这个目录下是一些可执行的bash代码,这是整个核心.

然后在主containers中执行了/tools/bats/bats -t /tests/mongodb-up-test.sh, 这个脚本内容如下:

cat mongodb-up-test.sh

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
#!/usr/bin/env bash

set -ex

CACRT_FILE=/work-dir/tls.crt
CAKEY_FILE=/work-dir/tls.key
MONGOPEM=/work-dir/mongo.pem

MONGOARGS="--quiet"

if [ -e "/tls/tls.crt" ]; then
# log "Generating certificate"
mkdir -p /work-dir
cp /tls/tls.crt /work-dir/tls.crt
cp /tls/tls.key /work-dir/tls.key

# Move into /work-dir
pushd /work-dir

cat >openssl.cnf <<EOL
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = $(echo -n "$(hostname)" | sed s/-[0-9]*$//)
DNS.2 = $(hostname)
DNS.3 = localhost
DNS.4 = 127.0.0.1
EOL

# Generate the certs
openssl genrsa -out mongo.key 2048
openssl req -new -key mongo.key -out mongo.csr -subj "/OU=MongoDB/CN=$(hostname)" -config openssl.cnf
openssl x509 -req -in mongo.csr \
-CA "$CACRT_FILE" -CAkey "$CAKEY_FILE" -CAcreateserial \
-out mongo.crt -days 3650 -extensions v3_req -extfile openssl.cnf
cat mongo.crt mongo.key > $MONGOPEM
MONGOARGS="$MONGOARGS --ssl --sslCAFile $CACRT_FILE --sslPEMKeyFile $MONGOPEM"
fi

if [[ "${AUTH}" == "true" ]]; then
MONGOARGS="$MONGOARGS --username $ADMIN_USER --password $ADMIN_PASSWORD --authenticationDatabase admin"
fi

pod_name() {
local full_name="${FULL_NAME?Environment variable FULL_NAME not set}"
local namespace="${NAMESPACE?Environment variable NAMESPACE not set}"
local index="$1"
echo "$full_name-$index.$full_name.$namespace.svc.cluster.local"
}

replicas() {
echo "${REPLICAS?Environment variable REPLICAS not set}"
}

master_pod() {
for ((i = 0; i < $(replicas); ++i)); do
response=$(mongo $MONGOARGS "--host=$(pod_name "$i")" "--eval=rs.isMaster().ismaster")
if [[ "$response" == "true" ]]; then
pod_name "$i"
break
fi
done
}

setup() {
local ready=0
until [[ "$ready" -eq $(replicas) ]]; do
echo "Waiting for application to become ready" >&2
sleep 1
ready=0
for ((i = 0; i < $(replicas); ++i)); do
response=$(mongo $MONGOARGS "--host=$(pod_name "$i")" "--eval=rs.status().ok" || true)
if [[ "$response" -eq 1 ]]; then
ready=$((ready + 1))
fi
done
done
}

@test "Testing mongodb client is executable" {
mongo -h
[ "$?" -eq 0 ]
}

@test "Connect mongodb client to mongodb pods" {
for ((i = 0; i < $(replicas); ++i)); do
response=$(mongo $MONGOARGS "--host=$(pod_name "$i")" "--eval=rs.status().ok")
if [[ ! "$response" -eq 1 ]]; then
exit 1
fi
done
}

@test "Write key to primary" {
response=$(mongo $MONGOARGS --host=$(master_pod) "--eval=db.test.insert({\"abc\": \"def\"}).nInserted")
if [[ ! "$response" -eq 1 ]]; then
exit 1
fi
}

@test "Read key from slaves" {
# wait for slaves to catch up
sleep 10

for ((i = 0; i < $(replicas); ++i)); do
response=$(mongo $MONGOARGS --host=$(pod_name "$i") "--eval=rs.slaveOk(); db.test.find({\"abc\":\"def\"})")
if [[ ! "$response" =~ .*def.* ]]; then
exit 1
fi
done

# Clean up a document after test
mongo $MONGOARGS --host=$(master_pod) "--eval=db.test.deleteMany({\"abc\": \"def\"})"
}

该脚本的上半部分可以先忽略

然后定义了几个函数, 其实setup()函数在bats中是特殊的函数,其实还有一个teardown,主要用于在执行测试函数前/后执行的两个函数, setup用于做一些准备工作,teardown用于做一些清理工作

最重要的部分是@test,比如:

1
2
3
4
@test "Testing mongodb client is executable" {
mongo -h
[ "$?" -eq 0 ]
}

@test是一个关键字,指定被包含代码块需要被testing.在上面的脚本中指定了4个@test函数. 它到底是如何执行的呢?

源码分析

首先: 在bats-preprocess中,这个代码主要是解析待测试脚本,可以发现以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pattern='^ *@test  *([^ ].*)  *\{ *(.*)$'

while IFS= read -r line; do
let index+=1
if [[ "$line" =~ $pattern ]]; then
quoted_name="${BASH_REMATCH[1]}"
body="${BASH_REMATCH[2]}"
name="$(eval echo "$quoted_name")"
encoded_name="$(encode_name "$name")"
tests["${#tests[@]}"]="$encoded_name"
echo "${encoded_name}() { bats_test_begin ${quoted_name} ${index}; ${body}"
else
printf "%s\n" "$line"
fi
done

通过解析@test来得到有多个需要testing的代码块,从pod的日志中也可以看出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
++ bats_test_function test_Testing_mongodb_client_is_executable
++ local test_name=test_Testing_mongodb_client_is_executable
++ BATS_TEST_NAMES["${#BATS_TEST_NAMES[@]}"]=test_Testing_mongodb_client_is_executable
++ bats_test_function test_Connect_mongodb_client_to_mongodb_pods
++ local test_name=test_Connect_mongodb_client_to_mongodb_pods
++ BATS_TEST_NAMES["${#BATS_TEST_NAMES[@]}"]=test_Connect_mongodb_client_to_mongodb_pods
++ bats_test_function test_Write_key_to_primary
++ local test_name=test_Write_key_to_primary
++ BATS_TEST_NAMES["${#BATS_TEST_NAMES[@]}"]=test_Write_key_to_primary
++ bats_test_function test_Read_key_from_slaves
++ local test_name=test_Read_key_from_slaves
++ BATS_TEST_NAMES["${#BATS_TEST_NAMES[@]}"]=test_Read_key_from_slaves
+ '[' -n '' ']'
+ bats_perform_tests test_Testing_mongodb_client_is_executable test_Connect_mongodb_client_to_mongodb_pods test_Write_key_to_primary test_Read_key_from_slaves
+ echo 1..4
+ test_number=1
+ status=0
1..4
+ for test_name in "$@"
+ /tools/bats/bats-exec-test /tests/mongodb-up-test.sh test_Testing_mongodb_client_is_executable 1
+

从日志中可以看到,成功解析到了4个@test代码块, 然后最后循环调用/tools/bats/bats-exec-test, 来看看这个函数

传递了三个参数,第一个是脚本文件,第二个是获取到的@test代码块的名字, 第三个是序号

/tools/bats/bats-exec-test,调用这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bats_perform_test() {
BATS_TEST_NAME="$1"
if [ "$(type -t "$BATS_TEST_NAME" || true)" = "function" ]; then
BATS_TEST_NUMBER="$2"
if [ -z "$BATS_TEST_NUMBER" ]; then
echo "1..1"
BATS_TEST_NUMBER="1"
fi

BATS_TEST_COMPLETED=""
BATS_TEARDOWN_COMPLETED=""
trap "bats_debug_trap \"\$BASH_SOURCE\"" debug # trap是bash中用于捕捉信号函数
trap "bats_error_trap" err
trap "bats_teardown_trap" exit
"$BATS_TEST_NAME" >>"$BATS_OUT" 2>&1 # 执行命令
BATS_TEST_COMPLETED=1

else
echo "bats: unknown test name \`$BATS_TEST_NAME'" >&2
exit 1
fi
}

因此,当命令在执行过程出现error时则会被trap捕获到,然后设置错误码

1
2
3
4
5
bats_error_trap() {
BATS_ERROR_STATUS="$?"
BATS_ERROR_STACK_TRACE=( "${BATS_PREVIOUS_STACK_TRACE[@]}" )
trap - debug
}

这样,一旦生成了错误码,则pod的状态即会出现异常状态,而正常情况下指定了restart:Never的pod执行完启动命令的状态会变成complete.

1
2
3
4
5
6
# kubectl get pod -n infra
NAME READY STATUS RESTARTS AGE
mongodb-rs-mongodb-replicaset-0 1/1 Running 0 4h36m
mongodb-rs-mongodb-replicaset-1 1/1 Running 0 4h35m
mongodb-rs-mongodb-replicaset-2 1/1 Running 0 4h35m
mongodb-rs-mongodb-replicaset-test 0/1 Completed 0 4h22m

bats还有一些其它的使用技巧, 比如可以使用skip来跳过某些@test代码块、可以用于@test网页等.可到github查看

不过batsN久没有更新了, 有一个改良版的bats-core,是在bats的基本之上改良的,感兴趣的可以看看.

参考文章:

转载请注明原作者: 周淑科(https://izsk.me)

 wechat
Scan Me To Read on Phone
I know you won't do this,but what if you did?