Appearance

基于Docker+Jenkins部署实现接口自动化持续集成

geekbing2022-10-30自动化测试接口自动化 docker jenkins

前言

Dockeropen in new window是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的LinuxMacWindows机器上,也可以实现虚拟化。容器是完全使用沙箱机制,互相之间不会有任何接口。

10 分钟快速掌握 Docker 必备基础知识open in new window

Docker 入门教程open in new window

Jenkinsopen in new window是一款开源 CI&CDopen in new window 软件,用于自动化各种任务,包括构建、测试和部署软件,支持各种运行方式,可通过系统包、Docker 或者通过一个独立的 Java 程序。

Jenkins 用户手册open in new window

环境搭建

前置条件

安装DockerLinux 安装open in new windowmacOS 安装open in new windowWindows 安装open in new window(比较麻烦,不建议)

正式开始

Docker部署Jenkins(2.346.2)CentOS8下。非root账号命令行前需添加sudo

  • 创建网络

    sudo docker network create jenkins
    
  • 下载并运行 docker:dind 镜像

    sudo docker run \
    --name jenkins-docker \
    --rm \
    --detach \
    --privileged \
    --network jenkins \
    --network-alias docker \
    --env DOCKER_TLS_CERTDIR=/certs \
    --volume jenkins-docker-certs:/certs/client \
    --volume jenkins-data:/var/jenkins_home \
    --publish 2376:2376 \
    docker:dind \
    --storage-driver overlav2
    
  • 创建 docker 目录,下面创建一个 Dockerfile 文件,输入如下内容保存

    FROM jenkins/jenkins:2.332.3-jdk11
    USER root
    Run sed -i 's/deb.debian.org/mirrors.tencent.com/' /etc/apt/sources.list
    RUN apt-get update && apt-get install -y lsb-release
    RUN curl -fsSLo /usr/share/keyrings/docker-archive-keyring.asc \
    https://download.docker.com/linux/debian/gpg
    RUN echo "deb [arch=$(dpkg --print-architecture) \
    signed-by=/usr/share/keyrings/docker-archive-keyring.asc] \
    https://download.docker.com/linux/debian \
    $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list
    RUN apt-get update && apt-get install -y docker-ce-cli
    
  • 进入 docker 目录,执行下述命令,根据 Dockerfile 创建一个自己的 jenkins 镜像

    sudo docker build -t myjenkins:latest .
    
  • 启动自己的 jenkins 镜像

    sudo docker run --name myjenkins --restart=on-failure --detach \
    --network jenkins --env DOCKER_HOST=tcp://docker:2376 \
    --env DOCKER_CERT_PATH=/certs/client --env DOCKER_TLS_VERIFY=1 \
    --volume jenkins-data:/var/jenkins_home \
    --volume jenkins-docker-certs:/certs/client:ro \
    --publish 8080:8080 --publish 50000:50000 myjenkins:latest
    
  • 运行后查看容器执行日志

    sudo docker logs -f myjenkins
    
  • 复制日志中Please use the following password to proceed to installation下面的密码,保存起来

    image-20220723122337555

  • 在浏览器访问http://ip:8080ip 替换成你服务器的 ip,进入初始安装页面,输入上面复制的密码,点击继续

    image-20220723122847348

  • 选择安装推荐的插件

    image-20220723123354692

  • 安装插件需要耐心等待,比较耗时,插件安装完后进入创建账号页面

    image-20220723123533534

    image-20220723124409245

    image-20220723124636999

  • 然后点击开始使用 jenkins,重启 jenkins,命令如下

    sudo docker restart myjenkins
    
  • 浏览器访问http://ip:8080ip 替换成你服务器的 ip,本地安装ip输入127.0.0.1localhost

    image-20220723125232496

安装需要的其他插件

  • 点菜单系统管理-->插件管理-->可选插件,依次搜索allurebuild user vars Build Timestamp插件安装

    image-20220723125937914

    image-20220723234127768

    image-20220723234340004

  • 安装完成后,重启jenkins

    sudo docker restart myjenkins
    

全局工具配置

  • 点击菜单系统管理-->全局工具配置

    image-20220723140728816

    image-20220723140953300

邮件配置

  • 下面以网易企业邮箱为例,网易企业邮箱默认支持SMTPSMTP 协议介绍open in new window

  • 系统管理-->Manage Credentials-->添加凭证,邮箱凭据配置

    image-20220723142000350

    image-20220723143421270

  • jenkins邮箱基础配置,进入系统管理-->系统配置,做如下配置

    增加系统管理员邮件地址 格式:发件人昵称<邮件地址>

    image-20220723144052349

    邮件配置测试网易企业邮箱 SMTP 地址查询open in new window

    image-20220723145246726

    image-20220723145558316

    点击测试后提示成功,收件人邮箱会收到一封邮件,说明邮件发送没问题

    配置扩展邮箱

    该配置将会作为我们 jenkins 任务执行完成后的邮件模板

    找到Extended E-mail Notification区域,做如下配置,点击高级-->Credentials选择前面添加的全局邮件凭证

    image-20220723150313615

    image-20220723150135980

    image-20220723150516967

git凭据配置

  • 进入系统管理-->Manage Credentials,填写GitLab/GitHub/码云的用户名和密码

    image-20220723151118548

企业微信添加群机器人

  • 在企业微信群中添加机器人

    选择你要通知的企业微信群,按照下图,选择添加群机器人,然后按照向导程序一步一步地操作,在这期间只需要你给机器人起个名字。至此你已经成功的创建了群机器人,在群对话窗口右下方,你可以看到刚刚创建的机器人。

    image-20220723151118999

    右键查看机器人资料,你就可以看到该机器人的Webhook 地址,接下来流水线脚本会用到

Jenkins节点配置

  • 因为我们要执行的是接口自动化,并且我们希望在jenkins slave节点上执行,而不是在 jenkins 所在的 master 上执行,因此我们需要配置节点相关的内容

    上图中 master 是指 jenkins 所在的服务器,用来统筹管理各个任务及配置

    slave指的是各个自动化任务执行的机器,也叫作节点

    master通过管理节点,及任务中的节点配置将不同的任务分配到不同的设备上执行

  • 系统管理->全局安全配置,找到代理进行如下设置并保存

    image-20220723164529175

  • 系统管理->节点管理,点击左侧的新建节点

    image-20220723165007732

    image-20220723165243306

    image-20220723165423830

    节点创建后初始是未连接状态

  • docker 部署 slave 节点及连接

    我们使用节点是用来完成接口自动化项目的执行,先创建一个目录叫 dockerpython,在其中创建一个 Dockerfile 文件,内容如下

    FROM jenkinsci/jnlp-slave
    USER root
    WORKDIR /home/jenkins
    Run sed -i 's/deb.debian.org/mirrors.tencent.com/' /etc/apt/sources.list
    RUN apt-get update && apt-get install -y python3 && apt-get install -y python3-pip
    
  • 然后在dockerpython目录下执行下述命令,来创建镜像

    sudo docker build -t autotest1:latest .
    
  • 系统管理->节点管理,查看slave1节点

    http://ip:8080 指的是jenkins master的连接地址

    522cb5d9dfdd8cxxxxxxxxxxx8d259fbe5这是在节点管理中未连接的节点,可以看到下面这一串,复制过来,下面将要使用

    image-20220723172218640

    使用下述命令启动节点,ip 替换成你服务器的 ip,slave1 前面一串为上面复制

    sudo docker run -itd --network jenkins --name slave1 autotest1 -url \
    http://ip:8080 \
    522cb5d9dfdd8cxxxxxxxxxxx8d259fbe5 slave1
    

    启动后输入如下命令查看日志,日志最后出现Connected字段,说明连接成功

    sudo docker logs -f slave1
    

pipeline流水线任务

  • 点击 jenkins 首页新建任务,创建流水线任务

    image-20220723180343515

  • 流水线脚本生成

    jenkins pipeline 脚本入门open in new window

    实际上我们不用手动去编写这样的脚本,可以利用 jenkins 提供的生成功能来做

    打开任务后,点击左侧的流水线语法

    image-20220723181408439

  • 生成节点脚本

    image-20220723181721687

  • 生成接口自动化项目代码拉取脚本

    image-20220723182244427

  • 生成 allure 报告处理脚本

    image-20220723182443242

  • 生成邮件发送脚本

  • 企业微信群消息发送脚本,直接用Shell脚本发送,Shell脚本会集成到流水线脚本中,暂时不用关注

    构建成功

    curl --location --request POST 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=1e352b24-8675-4d47-9652-xxxxxxxxxxxx' \
    --header 'Content-Type: application/json' \
    --data '{
    "msgtype": "markdown",
    "markdown": {
      "content": "<font color=\'warning\'>**Jenkins任务通知**</font> \n
      >构建人: <font color=\'comment\'>${env.BUILD_USER}</font>
      >构建时间: <font color=\'comment\'>${BUILD_TIMESTAMP}</font>
      >任务名称: <font color=\'comment\'>${JOB_BASE_NAME}</font>
      >构建日志: <font color=\'comment\'>[点击查看](${BUILD_URL}/console)</font>
      >测试报告: <font color=\'comment\'>[点击查看](${BUILD_URL}/allure)</font>
      >构建状态: <font color=\'info\'>**Success**</font>
      >用例总数为: <font color=\'comment\'>**${TOTAL}**</font>
      >通过用例数: <font color=\'info\'>**${PASSED}**</font>
      >失败用例数: <font color=\'warning\'>**${FAILED}**</font>
      >错误用例数: <font color=\'comment\'>${ERROR}</font>
      >跳过用例数: <font color=\'comment\'>${SKIPPED}</font>
      >通过率为: <font color=\'comment\'>**${SUCCESS_RATE}**</font>
      >用例执行时长: <font color=\'comment\'>${TOTAL_TIMES}</font>"
      }
    }'
    

    构建失败

    curl --location --request POST 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=1e352b24-8675-4d47-9652-xxxxxxxxxxxx' \
    --header 'Content-Type: application/json' \
    --data '{
      "msgtype": "markdown",
      "markdown": {
      "content": "<font color=\'warning\'>**Jenkins任务通知**</font> \n
      >构建人: <font color=\'comment\'>${env.BUILD_USER}</font>
      >构建时间: <font color=\'comment\'>${BUILD_TIMESTAMP}</font>
      >运行时长: <font color=\'comment\'>${currentBuild.durationString}</font>
      >任务名称: <font color=\'comment\'>${JOB_BASE_NAME}</font>
      >构建日志: <font color=\'comment\'>[点击查看](${BUILD_URL}/console)</font>
      >构建状态: <font color=\'comment\'>**Failure**</font>"
      }
    }'
    
  • 最终得pipeline脚本如下

    pipeline {
        agent {
            label 'slave1'
        }
    
        stages {
            stage('拉取自动化项目代码') {
                steps {
                    git credentialsId: 'c85edf96-60de-4259-abdf-xxxxxxxxxxxx', url: '代码仓库地址'
                }
            }
            stage('执行自动化测试') {
                steps {
                    // sh 'python3 -m pip install pipenv' 首次构建时执行,或者去docker容器手动执行安装pipenv
                    sh 'pipenv install'
                    sh 'pipenv run python main.py'
                }
            }
            stage('生成测试报告') {
                steps {
                    allure includeProperties: false, jdk: '', results: [[path: 'allure-results']]
                }
            }
            stage('读取用例执行结果文件, 设置环境变量') {
                steps {
                    script {
                        def data = readFile(file: 'result.txt')
    										// 按行读取
                        def lines = data.readLines()
                        def result_list = []
                        for (line in lines) {
                            String[] str;
                            str = line.split('=');
                            result_list.add(str)
                        }
                      	// 设置环境变量
                        env.TOTAL = result_list[0][1]
                        env.PASSED = result_list[1][1]
                        env.FAILED = result_list[2][1]
                        env.ERROR = result_list[3][1]
                        env.SKIPPED = result_list[4][1]
                        env.SUCCESS_RATE = result_list[5][1]
                        env.TOTAL_TIMES = result_list[6][1]
                    }
                }
            }
            stage('企业微信通知') {
                steps {
                    script {
                        wrap([$class: 'BuildUser']){
                            echo "full name is $BUILD_USER"
                            echo "user id is $BUILD_USER_ID"
                            echo "user email is $BUILD_USER_EMAIL"
                            echo "TOTAL is $TOTAL"
                            echo "PASSED is $PASSED"
                            echo "FAILED is $FAILED"
                            echo "ERROR is $ERROR"
                            echo "SKIPPED is $SKIPPED"
                            echo "SUCCESS_RATE is $SUCCESS_RATE"
                            echo "TOTAL_TIMES is $TOTAL_TIMES"
                            env.BUILD_USER = "${BUILD_USER}"
                        }
                    }
                }
            }
            stage('发送邮件') {
                steps {
                    emailext body: '''<!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>${ENV, var="JOB_NAME"}-第${BUILD_NUMBER}次构建日志</title>
    </head>
    
    <body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4"
        offset="0">
        <table width="95%" cellpadding="0" cellspacing="0"  style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">
            <tr>
                本邮件由系统自动发出,无需回复!<br/>
                各位同事,大家好,以下为${PROJECT_NAME}项目构建信息</br>
                <td><font color="#CC0000">构建结果 - ${BUILD_STATUS}</font></td>
            </tr>
            <tr>
                <td><br />
                <b><font color="#0B610B">构建信息</font></b>
                <hr size="2" width="100%" align="center" /></td>
            </tr>
            <tr>
                <td>
                    <ul>
                        <li>项目名称: ${PROJECT_NAME}</li>
                        <li>构建编号: 第${BUILD_NUMBER}次构建</li>
                        <li>触发原因: ${CAUSE}</li>
                        <li>任务地址: <a href="${BUILD_URL}">${BUILD_URL}</a></li>
                        <li>构建日志: <a href="${BUILD_URL}console">${BUILD_URL}console</a></li>
                        <li>测试报告: <a href="${BUILD_URL}allure">${BUILD_URL}allure</a></li>
                        <li>构建状态: ${BUILD_STATUS}</li>
                        <li>用例总数为: <span style="font-weight: bold;">${ENV, var="TOTAL"}</span></li>
                        <li>通过用例数: <span style="color: green;font-weight: bold;">${ENV, var="PASSED"}</span></li>
                        <li>失败用例数: <span style="color: red;font-weight: bold;">${ENV, var="FAILED"}</span></li>
                        <li>错误用例数: ${ENV, var="ERROR"}</li>
                        <li>跳过用例数: ${ENV, var="SKIPPED"}</li>
                        <li>通过率为: <span style="font-weight: bold;">${ENV, var="SUCCESS_RATE"}</span></li>
                        <li>用例执行时长: ${ENV, var="TOTAL_TIMES"}</li>
                    </ul>
                </td>
            </tr>
        </table>
    </body>
    </html>''', subject: '$PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS!', to: '[email protected]'
                }
            }
        }
        post {
            success {
                script {
                    WeiXinSuccess()
                }
            }
            failure {
                script {
                    WeiXinFailure()
                }
            }
        }
    }
    
    def WeiXinSuccess(){
            sh """
                curl --location --request POST 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=1e352b24-8675-4d47-9652-xxxxxxxxxxxx' \
                    --header 'Content-Type: application/json' \
                    --data '{
                        "msgtype": "markdown",
                        "markdown": {
                            "content": "<font color=\'warning\'>**Jenkins任务通知**</font> \n
                            >构建人: <font color=\'comment\'>${env.BUILD_USER}</font>
                            >构建时间: <font color=\'comment\'>${BUILD_TIMESTAMP}</font>
                            >任务名称: <font color=\'comment\'>${JOB_BASE_NAME}</font>
                            >构建日志: <font color=\'comment\'>[点击查看](${BUILD_URL}/console)</font>
                            >测试报告: <font color=\'comment\'>[点击查看](${BUILD_URL}/allure)</font>
                            >构建状态: <font color=\'info\'>**Success**</font>
                            >用例总数为: <font color=\'comment\'>**${TOTAL}**</font>
                            >通过用例数: <font color=\'info\'>**${PASSED}**</font>
                            >失败用例数: <font color=\'warning\'>**${FAILED}**</font>
                            >错误用例数: <font color=\'comment\'>${ERROR}</font>
                            >跳过用例数: <font color=\'comment\'>${SKIPPED}</font>
                            >通过率为: <font color=\'comment\'>**${SUCCESS_RATE}**</font>
                            >用例执行时长: <font color=\'comment\'>${TOTAL_TIMES}</font>"
                        }
                    }'
            """
    }
    
    def WeiXinFailure(){
            sh """
                curl --location --request POST 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=1e352b24-8675-4d47-9652-xxxxxxxxxxxx' \
                    --header 'Content-Type: application/json' \
                    --data '{
                        "msgtype": "markdown",
                        "markdown": {
                            "content": "<font color=\'warning\'>**Jenkins任务通知**</font> \n
                            >构建人: <font color=\'comment\'>${env.BUILD_USER}</font>
                            >构建时间: <font color=\'comment\'>${BUILD_TIMESTAMP}</font>
                            >运行时长: <font color=\'comment\'>${currentBuild.durationString}</font>
                            >任务名称: <font color=\'comment\'>${JOB_BASE_NAME}</font>
                            >构建日志: <font color=\'comment\'>[点击查看](${BUILD_URL}/console)</font>
                            >构建状态: <font color=\'comment\'>**Failure**</font>"
                        }
                    }'
            """
    }
    
  • 首页选择流水线任务-->配置,输入上述脚本保存,执行构建

    image-20220723234744961

  • 构建效果如下

    image-20220724213428767

  • 企业微信群消息

  • 邮件报告内容

    image-20220724215324740

后记

  • 怎么获取测试用例执行结果

    使用pytest自带的 hook 函数pytest_terminal_summary可以统计用例执行情况,收集测试结果

  • 在项目根目录下conftest.py文件中输入如下代码

    def pytest_terminal_summary(terminalreporter, exitstatus, config):
        """
        收集测试结果
        """
        result = {
            'total': terminalreporter._numcollected,
            'passed': len([i for i in terminalreporter.stats.get('passed', []) if i.when != 'teardown']),
            'failed': len([i for i in terminalreporter.stats.get('failed', []) if i.when != 'teardown']),
            'error': len([i for i in terminalreporter.stats.get('error', []) if i.when != 'teardown']),
            'skipped': len([i for i in terminalreporter.stats.get('skipped', []) if i.when != 'teardown']),
            'success_rate': len(terminalreporter.stats.get('passed', [])) / terminalreporter._numcollected * 100,
            'total_times': round(time.time() - terminalreporter._sessionstarttime, 2),
        }
    
        // 将结果保存到本地
        write_result_to_txt(**result)
    
    def write_result_to_txt(
        total,
        passed,
        failed,
        error,
        skipped,
        success_rate,
        total_times,
    ):
        """
        将测试结果写入本地文件
        """
        with open('result.txt', 'w') as fp:
            fp.write("TOTAL=%s" % total + "\n")
            fp.write("PASSED=%s" % passed + "\n")
            fp.write("FAILED=%s" % failed + "\n")
            fp.write("ERROR=%s" % error + "\n")
            fp.write("SKIPPED=%s" % skipped + "\n")
            fp.write("SUCCESS_RATE=%.2f%%" % success_rate + "\n")
            fp.write("TOTAL_TIMES=%.2fs" % total_times)
    
  • 在 Jenkins 流水线脚本中将 txt 文件内容读取出来,加入环境变量

    stage('读取用例执行结果文件, 设置环境变量') {
      steps {
        script {
          def data = readFile(file: 'result.txt')
          // 按行读取
          def lines = data.readLines()
          def result_list = []
          for (line in lines) {
            String[] str;
            str = line.split('=');
            result_list.add(str)
          }
          // 设置环境变量
          env.TOTAL = result_list[0][1]
          env.PASSED = result_list[1][1]
          env.FAILED = result_list[2][1]
          env.ERROR = result_list[3][1]
          env.SKIPPED = result_list[4][1]
          env.SUCCESS_RATE = result_list[5][1]
          env.TOTAL_TIMES = result_list[6][1]
        }
      }
    }
    

遇到的问题

Jenkins 构建时,发现时区不对,显示的 UTC 时间。这是因为执行 jenkins 任务的 docker 容器本地时间是 UTC,解决办法如下: Jenkins 系统时间不正确解决方案open in new window

其它参考资料

基于 docker 部署实现接口自动化持续集成open in new window

Jenkins: 配置 jenkins 任务构建成功通知企业微信@相关人open in new window

Jenkins 实践扩展企业微信消息通知open in new window

Jenkins 发送邮件时,修改发件人名称open in new window

Jenkins 自动发送邮件配置及定时构建open in new window

把 pytest 运行结果通过 jenkins 发送到邮件正文里open in new window

利用 pytest hook 函数实现 python 自动化测试结果推送企微open in new window

pytest 的 Hook 函数详解open in new window

Jenkins pipeline 中按行读文件open in new window

Groovy 基础语法open in new window

上次更新 3/31/2024, 6:29:32 AM