第 10 章 用 Jenkins 持续集成

不知各位有没有遇到过这种事——在自己的环境里运行一切正常的程序放到开发环境和正式环境中却突然无法运行了。如果有,那么在导致无法运行的原因中,有没有遗漏提交或者忘记安装关键程序呢?

这些恼人的低级错误往往会导致我们被经理叫去面谈,出错经过被刨根问底,最后被迫“写个应对方案出来”,心情沉重好几天。麻烦的是,就算我们在发现问题的头几天能提前注意,暂时避免问题的重复发生,但是随着时间推移早晚还会再犯,结果还是得对着经理铁青的脸咽苦水。相信不少人都有过类似经历吧?

如果每次修改程序都重新构建、测试、检查测试结果,我们或许能避免上述失误。但是,人类是一种不擅长单调重复劳作的生物。所以,我们需要借助工具让这一系列工作的执行自动化、定期化。

这种自动化、定期化执行的解决方案称为持续集成(Continuous Integration,CI)。这个解决方案极大降低了各种人为失误(粗心大意、重复劳作的惰性等)带来的风险。本章将以 Django 框架的 Web 应用为例,讲解如何用 CI 工具 Jenkins 实现持续集成。Django 的相关知识请参考第 14 章。

10.1 什么是持续集成

10.1.1 持续集成的简介

开发流程中存在的风险

持续集成是一种让计算机自动地任意次重复整个开发流程(编译、测试、汇报等)的开发手法,一般简称为 CI。由于其频繁重复整个开发流程,所以能帮助开发者提早发现问题。

为方便理解持续集成,现在我们把从写代码到向执行环境发布的整个开发流程大致分为以下 3 个阶段。

① 编写源码。修改已有代码

② 提交、push

③ 进行发布

在②和③的过程中容易出现下述问题,必须严加注意。

  • 遗漏提交。忘记提交要发布的程序

  • 遗漏合并。push 到 default 的时候忘记合并,出现多头现象

  • 遗漏安装。编写的程序需要特定 Python 模块才能运行,但是忘记安装该模块了

对于这类问题,只要事先准备好测试代码,在程序发布到目标环境之前执行测试,基本都能被发现。

长期开发中容易出现的问题

我们都希望测试代码在发布之后仍能够长期继续使用。毕竟开发是一个持续的过程,一次发布之后还会有下一次发布,并不是发布一次就结束了。

在开发长期持续的过程中,我们会遇到下面这些问题。

忘记以前发布的代码

肩负程序修改任务的开发者对源码的修改十分敏感,会频繁地执行测试代码。可是,一旦修改任务结束,问题关闭,就很少有人再愿意回过头去手动执行那些已经通过的测试了。

只关心自己负责的部分

大部分时候,我们会去执行与自己负责部分相关的测试代码,但不会关心责任范围以外的模型的测试。然而,我们对程序的修改有时会“枪毙”掉某些看似毫无关系的模块的测试结果。估计各位之中有不少人有过类似经历。

可见,我们很难通过人为的注意和操作来确认整个开发流程,而且这样做也是一种时间和劳动力的浪费。如果项目代码量很大,那么执行所有测试代码必然消耗大量时间,如果是在自己的环境中执行,这根本不现实。所以这类工作最好交给计算机去做。

一天内多构建几次能很快发现问题

一天内多次定期 pull 源码并运行测试代码可以帮助我们及早发现问题。与其等到后期一次性找出一大堆问题,不如在产生问题时及早处理来得方便和放心。这一做法被我们称为持续集成。

NOTE

持续集成是XP(Extreme Programming,极限编程)的最佳实践之一。原文出自 Martin Fowler1 的论文,各位可通过下述 URL 查看2。

1国际著名的面向对象分析设计、UML、模式等方面的专家,敏捷开发方法的创始人之一,现为 ThoughtWorks 公司的首席科学家。——编者注

2中文版论文请参考如下网址:http://www.cnblogs.com/cloudteng/archive/2012/02/25/2367565.html。——编者注

Continuous Integration

http://www.martinfowler.com/articles/continuousIntegration.html

10.1.2 Jenkins 简介

Jenkins 是基于 Java 的开源 CI 工具,其安装和操作都很简单。另外,Jenkins 不仅能在面板上轻松看出 Job 成功或失败,还可以借助通知功能将结果以邮件或 RSS 订阅的形式发给用户。

除此之外,Jenkins 还允许通过插件进行功能扩展,所需功能可以随用随添加,而且还支持主从式集群,能够轻松地横向扩展。

下述流程是用 Jenkins 实现持续集成的一个例子。本章我们将根据下述流程进行学习。

  • 为 Job 的执行制定日程表

  • 签出源码

  • 通过 shell 脚本执行测试(包括检测覆盖率)并输出结果

  • 通过 shell 脚本构建文档

  • 统计测试结果和覆盖率

  • 统计 TODO

  • 发送邮件通知 Job 的结果

  • 将 Job 的结果保存至 Slack

10.2 Jenkins 的安装

我们先从 Jenkins 的安装开始学习。Jenkins 发布了面向 Windows 和 OS X 的安装包。此外,还可以通过包管理器安装 Ubuntu/Debian、Red Hat/Fedora/CentOS、FreeBSD 等。我们可以根据构建对象的环境选择相应的版本。本书将以 Ubuntu 14.04 上的安装与运行为基准向各位讲解 Jenkins。

10.2.1 安装 Jenkins 主体程序

现在来安装 Jenkins 的主体程序。首先执行下述命令添加用于 apt 的公共密钥。

  1. $ wget -q -O - http://pkg.jenkins-ci.org/debian/jenkins-ci.org.key | sudo apt-key add -

然后打开 apt 的设置文件 etcapt/sources.list,将下述语句添加到最后一行。

  1. deb http://pkg.jenkins-ci.org/debian binary/

最后执行下述命令,Jenkins 就会被安装到计算机中。

  1. $ sudo apt-get update
  2. $ sudo apt-get install jenkins

安装完成之后系统会自动生成 jenkins 用户,Jenkins 也会启动。比如启动 Jenkins 的服务器 IP 地址为 10.0.0.1,那么只要访问 http://10.0.0.1:8080 即可看到 Jenkins 的首页。varlib/jenkins 是 Jenkins 用户的主目录,该目录下保存着设置文件和工作目录。

NOTE

Gunicorn、Tomcat 等运行着 Web 容器的环境可能已经占用了 8080 端口,这种时候, Jenkins 无法在默认设置状态下提供服务。要想使用 Jenkins,必须更改 Jenkins 启动时的 HTTP 端口号,比如用文本编辑器打开 etcdefault/jenkins,将 HTTP_PORT 的值从默认的 8080 改成其他值。

10.2.2 本章将用到的 Jenkins 插件

Jenkins 可以通过插件进行功能扩展,因此广大用户为适应各种用途开发了大量插件。本章我们将用到下述 4 个插件。

① Mercurial Plugin:使用源码管理系统(SCM)Mercurial 时所需的插件

② Cobertura Plugin:生成源码覆盖率报告时所需的插件

③ Task Scanner Plugin:统计源码中的 TODO 时所需的插件

④ Slack Plugin:用来向 Slack 发送通知的插件

插件的安装可以通过“系统管理”界面的“管理插件”进行。这里为方便说明,我们来了解一下通过 CLI(Command Line Interface,命令行接口)进行安装的方法。

首先下载 Jenkins 的 CLI 工具。

  1. $ wget http://localhost:8080/jnlpJars/jenkins-cli.jar

通过 CLI 工具安装插件。

  1. $ java -jar jenkins-cli.jar -s http://localhost:8080 install-plugin tasks mercurial slack cobertura

重启 Jenkins,使刚安装的插件生效。

  1. $ sudo service jenkins restart

方便好用的 Jenkins 插件很多,除了这里介绍的几种以外,还有 JobConfigHistory Plugin、Email-ext Plugin、Timestamper Plugin,等等。

下面是这些插件的官方维基百科。

JobConfigHistory

https://wiki.jenkins-ci.org/display/JENKINS/JobConfigHistory+Plugin

Email-ext Plugin

https://wiki.jenkins-ci.org/display/JENKINS/Email-ext+plugin

Timestamper Plugin

https://wiki.jenkins-ci.org/display/JENKINS/Timestamper

10.3 执行测试代码

现在我们拿一个简单的 Python 项目的测试代码作为例子执行一下。Python 项目由版本控制系统保存,Jenkins 将从版本控制系统获取 Python 项目并执行测试。

10.3.1 让 Jenkins 运行简单的测试代码

首先,我们写一个包含 unittest 模块的简单的测试代码,让 Jenkins 来运行它(LIST 10.1、 LIST 10.2)。

LIST 10.1 foo.py

  1. # -*- coding:utf-8 -*-
  2. def divide(num1, num2):
  3. """ 对传值参数做除法的简单函数
  4. """
  5. return num1 / num2

LIST 10.2 test_foo.py

  1. # -*- coding:utf-8 -*-
  2. import unittest
  3. import foo
  4. class SimpleTest(unittest.TestCase):
  5. """ 测试做除法的函数
  6. """
  7. def test1(self):
  8. self.assertEqual(foo.divide(2, 2), 1)
  9. def test2(self):
  10. self.assertEqual(foo.divide(0, 1), 1)
  11. if __name__ == "__main__":
  12. unittest.main()

将这两个文件保存在 Mercurial 的版本库里,放在 Jenkins 服务器上的 varhg/simple_test 目录下。

10.3.2 添加 Job

接下来给 Jenkins 添加 Job。Jenkins 的 Job 可以指定多种类型的任务,比如构建项目、签出代码等。现在请打开 Jenkins 面板侧边菜单的“新建”界面,如图 10.1。

图像说明文字

图 10.1 Jenkins 的面板

在“新建”界面的“Item 名称”处输入 Job 名,选择 Job 种类。如图 10.2,这里我们输入 simple_test 作为 Job 名,种类则选择“构建一个自由风格的软件项目”,然后点击界面下部的“OK”按钮。

图像说明文字

图 10.2 新建界面

指定源码管理系统

在 Job 设置界面的“源码管理”一栏中选择 Mercurial,随后会显示 3 个输入框,我们作如下输入。

① Repository URL:输入版本库的 URL

输入 varhg/simple_test

② Branch:输入 Mercurial 的分支名。留空则使用 default

这次我们要用的是 default,因此留空

③ 源码库浏览器:指定用于查看版本库更改的工具

现阶段先用默认的“自动”

NOTE

关于版本库 pull 出错

Jenkins 的 Job 从 Mercurial 上签出源码时,经常由于忘记设置版本库权限而导致 pull 出错。

安装 Jenkins 时会自动创建 jenkins 用户,执行 Job 的都是这个用户,所以一定要记得给这个用户权限。

另外,有时即便已经有权限,在执行 hg pull 时仍会遇到“不可信的用户”“不可信的群组”这类错误。这种情况一般出现在版本库的“.hg/hgrc”所有者与执行 hg 命令的用户不一致的时候。只要在 jenkins 用户的 $HOME/.hgrc 的 trusted 节中进行设置即可解决该问题。比如“不可信的用户”为 foo、“不可信的群组”为 bar 时,则需在 hgrc 作以下设置。

  1. [trusted]
  2. user = foo
  3. group = bar

此外,trusted 节的 user、group 元素可以一次指定多个值,相邻两值之间用逗号隔开。

制定日程表

然后我们来设置另一个重要项目——构建触发器。这个项目能像 cron 一样制定日程表,比如让 Job 定期执行,或者在其他 Job 结束后执行等。这里我们选择“Build periodically”。勾选之后会出现“日程表”输入框,我们在这里设置 Job 的执行间隔,具体方法与设置 crontab 时一样。比如我们想每个小时的 0 分时执行 Job,则写成下面这样。

  1. 0

此外,在 Jenkins 的 Job 设置界面里,每一项后面都有为用户准备的帮助。各位如果遇到看不懂的项目,可以点击相应的帮助图标作参考。

设置执行测试代码的命令

最后,在“构建”选项卡处设置执行测试代码的命令。点击“增加构建步骤”之后即可选择“Execute shell”。然后在文本框中描述下述命令,如图 10.3 所示。

  1. python test_foo.py

图像说明文字

图 10.3 构建步骤“Execute shell”

只要把我们写 unittest 的测试用例时手动执行的命令写在这里即可。完成上述所有步骤之后点击“保存”按钮进行保存,接下来 Jenkins 将在我们指定的时刻执行 Job。

10.3.3 Job 的成功与失败

Jenkins 的 Job 可以从管理界面手动启动。现在我们试着点击面板上的“立刻构建”,启动刚才创建的 Job。

随后各位看到的应该是一个红色图标,表示“失败”。我们可以点击左侧栏“控制台输出”链接查看执行结果(图 10.4)。Jenkins 会以控制台日志的形式记录每次执行 Job 的过程,便于用户调查失败原因。

图像说明文字

图 10.4 通过“控制台输出”查看 Job 执行失败的内容

从日志中一眼就能看出测试结果为 FAILURE。失败的原因是测试代码有问题,所以接下来,我们要将测试用例修改成 LIST 10.3 这样,然后重新启动 Job。

LIST 10.3 test_foo.py

  1. # -*- coding:utf-8 -*-
  2. import unittest
  3. import foo
  4. class SimpleTest(unittest.TestCase):
  5. """ 测试做除法的函数
  6. """
  7. def test1(self):
  8. self.assertEqual(foo.divide(2, 2), 1)
  9. def test2(self):
  10. self.assertEqual(foo.divide(0, 1), 0)
  11. if __name__ == "__main__":
  12. unittest.main()

如图 10.5,这次看到的应该是蓝色图标,测试结果为 SUCCESS

图像说明文字

图 10.5 执行成功时的“控制台输出”

10.4 测试结果输出到报告

经过前面的安装与设置,我们已经可以用 Jenkins 创建 Job 并执行测试代码了。不过,实际开发中的测试用例要比这个多得多,而且单纯从面板上查看测试成功 / 失败并不能满足开发的需求。

Jenkins 可以读取指定格式(xUnit 格式)的 xml 文件并将测试结果以报告形式显示在屏幕上。使用 Python 的 pytest 不仅能够一次性执行多个测试用例,还能将结果以 xUnit 格式输出。所以这里我们借助 pytest 来输出测试结果的报告。

10.4.1 安装 pytest

可以用 pip 安装 Pytest。

  1. $ pip install pytest

10.4.2 调用 pytest 命令

安装完 pytest 后就可以调用 py.test 命令了。我们先移动到放有 test_foo.py 的目录下,然后执行如下命令。

  1. $ py.test --junit-xml=test_result.xml

该命令会输出一个 xUnit 格式的 test_result.xml 文件。现在只要让 Jenkins 读取它,我们就能从屏幕中看到测试结果的报告了。

10.4.3 根据 pytest 更改 Jenkins 的设置

首先给 Jenkins 设置 virtualenv,安装 pytest。

  1. $ sudo su - jenkins
  2. $ virtualenv .virtualenv/simple_test
  3. $ source .virtualenv/simple_test/bin/activate
  4. $ pip install pytest

然后来更改 Jenkins 的设置。需要修改的只有“Execute shell”中指定的命令行操作内容。原本我们是用 python 命令直接执行 unittest 的测试用例的,这里替换成调用上面提到的 py.test 命令,如图 10.6。

图像说明文字

图 10.6 根据 pytest 修改命令

接下来设置“构建后操作”选项卡,让 Jenkins 读取 py.test 命令输出的 xml 文件,如图 10.7。

图像说明文字

图 10.7 设置读取测试报告

点击“增加构建后操作步骤”按钮,选择“Publish JUnit test result report”。随后该选项卡内会自动生成一个“Publish JUnit test result report”表格项,我们在“测试报告(XML)”里指定 test_result.xml。

现在再点击“立刻构建”,则将显示如图 10.8 的报告。

图像说明文字

图 10.8 测试结果报告

10.5 显示覆盖率报告

现在我们已经能通过 Jenkins 界面查看测试用例执行结果的报告了。接下来我们看看如何用 pytest-cov 获取代码的覆盖率。

10.5.1 安装 pytest-cov

pytest-cov 同样可以用 pip 安装,具体如下。

  1. $ pip install pytest-cov

在本地开发环境中安装完毕后,别忘了在 Jenkins 的 virtualenv 环境中也安装一遍。

10.5.2 从 pytest 获取覆盖率

执行下述命令,输出 xml 格式的覆盖率报告。

  1. $ py.test --cov=foo --cov-report=xml

报告会以固定名称 coverage.xml 输出到当前目录下。如果想同时输出 xUnit 格式的文件,则要用下述命令。

  1. $ py.test --junit-xml=test_result.xml --cov=foo --cov-report=xml

然后将 Jenkins 对应 Job 的“Execute shell”设置值替换成上述命令。

10.5.3 读取覆盖率报告

最后,让 Jenkins 读取上述命令输出的 xml 文件并显示在屏幕上即可。

点击“增加构建后操作步骤”按钮,选择“Publish CoberturaCoverage Report”。随后该选项卡内会自动生成一个“Publish Cobertura Coverage Report”表格项。现在点击“高级”按钮,按照下述方法在图 10.9 中进行设置。

  • Cobertura xml report pattern :coverage.xml

  • Source Encoding :UTF-8

图像说明文字

图 10.9 设置读取覆盖率报告

设定完成之后执行 Job,接下来各位应该能在屏幕上看到如图 10.10 所示的覆盖率报告了。

图像说明文字

图 10.10 覆盖率报告

测试覆盖到的源码行显示为绿色,反之显示为粉红色。

图像说明文字

图 10.11 查看各个文件的覆盖率

如图 10.11,这样一来我们就能掌握执行测试代码之后的覆盖率了。接下来我们将学习 Django 测试代码的执行方法。

10.6 执行 Django 的测试

前面我们学习了涉及到 unittest 和 pytest 的测试代码的执行方法,这里我们学习一下如何执行 Django 的测试。Django 有着专用的测试机制,各位在采用 Django 时可以拿本节内容作为参考。在学习如何执行测试的同时,我们还会学习如何向邮箱、Slack 等发送通知。

10.6.1 安装 Python 模块

这里要安装 Django 主体程序以及 Django 与 Jenkins 进行联动时所需的几个工具。

Django 主体程序

首先安装 Django 主体程序。

  1. $ pip install django

unittest-xml-reporting

我们知道,要想让 Jenkins 显示测试结果报告,需要将测试执行结果以 Junit 格式的 xml 文件形式输出。

使用 Django 的 manage.py test 命令执行测试后,生成的结果只是纯文本,并不满足上述格式要求。用 unittest-xml-reporting 可以解决这一问题,其安装如下。

  1. $ pip install unittest-xml-reporting

coverage

还要安装 coverage 模块。

  1. $ pip install coverage

10.6.2 Django 的调整

创建项目目录

创建项目目录。这里我们用 cafe/apps 作为其名称。

  1. $ mkdir cafe
  2. $ cd cafe
  3. $ django-admin.py startproject apps

这个项目由 Mercurial 的版本库管理,保存在 varhg/cafe 目录下。

创建 Django 应用

接下来创建 Django 应用。运行上面那条命令之后,我们会得到 apps 目录。现在移动到该目录下,创建一个名为 menu 的应用。

  1. $ cd apps
  2. $ python manage.py startapp menu

执行完毕后将生成 apps/menu 目录。

10.6.3 示例代码

编写模型

编写模型。我们这里所用的概念是一间咖啡厅的茶品清单,代码如 LIST 10.4 所示。

LIST 10.4 cafe/apps/menu/models.py

  1. # -*- coding:utf-8 -*-
  2. from django.db import models
  3. TEA_KINDS = (
  4. ("english", u" 英式红茶"),
  5. ("chinese", u" 中国茶"),
  6. ("japanese", u" 日本茶"),
  7. )
  8. class TeaManager(models.Manager):
  9. def recommended(self):
  10. """ 仅显示推荐商品 """
  11. return self.filter(is_flavor=True)
  12. def count_each_kind(self):
  13. """ 以字典形式返回各类茶的件数 """
  14. result = self.values_list("kind").annotate(
  15. count=models.Count("kind"))
  16. return dict(result)
  17. class Tea(models.Model):
  18. objects = TeaManager()
  19. name = models.CharField(u" 名称", max_length=255)
  20. kind = models.CharField(u" 种类", max_length=255, choices=TEA_KINDS)
  21. price = models.IntegerField(u" 价格")
  22. is_recommended = models.BooleanField(
  23. u" 推荐商品", default=False)

编写表单

表单代码如 LIST 10.5 所示。

LIST 10.5 cafe/apps/menu/forms.py

  1. # -*- coding:utf-8 -*-
  2. from django import forms
  3. from menu.models import Tea, TEA_KINDS
  4. class TeaSearchForm(forms.Form):
  5. name = forms.CharField(label=u"", max_length=255, required=False)
  6. kind = forms.MultipleChoiceField(
  7. label=u" 种类", choices=TEA_KINDS, required=False)
  8. extra_report = forms.BooleanField(
  9. label=u" 输出追加报告", required=False)False)
  10. def clean(self):
  11. clnd = self.cleaned_data
  12. if not self.is_valid():
  13. return clnd
  14. if not clnd["name"] and not clnd["kind"]:
  15. raise forms.ValidationError(
  16. u" 名称和种类请至少输入一项")
  17. return clnd

向 settings.INSTALLED_APPS 添加 menu

创建完 menu 应用后记得在 settings.py 的 INSTALLED_APPS 里指定它(LIST 10.6)。缺少这一步是不能用 manage.py 执行测试的。

LIST 10.6 cafe/apps/settings.py

  1. INSTALLED_APPS = (
  2. 'django.contrib.admin',
  3. 'django.contrib.auth',
  4. 'django.contrib.contenttypes',
  5. 'django.contrib.sessions',
  6. 'django.contrib.messages',
  7. 'django.contrib.staticfiles',
  8. 'menu',
  9. )

为 Jenkins 写 settings

我们希望 Jenkins 利用 unittest-xml-reporting 将测试结果以 xml 文件的形式输出,所以这里要专门为 Jenkins 写一个 settings。

代码如 LIST 10.7 所示。

LIST 10.7 cafe/apps/settings_jenkins.py

  1. # -*- coding:utf-8 -*-
  2. from settings import *
  3. ######################
  4. # Xml test runner
  5. ######################
  6. TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner'
  7. TEST_OUTPUT_DIR = 'test_reports'
  8. DATABASES = {
  9. 'default': {
  10. 'ENGINE': 'django.db.backends.sqlite3',
  11. 'NAME': 'jenkins.db',
  12. 'USER': '',
  13. 'PASSWORD': '',
  14. 'HOST': '',
  15. 'PORT': '',
  16. }
  17. }

生成迁移文件

前面我们用示例代码创建了模型,所以这里还需要生成迁移文件。移动到 manage.py 文件所在的目录下执行下述命令,即可完成迁移文件的生成。

  1. $ python manage.py makemigrations

编写测试代码

模型和表单的代码都已经写好,接下来该编写测试代码了。测试代码如 LIST 10.8 所示。

LIST 10.8 cafe/apps/menu/tests.py

  1. # -*- coding:utf-8 -*-
  2. import unittest
  3. from django.test import TestCase as DjangoTest
  4. from menu.models import Tea
  5. from menu.forms import TeaSearchForm
  6. row = lambda L: (dict(zip(L[0], x)) for x in L[1:])
  7. class TeaManagerTest(DjangoTest):
  8. def setUp(self):
  9. datas = (
  10. ("name", "kind"),
  11. (u" 大吉岭", "english"),
  12. (u" 锡兰红茶", "english"),
  13. (u" 乌龙茶", "chinese"),
  14. (u" 铁观音", "chinese"),
  15. (u" 普洱茶", "chinese"),
  16. (u" 静冈茶", "japanese"),
  17. )
  18. for data in row(datas):
  19. Tea.objects.create(price=100, **data)
  20. def test_count_each_kind(self):
  21. result = Tea.objects.count_each_kind()
  22. self.assertEqual(result, dict(english=2, chinese=3, japanese=1))
  23. class TeaSearchFormTest(unittest.TestCase):
  24. def test_valid(self):
  25. """ 检查输入正常时是否会报错 """
  26. params = dict(name="foo", kind=["english"])
  27. form = TeaSearchForm(params)
  28. self.assertEqual(form.is_valid(), True, form.errors.as_text())
  29. def test_either1(self):
  30. """ 检查名称和种类都无输入时是否会报错 """
  31. params = dict()
  32. form = TeaSearchForm(params)
  33. self.assertEqual(form.is_valid(), False, form.errors.as_text())
  34. def test_either2(self):
  35. """ 检查输入名称后是否会报错 """
  36. params = dict(name="foo")
  37. form = TeaSearchForm(params)
  38. self.assertEqual(form.is_valid(), True, form.errors.as_text())
  39. def test_either3(self):
  40. """ 检查输入种类后是否会报错 """
  41. params = dict(kind=["english", "chinese"])
  42. form = TeaSearchForm(params)
  43. self.assertEqual(form.is_valid(), True, form.errors.as_text())

10.6.4 Jenkins 的调整

新建 Job

点击 Jenkins 面板侧边栏的“新建”,创建 Job。

① 选择“构建一个自由风格的软件项目”

② 在“Item 名称”处输入 cafe(Job 名并没有硬性要求,这里只是特意选择了与项目名称相同的 cafe)

“源码管理系统”的设置

这里使用 Mercurial。我们事先已经将 cafe 的版本库放在了 varhg/cafe 下,所以直接在 Jenkins 的设置界面作如下输入即可。

  • 源码管理系统:Mercurial

  • Repository URL :varhg/cafe

  • Branch :default

  • 源码库浏览器:Auto

在 Execute shell 中填写测试命令

把 Django 测试结束后输出覆盖率报告的命令写在“Execute shell”中。如果只是单纯地进行 Django 测试,可以用下述命令执行。

  1. $ PYTHONPATH=apps python -m manage test menu --settings=settings_jenkins

通过 coverage 命令执行上述代码即可获取覆盖率。接下来,我们将上述代码改成下面这样。

  1. $ PYTHONPATH=apps coverage run --source=apps -m manage test menu --settings=s
  2. ettings_jenkins && coverage xml -o coverage_reports/coverage.xml

然后,run 成功之后,执行 coverage xml 命令即可输出 xml 格式的报告。

这里我们指定了 —source=apps 选项。该选项可以将覆盖率统计对象限制在指定的目录下。在遇到这种 Django 项目目录与版本库路径不一致的情况时,指定该选项能有效防止读取无关代码。

专栏 不要直接在Django 的项目目录下执行测试

获取覆盖率时有一点需要注意,那就是不能直接在 Django 的项目目录下执行测试。使用 Django 时有很多工作是在 Django 的项目目录下进行(比如上面例子中移动到 apps 目录下并执行 manage.py test)。如果用上述方法统计覆盖率,那么一旦我们移动到 apps 并执行 manage.py test,会连同 Django 本身一同测试并统计覆盖率,最终的报告会变成图 10.12 这个样子。因此,为了防止非测试对象混入覆盖率报告,必须对这一点强加注意。

{%}

图 10.12 混入多余代码之后的覆盖率

10.6.5 “构建后操作”选项卡的设置

“构建后操作”选项卡里设置的任务将在 Job 的构建处理执行完毕后执行。

Jenkins 读取测试结果报告与覆盖率报告的操作需要在这里进行设置。另外,执行 Job 失败的邮件通知也是在这里设置的。

读取测试结果报告

“ Publish JUnit test result report”中有“测试报告( XML)”一项,用于指定输出的 xml 文件。

这里允许使用通配符。unittest-xml-reporting 会针对每一个测试类输出一个 xml 文件,所以按照下述方法设置即可。另外,测试结果的输出目录要与 Jenkins 的 settings 中指定的目录一致。

  • 测试报告(XML)

cafe/test_reports/*.xml

读取覆盖率报告

“Publish CoberturaCoverage Report”里有“Cobertura xml report pattern”,我们要在这里指定 coverage xml 命令输出的文件。

  • Cobertura xml report pattern

cafe/coverage_reports/*.xml

查看结果

上述设置完成之后,我们就可以看到测试结果报告和覆盖率报告了,如图 10.13 和图 10.14。

图像说明文字

图 10.13 测试结果报告

图像说明文字

图 10.14 覆盖率报告

NOTE

输出代码覆盖率报告时可能会出现乱码,这是 Java 字体设置导致的。解决中文乱码的方法如下。

  • Linux

    配置环境变量 export JAVA_TOOL_OPTIONS="-Dfile.encoding=UTF-8"

  • Windows

    新建变量 JAVA_TOOL_OPTIONS ,value 值为-Dfile.encoding=UTF-8

重启 Jenkins 之后再执行一遍刚刚出现乱码的Job,可以看到乱码问题已经被解决。

邮件通知

“E-mail Notification”中有“Recipients”,我们要在这里指定通知接收方的邮箱地址,如图 10.15 所示。

图像说明文字

图 10.15 设置收件人

除此之外,还要在侧边栏“系统管理”→“系统设置”里找出“Jenkins Location”,在“系统管理员邮件地址”中填写 Jenkins 用来发送邮件的邮箱地址,然后在“E-mail Notification”下的“SMTP 服务器”中设置 SMPT 服务器(图 10.16、图 10.17)。另外,如果要使用 Gmail 等需要认证的邮箱账户,需要点击“E-mail Notification”中的“高级”按钮,指定“用户名”“密码”等信息。

图像说明文字

图 10.16 设置发件人的邮箱地址

图像说明文字

图 10.17 设置发件人的邮箱服务器

在上述设置完成之后,Jenkins 会在 Job 执行失败时发送邮件,以通知开发者。

NOTE

安装插件后还可以向即将讲到的 Slack、IRC、Jabber、Growl 等发送通知。另外,安装 Email-ext plugin 之后还可以对邮件的主题、正文、通知设置等进行自定义。

向 Slack 发送通知

Slack 是一款可以跨平台使用的团队交流工具,还可以与许多第三方服务集成。Jenkins 可以借助 Slack Plugin 向 Slack 发送通知。Slack 的相关说明请参考 4.3 节。

首先进行 Slack 的设置。找出需要通知的频道或组,选择“Add a service integration”,添加“Jenkins CI”。接下来会跳转到“Post to Channel”界面,确认要通知的频道或组无误后,点击“Add Jenkins CI Integration”按钮,跳转至自定义界面。这里要先将“Step 3”中写的“TeamDomain”和“Integration Token”复制下来,因为后面设置 Jenkins 时会用到。Slack 发送通知时所用的名字和头像可以在这个界面修改。最后点击“Save Setting”完成 Slack 的设置。

接下来设置 Jenkins。在 Jenkins 的“系统管理”→“系统设置”中找到“Global Slack Notifier Settings”,然后作如下更改(图 10.18)。

① TeamDomain:设置 Slack 时复制的 TeamDomain

② Integration Token:设置 Slack 时复制的 Integration Token

③ Channel:Slack 的频道名或组名

④ Build Server URL:Jenkins 服务器的 URL

图像说明文字

图 10.18 Global Slack Notifier Settings 示例

接着在 Job 的设置中找到“Slack Notifications”,勾选要通知的项目,最后在“构建后操作”处选择添加 Slack Notifications”即可(图 10.19)。

图像说明文字

图 10.19 通知项目示例

现在再执行 Job 就会向 Slack 发送通知了(图 10.20)。

{%}

图 10.20 向 Slack 发送通知

10.7 通过 Jenkins 构建文档

我们在第 7 章中介绍了如何用 Sphinx 写文档,现在来看看如何用 Jenkins 来辅助 Sphinx 编写文档。

用 Sphinx 编写文档时经常会遇到下述问题。

  • reST 语法错误

  • 忘记提交文档中引用的图片或源码

如果存在上述问题,Sphinx 会在执行构建命令时发出警告。因此,只要我们利用 Jenkins 定期监视文档的构建,就能尽早发现这些问题。

本书就是借助 Sphinx 编写的。接下来我们将以本书的编写过程为例,学习一下我们应该怎样用 Jenkins 管理多人共同执笔的书稿。该方法不仅适用于多人共同管理文档的情况,对单独管理文档的情况也有一定的帮助。

10.7.1 安装 Sphinx

首先将 Sphinx 安装到本地环境中。

  1. $ pip install sphinx

10.7.2 在 Jenkins 添加 Job

该 Job 的设置如下(varhg/bpbook 为源码版本库)。

① 基本设置

  • Job 类型:自由风格

  • Job 名称:bpbook

② 源码管理系统

  • SCM:Mercurial

  • 版本库:varhg/bpbook

  • 分支:default

③ 构建触发器

  • 种类:定期执行

  • 日程表:H/15**(每小时的 15 分)

10.7.3 Sphinx 构建发出警告时令 Job 失败

Sphinx 的 -w 选项可以将警告内容以文件形式输出。我们希望出现警告时让 Jenkins 的 Job 失败,所以要在“Execute shell”选项卡处进行如下设置:当输出警告内容的文件不为空时,用返回码 9 结束执行。

  1. sphinx-build -a -E -w build_warnings.txt source docs
  2. if [ -s build_warnings.txt ] ; then
  3. exit 9
  4. fi

这里的返回码可以用 0 之外的任意数字。Jenkins 将所有构建完毕时返回码不为 0 的 Job 均视为失败。另外,为保证每次都重新构建所有文档,要加上 -a -E 选项。

10.7.4 查看成果

Jenkins 可以在管理界面查看工作区内的文件,因此我们可以在这里查看 Job 构建的文档(图 10.21)。

{%}

图 10.21 查看工作区

Jenkins 中的各 Job 首页的“描述”中允许添加 html 标签,我们可以将已构建的文档的链接插入“描述”中,这样一来就能直接从首页点击链接查看构建后的文档了(图 10.22)。

图像说明文字

图 10.22 编辑 Job 首页的“描述”

10.7.5 通过 Task Scanner Plugin 管理 TODO

编写代码和稿件时,我们常需要将一些临时想到的事或暂时不需要做的事以 TODO 注释的形式记录下来。用 Jenkins 的 Task Scanner Plugin 插件可以让我们在报告中看到 TODO 注释(图 10.23)。

图像说明文字

图 10.23 Task Scanner Plugin 报告

如图 10.24 所示,报告中还对对应行做了高亮处理。

图像说明文字

图 10.24 高亮显示

另外,Sphinx 扩展中有一个 todo 扩展,激活之后即可允许用户使用 todo 指令(图 10.25)。todo 指令的布局与 note 和 warning 相同。

{%}

图 10.25 使用 todo 指令的示例

与其他人合著一本书时,我们常会怀疑自己写的内容与其他章节是否存在矛盾,或是自己写的稿子是否满足与合著者之间定下的规则。如果每出现一次这种情况就停下手去检查一遍,势必拖累稿子的进度,所以我们选择先留下 TODO 注释,等待最后统一解决。

另外,TODO 注释经常写了不删,让人搞不清到底问题有没有解决。这种情况不仅存在于写稿的过程中,在敲代码时也同样会发生。虽然每次处理完源码或稿件都手动 grep 搜索 TODO 注释能避免这种情况发生,但实在太麻烦了。

因此,在用 Jenkins 执行测试或构建文档时,可以顺便生成一个 TODO 的列表。另外,Task Scanner Plugin 能够用图表显示 TODO 注释的数量。随着 TODO 被一个个解决,我们能够直观地掌握其减少的过程。看到 TODO 减少能够激发开发者解决 TODO 的积极性,在某种意义上起到提高效率的作用。

10.7.6 Task Scanner Plugin 的设置示例

首先从“管理插件”安装 Task Scanner Plugin。完成后,Job 设置界面的“构建后操作”选项卡处会增加“Scan workspace for open tasks”一项。勾选该项,输入下述几项。

  • Files to scan:TODO 的搜索对象

  • Files to exclude:这里设置的对象将不包含在搜索范围内

  • Tasks tags:这里输入的字符串将被视为 TODO,计入统计结果

管理本书稿的 Job 就是按照图 10.26 设置的。

图像说明文字

图 10.26 Task Scanner Plugin 设置示例

10.8 Jenkins 进阶技巧

10.8.1 好用的功能

这里简单了解一下前面没有讲到的 Jenkins 的好用功能。

Job 触发器

Job 触发器是指通过 Jenkins 的某个 Job 来启动其他 Job 的功能。它没有专门的设置界面,必须到“构建触发器”的“Build after other projects are built”或“构建后操作”的“Build other projects”中进行设置。

这里可以设置前一个 Job 失败时不执行后续 Job,因此能够构成“测试不通过则不进行部署”的工作流程。

用户管理

用户管理可以设置 Jenkins 的认证与认可。设置可以满足很多细节,比如只有公司内部人员可通过外部网络访问 Jenkins,或者只有管理员能够执行 Job,等等。

另外,用户管理后端可以使用 LDAP,因此可以将用户管理交给 OpenLDAP 或 Active Directory。使用 Active Directory 时建议安装 Active Directory plugin,它能够让我们轻轻松松地进行设置。

NOTE

我们可以在下列 URL 上找到关于 Jenkins 用户管理的详细介绍,有兴趣的读者不妨参考一下。

Standard Security Setup

https://wiki.jenkins-ci.org/display/JENKINS/Standard+Security+Setup

Active Directory plugin

https://wiki.jenkins-ci.org/display/JENKINS/Active+Directory+plugin

集群

Jenkins 允许以主从方式组建集群。我们在使用 Jenkins 时,往往一建就是十几二十个定期执行的 Job。另外,Job 的执行时间可能会被测试拖长(测试过慢),导致 1 台计算机无法处理所有 Job。这种时候组件集群就显得行之有效了。此外,对于需要在多个不同环境中进行的测试而言,集群也是一种很好的解决方法。

简单来说,Jenkins 集群的机制如下所述。

  • 主计算机向从计算机发送指令

  • 从计算机执行指令

  • 主计算机统计从计算机的结果

NOTE

我们可以在下列URL 上找到Jenkins 集群的详细介绍,有兴趣的读者不妨参考一下。

Distributed builds

https://wiki.jenkins-ci.org/display/JA/Distributed+builds

CLI

我们在 10.2.2 节了解过,除了 Web UI 以外,Jenkins 还有其他 CLI。Jenkins 的 CLI 很适合用来执行插件定期升级等标准化处理。另外,Jenkins 的 CLI 允许远程执行,便于用户在不能使用 Web 浏览器的终端上操作 Jenkins。

NOTE

我们可以在下列 URL 上找到 Jenkins CLI 的详细介绍,有兴趣的读者不妨参考一下。

Jenkins CLI

https://wiki.jenkins-ci.org/display/JA/Jenkins+CLI

10.8.2 进一步改善

到 10.7 节为止,我们学习了如何用 Jenkins 签出源码、执行测试(测量覆盖率)、统计 TODO 任务、发送结果通知和编写文档等。但是,这些内容只是整个开发流程的一部分。相信各位还希望能用 Jenkins 做更多的事,比如下面这些。

  • Pylint 等的静态解析

  • 向正式环境或演示环境部署

  • 测试已部署的产品

  • 向 PyPI 服务器上传

  • 所交付产品的 Zip 文件存档

另外,只要 Jenkins 的运用步入正轨,我们将会在改善业务流程、根据业务流程自定义 Jenkins 等方面产生需求。

遇到此类需求时可以参考下述信息。

NOTE

官方维基百科的信息

邮件列表

书籍

  • 《Jenkins 入门与实践》3(技术评论社,2011 年)

  • Jenkins: The Definitive Guide 4(O'Reilly Media,2011 年)

  • 《持续集成:软件质量改进和风险降低之道》(机械工业出版社,2008 年)

  • 《Jenkins 入门》5(Shuwa System,2012 年)

3原书名为『Jenkins 実践入門』,暂无中文版。——译者注

4本书作者为 John Ferguson Smart,暂无中文版。——译者注

5原书名为『入門 Jenkins』,暂无中文版。——译者注

虽然用 Jenkins 能完成许多精细的工作,但这不代表自动化优于一切。使用 Jenkins 实现自动化时,务必要保证作业成本和便捷性的平衡。

10.9 小结

本章对持续集成进行了简单的说明,同时讲解了 Jenkins 的用法。读完本章之后,相信各位已经发现持续集成和 Jenkins 并没有想象中那么难。虽然本章主要是以 Django 为例进行讲解的,但 Jenkins 的用武之地远不止于此。各位可以从小规模项目入手,逐步感受导入 Jenkins 的方便之处。