使用 Travis CI 实现项目的持续测试反馈
[篇幅较长,10.15前补充完毕,如希望探索可直接移步Github仓库:https://github.com/SivilTaram/CITest]
在编程课中,我们可以使用成熟的在线评测系统来测试某个代码块或文件在功能实现上的正确性。但在软件工程课中,对项目的自动测试仍然是一个有挑战性的问题。一个比较复杂的软件工程项目往往由多个文件组成,开发者可能会调用不同的第三方库函数,使用不同的编译环境(比如Mac/Linux/Windows),这些因素导致了自动测试项目的复杂性。
早在布置数独项目给福州大学的学生时,我就意识到了自动测试项目不是一个可以一锤子做完的买卖。核心测试的代码并不难写,例如数独生成的项目,核心就是检查被测试程序生成的数独棋盘是否符合规范。按照我的经验,真正阻碍许多助教独立开发自动测试项目的因素主要是三点:
- 助教需要写一个爬虫自动爬取所有学生的Github仓库地址,将它们克隆到本机,并把每个项目仓库存储目录都重命名为学生学号以记录测试成绩。这一点要求助教能分析一些基本的网络请求,并利用代码构造请求。
- 助教要在布置项目时对学生的开发进行一些约束,并写一个带异常处理的测试程序来处理不同约束被破坏的情况。这就意味着,自动测试项目除了能够评测正确情况外,还要对一些约束不满足的情况给出合理的报错,比如说
规定的 src 文件夹不存在
。这一点要求助教完成一个鲁邦、友好又准确无误的测试程序,有些时候测试程序甚至比项目本身更复杂。 - 如果在布置项目时要求学生提交源码的同时也提交可执行程序(比如.exe),助教无法从源头上保证学生提交的可执行程序编译于他自己的代码;但如果不要求学生提交可执行程序,即使约束了指定编译环境与测试命令,助教也会面临大部分项目编译失败的风险(这是由项目复杂度引起的)。这一点要求助教在本机安装必要的编译环境,每个助教都需要做一遍环境配置,很慢很麻烦。
其实这三点问题多多少少都与测试策略的选择有关:目前软件工程课程中普遍采用后处理的自动测试策略,即学生完成开发后助教才进行测试,再过一定时间学生才能知道自己项目的错误。 但在实践中,大部分学生是无法一次通过测试,不能通过的原因往往不是代码没写好,而是因为他们遗漏或错误解读了作业细节。这种源于双方认知差异而导致的问题也不一定就是学生们的失误,也可能是老师或助教没有明确指定一些看似显然的约束条件。为了避免这种问题的产生,现有的测试流程中需要加入持续测试反馈的环节。结合前人的经验,本文制定了一套新的测试流程,其中的关键就在于 Git 提供的 Pull Request 与 Continuous Integration (持续集成,下面简称为CI) 工具。本文下面将分三个部分来说明新的测试流程,首先是前后测试流程的对比,接着是一个基于Travis CI与Github搭建的持续测试的最小工作示例,最后是教各位助教定制化自己的测试脚本。
测试流程
在传统软件工程课程的测试中,测试流程如下图所示:
- 收集地址: 每个学生建一个独立的仓库,将仓库地址评论到教师博客下(或者以其他方式收集)。
- 克隆仓库: 在作业截止日期后,助教克隆所有项目。
- 自动编译[可选]: 助教运行自动编译程序,得到能够编译项目的可执行程序。
- 自动测试: 助教运行自动测试程序,记录学生项目的错误日志。
- 错误反馈: 助教向学生反馈项目中出现的错误,学生进行更正。返回第 2 步。
在将持续测试反馈加入之前的测试闭环中后,新的测试流程如下图所示:
- 发布仓库: 教师发布一个公开仓库,学生 Fork。
- 静态检查: 学生完成开发,发起 Pull Request。CI 检测到新的 Pull Request 发起,运行教师预先设定的静态检查程序。
- 持续反馈: 完成静态检查后, CI 自动在相应的 Pull Request 中追加评测信息(包括是否通过初步检查,错误日志等)。学生看到评测信息后更正程序,回到第 2 步。
- 持续编译[可选]: 完成静态检查后,CI 运行教师预先设定的编译脚本,把由学生仓库编译得到的可执行程序补充 commit 到相应的文件夹下。
- 动态测试: 在作业截止日期后,助教运行动态测试程序,测试可执行程序在相应输入下是否可以得到期望输出,得到不同测试样例下的得分。
新的测试流程对助教显得更友好一些:Pull Request 的仓库收集方式避免了爬虫的工作量,CI 的自动反馈帮助学生持续地获得反馈,也使得助教不再需要定时反馈项目错误信息。同时,这种测试流程可以保证 git 仓库中的可执行文件一定是从学生自己的源代码编译得到的,不需要助教在本机安装任何编译器。
最小工作范例
接下来本文将展示一个基于上述测试流程的最小工作范例,主要用到的相关知识如下:
前两项是必须学会的背景知识,如果不会的话请先熟悉Git的基本命令与至少一门编程语言。
发布仓库
在 https://github.com/join 这个网址处申请注册一个Github账号,申请成功后可在https://github.com/login 处利用刚刚注册的账号进行登录,开始在Github上进行开发。
点击右上角的 +
号,选择 New repository
。
给 Respository 起个名字,比如叫 CITest
,点击 Create repository
即可发布一个仓库。
持续集成工具对 Public 仓库是免费的,仓库默认Public;如果选择 Private 的话,持续集成工具会收费。
后续的 Fork
与发起 Pull Request
的步骤可以在这个教程中找到,这里不再赘述。
静态检查
静态检查,顾名思义,主要用于规范学生们提交仓库的组织目录与文件结构。例如检查仓库中是否有 src 文件夹,是否有 main.java等,是否可以成功编译等。静态检查原则上不会运行仓库中的可执行程序。比如下面就是一个用Python
写的静态检查样例程序:
import os
import sys
def check_structure(dir_path):
# check if exist `src`
src_full = os.path.join(dir_path, "src")
if not os.path.exists(src_full):
raise AssertionError("没有 src 文件夹!")
# check if exist `bin`
bin_full = os.path.join(dir_path, "bin")
if not os.path.exists(bin_full):
raise AssertionError("没有 bin 文件夹!")
# check is exist `main.py`
main_full = os.path.join(dir_path, "src", "main.py")
if not os.path.exists(main_full):
raise AssertionError("在 src 文件夹下没有 main.py 文件!")
if __name__ == '__main__':
check_dir = sys.argv[1]
try:
check_structure(check_dir)
sys.exit(0)
except AssertionError as e:
sys.exit(-1)
上面这个样例程序接收来自系统的参数check_dir
,检查该目录下是否有以下文件夹或文件 src
,bin
,src/main.py
。如果这些检查都通过了,就正常返回 0,否则返回错误码 -1。
因为静态检查是一种规范组织目录与编译环境的手段,并不涉及实际的测试样例。所以助教不必担心它被学生们看到,学生甚至也可以向静态检查贡献更丰富的错误类型。助教也不必追求一开始就完美,以学生作为直接反馈源迭代改进静态检查程序即可。
这样,我们就得到了一个非常简单的测试程序,把它命名为文件check.py
,提交到刚刚建好的仓库中。
持续反馈
接下来就需要 CI 出场了,这里我们采用的是 Travis CI。首先,我们要激活 Github 仓库对应的 CI 工具。进入网站 https://travis-ci.org/,点击右上角的Sign in with Github
,可以使用 Github 账号直接登陆到 Travis CI。
登陆成功后,点击左侧My Repositories
旁边的+
,寻找要激活的 Github 仓库。
如果没有发现新建的CITest
项目,可以点击左侧的sync account
同步仓库。
找到仓库后,点击仓库链接,最后点击底部的Activate
即可激活仓库的持续集成。
Travis CI 的基本原理是:用户依照规定的语法在想要持续集成的Github 仓库创建名为.travis.yml
的配置文件。当新的Pull Request
发起时,Travis CI 自动克隆仓库,并提供一个Linux
(或其他)虚拟环境运行.travis.yml
中预设好的脚本,完成用户的预期功能。在有了check.py
简易测试文件后,我们现在需要一个.tavis.yml
配置文件来帮助它搭建运行环境。下面是一个非常简单的配置文件:
sudo: required
language: python
before_install:
- sudo apt-get install python3.5
script:
- python3 check.py $dirPath
before_install
是指在跑脚本之前安装哪些库与包,不同语言需要的运行环境不同,这里我们安装python3
来运行我们的check.py
。$dirPath
比较特殊,是指开发者新增加的文件夹的名字。这里我们假设已经获取了,由变量dirPath
指代。
注意,在发起`Pull Request`时,正确的学生仓库组织应如下所示。其中 Fork 是指继承自教师仓库的内容,+ 是指学生自己新添加的被测试项目文件夹。
|-- 20131514 (+)
|-- src
|-- main.py
|-- bin
|-- .travis.yml (Fork)
|-- check.py (Fork)
|-- README.md (Fork)
上面这只是一个简易版本,通过这个配置文件相信读者可以大概明白Travis CI工作的基本原理。接下来本文将从实际使用的CI 配置文件出发,解析其作用。首先,在实际使用时,用于测试的脚本比较复杂,会引入 Shell 中的 if
语句,所以建议将复杂的scripts一起写在一个.sh
脚本中,比如我们现在将所有希望执行的脚本都写在py-run.sh
脚本中。这个脚本的内容如下所示:
#!/bin/bash
set -v
logfile=log.txt
dir=$(ls -l | grep ^d | awk '/^d/ {print i$NF}' i=`pwd`'/')
curl -X GET "https://api.github.com/repos/$TRAVIS_REPO_SLUG/pulls/$TRAVIS_PULL_REQUEST/commits" > commit.log
cat commit.log
lastCommit=`cat commit.log | jq '.[-1].sha' -r`
shellSuccess=0
for i in $dir
do
exitCode=`python3 check.py $i $logfile`
logContent=$(cat $logfile)
if [ exitCode == 0 ]
then
buildStatus="Success"
else
buildStatus="Fail"
shellSuccess=-1
fi
if [ "$TRAVIS_PULL_REQUEST" != "false" ]
# just pull request get comments
then
curl -H "Authorization: token $GITHUB_TOKEN" -X POST -d "{\"body\":\"**Commit**:$lastCommit\n\n**Build Status**:$buildStatus\n\n**Detail**:$logContent\"}" "https://api.github.com/repos/$TRAVIS_REPO_SLUG/issues/$TRAVIS_PULL_REQUEST/comments"
fi
done
exit $shellSuccess
[ 未完待续 ... ]