使用 Jenkins + 企微机器人 + docker + cypress + allure 完成自动化测试及结果通知
镜像定义文件:开发镜像开发使用的镜像定义文件:镜像依赖文件启动脚本:修改过的 ,可选 - 可以使用官方版本。
·
CI 镜像
镜像定义文件: ssoor/cypress:yk-test
FROM ubuntu:latest
RUN sed -i s@/archive.ubuntu.com/@/mirrors.tuna.tsinghua.edu.cn/@g /etc/apt/sources.list
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y curl && \
apt-get clean all && rm -rf /var/lib/apt/lists/*
# Install Node
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - && \
apt-get update && \
apt-get install -y nodejs && \
apt-get clean all && rm -rf /var/lib/apt/lists/*
# Install allure
RUN curl -sL https://hub.fastgit.xyz/allure-framework/allure2/releases/download/2.17.3/allure_2.17.3-1_all.deb -o /tmp/allure_2.deb && \
apt-get update && \
apt-get install -y /tmp/allure_2.deb && \
rm -f /tmp/allure_2.deb && \
apt-get clean all && rm -rf /var/lib/apt/lists/*
# Install cypress-dependencies
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y \
libgtk2.0-0 \
libgtk-3-0 \
libgbm-dev \
libnotify-dev \
libgconf-2-4 \
libnss3 \
libxss1 \
libasound2 \
libxtst6 \
xauth \
xvfb && \
apt-get clean all && rm -rf /var/lib/apt/lists/*
# Install git
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y git && \
apt-get clean all && rm -rf /var/lib/apt/lists/*
# Install yarn
RUN npm config set registry 'https://registry.npm.taobao.org' && \
npm install -g yarn --cache /tmp/empty-cache && \
yarn config set registry 'https://registry.npm.taobao.org' && \
rm -rf /tmp/empty-cache
RUN useradd -ms /bin/bash app
USER app
# Configure npm global install path
RUN npm config set prefix '~/.npm-global' && \
npm config set registry 'https://registry.npm.taobao.org' && \
yarn config set registry 'https://registry.npm.taobao.org'
ENV PATH=~/.npm-global/bin:$PATH
# Install cypress
RUN npm install -g cypress
WORKDIR /app
开发镜像
开发使用的镜像定义文件:
FROM ssoor/cypress:yk-test
USER root
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y \
git \
curl \
x11vnc \
fluxbox && \
apt-get clean all && rm -rf /var/lib/apt/lists/*
# Install noVNC
RUN git clone https://hub.fastgit.xyz/novnc/noVNC.git --depth 1 /usr/local/share/noVNC && \
git clone https://hub.fastgit.xyz/novnc/websockify.git /usr/local/share/noVNC/utils/websockify && \
ln -s /usr/local/share/noVNC/utils/novnc_proxy /usr/local/bin/novnc_proxy
# Install Chrome
RUN curl https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -o /tmp/google-chrome.deb && \
apt-get update && \
apt-get install -y /tmp/google-chrome.deb && \
apt-get install ttf-wqy-microhei ttf-wqy-zenhei xfonts-wqy && \
rm -f /tmp/google-chrome.deb && \
apt-get clean all && rm -rf /var/lib/apt/lists/*
RUN npx cypress install
COPY start.sh /usr/local/bin/start.sh
COPY noVNC.html /usr/local/share/noVNC/index.html
CMD ["/usr/local/bin/start.sh"]
镜像依赖文件
启动脚本:start.sh
#!/bin/bash
LANG=C.UTF-8
SCREEN_WHD="${SCREEN_WHD:-1440x900x16}"
Xvfb :20 -screen 0 "${SCREEN_WHD}" &
sleep 1
x11vnc -display :20 -forever -bg -o "/tmp/x11vnc.log"
sleep 1
DISPLAY=:20 fluxbox -log /tmp/fluxbox.log &
sleep 1
novnc_proxy --vnc localhost:5900 --listen 0.0.0.0:6080 --cert /app/ssl/root.crt --key /app/ssl/root.key &
sleep 1
yarn
DISPLAY=:20 yarn cypress open --browser chrome
# yarn cypress run --env allure=true,allureResultsPath=results/allure -s cypress/integration/todo.spec.js
# allure serve -h 0.0.0.0 -p 1806 ./results/allure
wait
修改过的 noVNC
,可选 - 可以使用官方版本。
<!DOCTYPE html>
<html lang="en" class="noVNC_loading">
<head>
<!--
noVNC example: simple example using default UI
Copyright (C) 2019 The noVNC Authors
noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
Connect parameters are provided in query string:
http://example.com/?host=HOST&port=PORT&encrypt=1
or the fragment:
http://example.com/#host=HOST&port=PORT&encrypt=1
-->
<title>noVNC</title>
<meta charset="utf-8">
<!-- Icons (see app/images/icons/Makefile for what the sizes are for) -->
<link rel="icon" sizes="16x16" type="image/png" href="app/images/icons/novnc-16x16.png">
<link rel="icon" sizes="24x24" type="image/png" href="app/images/icons/novnc-24x24.png">
<link rel="icon" sizes="32x32" type="image/png" href="app/images/icons/novnc-32x32.png">
<link rel="icon" sizes="48x48" type="image/png" href="app/images/icons/novnc-48x48.png">
<link rel="icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-60x60.png">
<link rel="icon" sizes="64x64" type="image/png" href="app/images/icons/novnc-64x64.png">
<link rel="icon" sizes="72x72" type="image/png" href="app/images/icons/novnc-72x72.png">
<link rel="icon" sizes="76x76" type="image/png" href="app/images/icons/novnc-76x76.png">
<link rel="icon" sizes="96x96" type="image/png" href="app/images/icons/novnc-96x96.png">
<link rel="icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-120x120.png">
<link rel="icon" sizes="144x144" type="image/png" href="app/images/icons/novnc-144x144.png">
<link rel="icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-152x152.png">
<link rel="icon" sizes="192x192" type="image/png" href="app/images/icons/novnc-192x192.png">
<!-- Firefox currently mishandles SVG, see #1419039
<link rel="icon" sizes="any" type="image/svg+xml" href="app/images/icons/novnc-icon.svg">
-->
<!-- Repeated last so that legacy handling will pick this -->
<link rel="icon" sizes="16x16" type="image/png" href="app/images/icons/novnc-16x16.png">
<!-- Apple iOS Safari settings -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<!-- Home Screen Icons (favourites and bookmarks use the normal icons) -->
<link rel="apple-touch-icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-60x60.png">
<link rel="apple-touch-icon" sizes="76x76" type="image/png" href="app/images/icons/novnc-76x76.png">
<link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-120x120.png">
<link rel="apple-touch-icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-152x152.png">
<!-- Stylesheets -->
<link rel="stylesheet" href="app/styles/base.css">
<!-- Images that will later appear via CSS -->
<link rel="preload" as="image" href="app/images/info.svg">
<link rel="preload" as="image" href="app/images/error.svg">
<link rel="preload" as="image" href="app/images/warning.svg">
<script type="module" crossorigin="anonymous" src="app/error-handler.js"></script>
<script type="module" crossorigin="anonymous" src="app/ui.js"></script>
<script type="module">
let init = false;
let parentClipboard = "";
const clipboardBtn = document.getElementById("noVNC_clipboard_button");
const elementStyle = (elem) => elem.currentStyle ? elem.currentStyle : window.getComputedStyle(elem, null);
const syncClipboard = () => {
try {
if (elementStyle(clipboardBtn).display == "none" || document.hasFocus() == false) {
return;
}
navigator.clipboard.readText().then(
(text) => {
text = text.replace(/\r\n/g,"\n");
const remoteClipboard = document.getElementById("noVNC_clipboard_text").value.replace(/\r\n/g,"\n");
if (init === false && text === remoteClipboard && remoteClipboard === "") {
init = true;
console.log("clipboard init");
document.getElementById("noVNC_clipboard_text").value = " ";
document.getElementById("noVNC_clipboard_text").dispatchEvent(new UIEvent('change'));
document.getElementById("noVNC_clipboard_text").value = "";
document.getElementById("noVNC_clipboard_text").dispatchEvent(new UIEvent('change'));
}
let sync = "";
if (parentClipboard !== text) {
sync = "local";
} else if (remoteClipboard !== text) {
sync = "remote";
text = remoteClipboard;
}
if (sync != "") {
init = true;
console.log(sync, "clipboard:", JSON.stringify(text));
parentClipboard = text;
navigator.clipboard.writeText(text);
document.getElementById("noVNC_clipboard_text").value = text;
document.getElementById("noVNC_clipboard_text").dispatchEvent(new UIEvent('change'));
}
}
);
} catch (e) {
}
}
setInterval(syncClipboard, 200);
setTimeout(() => { document.getElementById("noVNC_connect_button").dispatchEvent(new UIEvent('click')); }, 1000);
</script>
</head>
<body>
<div id="noVNC_fallback_error" class="noVNC_center">
<div>
<div>noVNC encountered an error:</div>
<br>
<div id="noVNC_fallback_errormsg"></div>
</div>
</div>
<!-- noVNC Control Bar -->
<div id="noVNC_control_bar_anchor" class="noVNC_vcenter">
<div id="noVNC_control_bar">
<div id="noVNC_control_bar_handle" title="Hide/Show the control bar">
<div></div>
</div>
<div class="noVNC_scroll">
<h1 class="noVNC_logo" translate="no"><span>no</span><br>VNC</h1>
<!-- Drag/Pan the viewport -->
<input type="image" alt="Drag" src="app/images/drag.svg" id="noVNC_view_drag_button"
class="noVNC_button noVNC_hidden" title="Move/Drag Viewport">
<!--noVNC Touch Device only buttons-->
<div id="noVNC_mobile_buttons">
<input type="image" alt="Keyboard" src="app/images/keyboard.svg" id="noVNC_keyboard_button"
class="noVNC_button" title="Show Keyboard">
</div>
<!-- Extra manual keys -->
<input type="image" alt="Extra keys" src="app/images/toggleextrakeys.svg"
id="noVNC_toggle_extra_keys_button" class="noVNC_button" title="Show Extra Keys">
<div class="noVNC_vcenter">
<div id="noVNC_modifiers" class="noVNC_panel">
<input type="image" alt="Ctrl" src="app/images/ctrl.svg" id="noVNC_toggle_ctrl_button"
class="noVNC_button" title="Toggle Ctrl">
<input type="image" alt="Alt" src="app/images/alt.svg" id="noVNC_toggle_alt_button"
class="noVNC_button" title="Toggle Alt">
<input type="image" alt="Windows" src="app/images/windows.svg" id="noVNC_toggle_windows_button"
class="noVNC_button" title="Toggle Windows">
<input type="image" alt="Tab" src="app/images/tab.svg" id="noVNC_send_tab_button"
class="noVNC_button" title="Send Tab">
<input type="image" alt="Esc" src="app/images/esc.svg" id="noVNC_send_esc_button"
class="noVNC_button" title="Send Escape">
<input type="image" alt="Ctrl+Alt+Del" src="app/images/ctrlaltdel.svg"
id="noVNC_send_ctrl_alt_del_button" class="noVNC_button" title="Send Ctrl-Alt-Del">
</div>
</div>
<!-- Shutdown/Reboot -->
<input type="image" alt="Shutdown/Reboot" src="app/images/power.svg" id="noVNC_power_button"
class="noVNC_button" title="Shutdown/Reboot...">
<div class="noVNC_vcenter">
<div id="noVNC_power" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/power.svg"> Power
</div>
<input type="button" id="noVNC_shutdown_button" value="Shutdown">
<input type="button" id="noVNC_reboot_button" value="Reboot">
<input type="button" id="noVNC_reset_button" value="Reset">
</div>
</div>
<!-- Clipboard -->
<input type="image" alt="Clipboard" src="app/images/clipboard.svg" id="noVNC_clipboard_button"
class="noVNC_button" title="Clipboard">
<div class="noVNC_vcenter">
<div id="noVNC_clipboard" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/clipboard.svg"> Clipboard
</div>
<textarea id="noVNC_clipboard_text" rows=5></textarea>
<br>
<input id="noVNC_clipboard_clear_button" type="button" value="Clear" class="noVNC_submit">
</div>
</div>
<!-- Toggle fullscreen -->
<input type="image" alt="Full Screen" src="app/images/fullscreen.svg" id="noVNC_fullscreen_button"
class="noVNC_button noVNC_hidden" title="Full Screen">
<!-- Settings -->
<input type="image" alt="Settings" src="app/images/settings.svg" id="noVNC_settings_button"
class="noVNC_button" title="Settings">
<div class="noVNC_vcenter">
<div id="noVNC_settings" class="noVNC_panel">
<ul>
<li class="noVNC_heading">
<img alt="" src="app/images/settings.svg"> Settings
</li>
<li>
<label><input id="noVNC_setting_shared" type="checkbox"> Shared Mode</label>
</li>
<li>
<label><input id="noVNC_setting_view_only" type="checkbox"> View Only</label>
</li>
<li>
<hr>
</li>
<li>
<label><input id="noVNC_setting_view_clip" type="checkbox"> Clip to Window</label>
</li>
<li>
<label for="noVNC_setting_resize">Scaling Mode:</label>
<select id="noVNC_setting_resize" name="vncResize">
<option value="off">None</option>
<option value="scale">Local Scaling</option>
<option value="remote">Remote Resizing</option>
</select>
</li>
<li>
<hr>
</li>
<li>
<div class="noVNC_expander">Advanced</div>
<div>
<ul>
<li>
<label for="noVNC_setting_quality">Quality:</label>
<input id="noVNC_setting_quality" type="range" min="0" max="9" value="6">
</li>
<li>
<label for="noVNC_setting_compression">Compression level:</label>
<input id="noVNC_setting_compression" type="range" min="0" max="9"
value="2">
</li>
<li>
<hr>
</li>
<li>
<label for="noVNC_setting_repeaterID">Repeater ID:</label>
<input id="noVNC_setting_repeaterID" type="text" value="">
</li>
<li>
<div class="noVNC_expander">WebSocket</div>
<div>
<ul>
<li>
<label><input id="noVNC_setting_encrypt" type="checkbox">
Encrypt</label>
</li>
<li>
<label for="noVNC_setting_host">Host:</label>
<input id="noVNC_setting_host">
</li>
<li>
<label for="noVNC_setting_port">Port:</label>
<input id="noVNC_setting_port" type="number">
</li>
<li>
<label for="noVNC_setting_path">Path:</label>
<input id="noVNC_setting_path" type="text" value="websockify">
</li>
</ul>
</div>
</li>
<li>
<hr>
</li>
<li>
<label><input id="noVNC_setting_reconnect" type="checkbox"> Automatic
Reconnect</label>
</li>
<li>
<label for="noVNC_setting_reconnect_delay">Reconnect Delay (ms):</label>
<input id="noVNC_setting_reconnect_delay" type="number">
</li>
<li>
<hr>
</li>
<li>
<label><input id="noVNC_setting_show_dot" type="checkbox"> Show Dot when No
Cursor</label>
</li>
<li>
<hr>
</li>
<!-- Logging selection dropdown -->
<li>
<label>Logging:
<select id="noVNC_setting_logging" name="vncLogging">
</select>
</label>
</li>
</ul>
</div>
</li>
<li class="noVNC_version_separator">
<hr>
</li>
<li class="noVNC_version_wrapper">
<span>Version:</span>
<span class="noVNC_version"></span>
</li>
</ul>
</div>
</div>
<!-- Connection Controls -->
<input type="image" alt="Disconnect" src="app/images/disconnect.svg" id="noVNC_disconnect_button"
class="noVNC_button" title="Disconnect">
</div>
</div>
<div id="noVNC_control_bar_hint"></div>
</div> <!-- End of noVNC_control_bar -->
<!-- Status Dialog -->
<div id="noVNC_status"></div>
<!-- Connect button -->
<div class="noVNC_center">
<div id="noVNC_connect_dlg">
<div class="noVNC_logo" translate="no"><span>no</span>VNC</div>
<div id="noVNC_connect_button">
<div>
<img alt="" src="app/images/connect.svg"> Connect
</div>
</div>
</div>
</div>
<!-- Server Key Verification Dialog -->
<div class="noVNC_center noVNC_connect_layer">
<div id="noVNC_verify_server_dlg" class="noVNC_panel">
<form>
<div class="noVNC_heading">
Server identity
</div>
<div>
The server has provided the following identifying information:
</div>
<div id="noVNC_fingerprint_block">
<b>Fingerprint:</b>
<span id="noVNC_fingerprint"></span>
</div>
<div>
Please verify that the information is correct and press
"Approve". Otherwise press "Reject".
</div>
<div>
<input id="noVNC_approve_server_button" type="submit" value="Approve" class="noVNC_submit">
<input id="noVNC_reject_server_button" type="button" value="Reject" class="noVNC_submit">
</div>
</form>
</div>
</div>
<!-- Password Dialog -->
<div class="noVNC_center noVNC_connect_layer">
<div id="noVNC_credentials_dlg" class="noVNC_panel">
<form>
<div class="noVNC_heading">
Credentials
</div>
<div id="noVNC_username_block">
<label for="noVNC_username_input">Username:</label>
<input id="noVNC_username_input">
</div>
<div id="noVNC_password_block">
<label for="noVNC_password_input">Password:</label>
<input id="noVNC_password_input" type="password">
</div>
<div>
<input id="noVNC_credentials_button" type="submit" value="Send Credentials" class="noVNC_submit">
</div>
</form>
</div>
</div>
<!-- Transition Screens -->
<div id="noVNC_transition">
<div id="noVNC_transition_text"></div>
<div>
<input type="button" id="noVNC_cancel_reconnect_button" value="Cancel" class="noVNC_submit">
</div>
<div class="noVNC_spinner"></div>
</div>
<!-- This is where the RFB elements will attach -->
<div id="noVNC_container">
<!-- Note that Google Chrome on Android doesn't respect any of these,
html attributes which attempt to disable text suggestions on the
on-screen keyboard. Let's hope Chrome implements the ime-mode
style for example -->
<textarea id="noVNC_keyboardinput" autocapitalize="off" autocomplete="off" spellcheck="false"
tabindex="-1"></textarea>
</div>
<audio id="noVNC_bell">
<source src="app/sounds/bell.oga" type="audio/ogg">
<source src="app/sounds/bell.mp3" type="audio/mpeg">
</audio>
</body>
</html>
Jenkinsfile 参考脚本
pipeline{
agent any
options {
ansiColor('xterm')
}
stages{
stage("执行测试用例"){
agent {
docker { image 'ssoor:cypress' }
}
steps{
checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [], userRemoteConfigs: [[credentialsId: '02007fe4-2bff-4989-98a1-1503397b03ea', url: 'https://github.com/ssoor/allure-report.git']]])
dir("${CI_PROJECT_PATH}") {
echo "Install cypress dependency"
sh """
yarn install --production
"""
echo "Generate cypress results"
sh """
rm -rf "results/allure"
npx cypress run -s cypress/integration/judge.spec.js
"""
}
withCredentials([gitUsernamePassword(credentialsId: '02007fe4-2bff-4989-98a1-1503397b03ea', gitToolName: 'Default')]) {
sh 'git checkout origin/master'
}
dir("${CI_PROJECT_PATH}") {
echo "Generate allure-report results from cypress"
sh """
mkdir -p "reports/allure/history"
cp -r "reports/allure/history" "results/allure"
allure generate "results/allure" -o "results/allure-report" --clean
"""
}
}
}
stage("保存测试报告"){
agent {
docker { image 'ssoor:gitlab-runner' }
}
steps{
dir("${CI_PROJECT_PATH}") {
// Jenkins 如果设置 Jenkinsfile 通过 GIT 仓库拉取
// 则每个 stage 都会触发 fetch 操作导致之前 stage 对仓库已有文件的修改丢失
// 所以在这里才真正修改仓库已有文件
echo "Save allure-report to git reports"
sh """
rm -rf "reports/allure"
mv "results/allure-report" "reports/allure"
"""
echo "Commit allure-report to gitlab"
withCredentials([gitUsernamePassword(credentialsId: '02007fe4-2bff-4989-98a1-1503397b03ea', gitToolName: 'Default')]) {
sh """
git config --global user.name "gitlab-ci"
git config --global user.email "gitlab-ci@mail.com"
"""
sh """
git add .
git commit -m "Gitlab generate by ${BUILD_ID}"
git fetch origin master
git rebase origin/master
git push -u origin HEAD:master
"""
}
}
}
}
stage("发送测试结果通知"){
agent {
docker { image 'ssoor:gitlab-runner' }
}
environment {
QYWX_ROBOT_URL = "${QYWX_ROBOT_URL}"
CI_PROJECT_PATH = "${CI_PROJECT_PATH}"
CI_COMMIT_BRANCH = "${CI_COMMIT_BRANCH}"
}
steps{
dir("${CI_PROJECT_PATH}") {
echo "Send test repost to QYWX"
sh '''
RESULT_ALL=`cat reports/allure/history/history-trend.json | jq .[0].data`
RESULT_TOTAL=`echo $RESULT_ALL | jq .total`
RESULT_PASSED=`echo $RESULT_ALL | jq .passed`
RESULT_FAILED=`echo $RESULT_ALL | jq .failed`
RESULT_SKIPPED=`echo $RESULT_ALL | jq .skipped`
cat | bash << EOF
curl --location --request POST '${QYWX_ROBOT_URL}' \
--header 'Content-Type: application/json' \
--data-raw '{
"msgtype": "markdown",
"markdown": {
"content": "# 仓库:*<font color=\\"#660000\\">[${CI_PROJECT_PATH}](https://github.com/${CI_PROJECT_PATH})</font>*\\n\\n\\n\\n本次执行用例 ${RESULT_TOTAL} 例,执行分支:<font color=\\"#000066\\">**${CI_COMMIT_BRANCH}**</font>\\n> 成功:<font color=\\"#006600\\">${RESULT_PASSED}例</font>\\n> 失败:<font color=\\"#dd0000\\">${RESULT_FAILED}例</font>\\n> 跳过:<font color=\\"#808080\\">${RESULT_SKIPPED}例</font>\\n\\n点击查看详情:[Allure-report](http://10.9.40.75:4480/${CI_PROJECT_PATH})"
}
}'
'''
}
}
}
}
}
cypress.json 配置文件
{
"defaultCommandTimeout": 0,
"env": {
"allure": true,
"allureAttachRequests": true,
"allureResultsPath": "./results/allure",
"JUDGE_ORGCODE": "huaxiaadmin_test",
"JUDGE_USERCODE": "mysoftadmin",
"JUDGE_URL": "http://gateway-test.myscrm.cn/channel-judge/v1/judge"
}
}

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)