- 微信小程序UI自动化实践:python+minium+PO模式
@TOC 前言 公司要求做小程序的自动化,最开始让我用jest+SDK做,但后面学了两天,困难重重,第一,这种方式只支持JS语言,我以前从来没学过JS语言,JS一窍不通;第二,网上资料太少太少了,本来对JS就不懂,网上资料还少,后面去网上百度了下,minium支持python语言,我原来有一点python基础,所以最后改用了python+minium框架。 一、minium介绍 minium提供一个基于unittest封装好的测试框架,MiniTest是minium中继承自unittest.TestCase的测试基类, 你可以在testcase中使用框架实例化好的Minium/App/Native实例,也可以使用unittest中的各种断言函数,并做了以下改动: 1、加载读取测试配置 2、在合适的时机初始化minium.Minium、minium.App和minium.Native 3、根据配置打开IDE,拉起小程序项目或自动打开真机调试 4、拦截assert调用,记录检验结果 5、记录运行时数据和截图,用于测试报告生成 二、安装环境 1. 安装minium doc (这个主要是minium框架的一些介绍,可以略过不安装直接去官网查看文档:https://minitest.weixin.qq.com/#/minium/Python/readme) (1)该文档使用 docsify 框架,需先安装 docsify [代码] npm i docsify-cli -g [代码] (2)下载文档(会要求填写账号密码,指的是微信代码管理的账号和密码) [代码] git clone https://git.weixin.qq.com/minitest/minium-doc [代码] (3)安装依赖 [代码] cd minium-doc npm install [代码] (4)本地部署(浏览器能浏览http://localhost:3000/说明文档安装好了) [代码] docsify serve . [代码] [图片] [图片] 2. 安装minium (1)运行环境 Python 3.8及以上 微信开发者工具安全模式: 设置 -> 安全设置 -> 服务端口: 打开 确认微信公共库版本 >= 2.7.3 (2)安装 下载minium安装包, 解压后进入文件夹, 运行 [代码] python3 setup.py install [代码] 3. 启动小程序 [代码]import minium class TestMiniprogram(minium.MiniTest): def test_01(self): pass [代码] 命令行运行脚本 [代码]minitest -c config.json -m tests.igtest [代码] 三、准备知识 1. 启动 minium.MiniTest类里面已经封装好了小程序的启动、关闭、调用配置、执行测试用例等一系列的方法,所以我们编写测试用例脚本的时候,定义的类在继承minium.MiniTest类之后,可以直接写测试用例,无需关注怎么启动。 2. 配置 minium框架里面默认配置的项目路径以及CLI工具路径都为None,所以会加载默认配置,如果我们的项目路径以及CLI工具路径不是用的默认路径,执行会报错找不到路径,所以我们需要在项目路径下新建一个config.json文件,将里面的project_path改为你的小程序项目路径,dev_tool_path改为你的CLI工具路径 [图片] 3. 命令行运行 相关字段的说明 [代码]minitest -c config.json -m tests.igtest -g -c 指定配置文件 -m 指定要执行的用例文件名(注意不需要.py) -g 生成测试报告 [代码] 4. 元素定位 [图片] (1)单选择器定位:一般可以使用.class或者#id去定位到元素 (2)多选择器定位:如果元素class有重名,id也有相同的,可以使用.class+#id去定位 (3)组合定位:如果有多个元素的class相同,id又是变化的,可以使用page.get_element(’.main-menu-txt’, inner_text=’租赁合同’, text_contains=‘租赁合同’),或者使用page.get_elements(’.main-menu-txt’)[序号] 5. 断言 常用的断言主要有三种: (1)assertEqual(first, second, msg) first == second时,断言成功,用例结果符合预期 first != second时,断言失败,抛出错误信息及msg (2)assertTrue(expr, msg) expr为True,断言成功,用例结果符合预期 expr为False,断言失败,抛出错误信息及msg (3)assertTexts(texts, selector, msg) texts中每个元素的值都包含在selector选择器对应的元素文本集合中,则断言成功,否则,断言失败,抛出错误信息及msg 6. 第一个测试用例 [代码]import minium class TestIg(minium.MiniTest): def test_01_login(self): self.mini.page.wait_for(2) # 当前页面等待2s self.mini.page.get_element('.login-word-input').input(user) # 输入用户名 self.mini.page.get_element('.login-pass').input(password) # 输入密码 self.mini.page.get_element('.loginBtn').click() # 点击登录按钮 self.mini.page.wait_for(3) # 当前页面等待3s self.assertEqual('/pages/main/index', self.mini.app.current_page.path, msg='登录失败') # 断言登录后是否跳转到首页,如果断言失败,则返回mgs"登录失败" [代码] [代码]minitest -c config.json -tests.igtest [代码] 四、用例设计模式 上面第一个测试用例写到了登录,非常简单常见的一个场景,但像我在做的这个项目,在登录后,跳转到首页后有项目切换、项目信息、项目试算、租赁订单、租赁合同等等非常多模块的定位、元素点击、页面滑动、页面跳转以及断言等,把这么多代码全部写到一个类里面,代码多而繁杂,如果页面元素有更改,或者需要增加用例,查找起来非常不方便,更不要说整个小程序内有那么多页面,所以需要进行分层管理,我使用的是PO模式 1. 什么是PO模式 PO模式,即page object mode,页面对象模式,通过对界面元素和功能模块的封装减少冗余代码,同时在后期维护中,若元素定位或功能模块发生变化,只需要调整页面元素或功能模块封装的代码,提高测试用例的可维护性。 2. 层级关系 第一层:基础层BasePage,作用:封装一些minium的原生方法,如元素定位、框架跳转等 第二层:PO层,页面对象层,如元素定位、获得元素对象、页面操作 第三层:测试用例层,主要负责业务逻辑和数据驱动 三层之间的关系:PO层继承基础层的类,测试用例层调用PO层 [图片] 3. 优点 (1)分层清晰,易读 (2)易维护,由于元素定位与业务逻辑的分层特性,元素定位变动只需要修改PO层的元素定位,不需要修改业务逻辑,容易维护 (3)可复用,由于在基础层已经对minium的原生方法进行过二次封装,所以方法具有一定的通用性,可复用(但这取决于二次封装方法的通用性有多高) 五、框架 1. 目录结构 [图片] (1)/cases/base/ /cases/base/basecase:测试用例基类,用于设置用例输出路径和清理工作,项目的测试用例都继承此类 /cases/base//basedef:页面基类,封装所有页面会用到的公用方法 /cases/base/router:小程序内各页面路径 (2)/cases/pages 页面对象模型:获得元素对象、页面操作等 (3)tests包 各页面的测试脚本 /tests/outputs 测试报告:记录各测试用例的执行情况 /tests/config.json 配置信息,包括小程序项目地址、cli工具地址等 /tests/suite.json 测试计划,配置要执行的用例及执行顺序 2. BaseCase基类 (1)复写minium.MiniTest类里面的setUpClass、tearDownClass、setUp、tearDown方法 setUpClass、tearDownClass前面一定要加@classmethod修饰器,这两个方法在整个测试计划执行期间只会执行一次,setUp、tearDown在每个测试用例case执行前后都会执行,minium.MiniTest内的setUp方法会将小程序恢复到默认页面,即小程序首页 (2)因为我写了很多页面,如果在每个页面里面的每条用例执行前都恢复到小程序首页,会大大增加用例执行时间,所以我复写的setUp、tearDown方法都直接pass了 [代码]from pathlib import Path import minium class BaseCase(minium.MiniTest): """测试用例基类""" @classmethod def setUpClass(cls): super(BaseCase, cls).setUpClass() output_dir = Path(cls.CONFIG.outputs) if not output_dir.is_dir(): output_dir.mkdir() @classmethod def tearDownClass(cls): super(BaseCase, cls).tearDownClass() def setUp(self): pass def tearDown(self): pass [代码] 3. BaseDef公共方法类 定义一些公共方法 [代码]class BaseDef: def __init__(self, mini): self.mini = mini '''跳转到指定页面''' def navigate_to_open(self, route): self.mini.app.navigate_to(route) '''跳转到指定页面并关闭当前页面''' def redirect_to_open(self, route): self.mini.app.redirect_to(route) '''跳转到tabbar页面,关闭其他非tabbar页面''' def switch_to_tabbar(self, route): self.mini.app.switch_tab(route) '''跳转到非原生tabbar页面''' def switch_to_not_tabbar(self, selector, str=None): self.mini.page.get_element(selector, inner_text=str).click() '''无需加载的页面滑动到页面底部''' def scroll_to_buttom(self, selector): el = self.mini.page.get_element('scroll-view') rect = self.mini.page.get_element(selector).rect el.scroll_to(y=rect['top']) '''元素列表中找第几个元素''' def get_el_in_els(self, selector, index=0): els = self.mini.page.get_elements(selector) return els[index] '''断言每个元素文本中都包含某元素''' def assertIn(self, selector, text, msg=None): els1 = self.mini.page.get_elements(selector, text_contains=text) els2 = self.mini.page.get_elements(selector) if els1 != els2: raise AssertionError("selector:%s, inner_text=%s not Found") @property def current_path(self): return self.mini.page.path [代码] 4. /pages页面对象类 里面写了每个页面的元素定位,页面操作 (1)homepage:首页相关的元素定位、页面操作 [代码]from business_test.cases.base import router, basedef class HomePage(basedef.BaseDef): locator = { 'top_area': '.true-top-menu-item', # 金刚位 'main_area': '.main-menu-item', # 功能位 'project': '.content', # 项目切换 'project_select': '.list-cell' # 项目选择 } # 校验首页页面路径 def check_homepage_path(self): self.mini.page.wait_for(2) self.mini.assertEqual(self.current_path, router.homePage) # 根据index,点击金刚位的项目卡片、试算助手、联系人、流程中心 def toparea_hop(self, index=0): els = self.mini.page.get_elements(self.locator['top_area']) els[index].click() # 根据序号,点击功能位的租赁订单、租赁合同…… def mainarea_hop(self, index=0): els = self.mini.page.get_elements(self.locator['main_area']) els[index].click() # 当前项目名称 def current_project_name(self): self.mini.page.wait_for(self.locator['project'], max_timeout=3) return self.mini.page.get_element(self.locator['project']).inner_text # 跳转到项目选择页面 def project_select_page(self): self.mini.page.wait_for(self.locator['project'], max_timeout=3) self.mini.page.get_element(self.locator['project']).click() self.mini.page.wait_for(3) return self.mini.app.current_page # 选择项目 def project_selector(self, project_name=''): page = self.mini.app.current_page page.wait_for(self.locator['project_select'], max_timeout=3) if project_name == '': return page.get_elements(self.locator['project_select']) else: return page.get_element(self.locator['project_select'], inner_text=project_name) [代码] (2)customerpage:客户管理页面相关的元素定位、页面操作 [代码]略。。。 [代码] 4. /tests测试用例类 (1)homepage_test:首页相关的测试用例 [代码]from business_test.cases.pages import loginpage from business_test.cases.pages import homepage from business_test.cases.base.basecase import BaseCase from business_test.cases.base import router, accounts from random import randint import minium # 小程序首页测试 @minium.ddt_class() class HomePageTest(BaseCase): def __init__(self, methodName = 'runTest'): super(HomePageTest, self).__init__(methodName) self.homePage = homepage.HomePage(self) def test_00_login_sure(self): if self.app.current_page.path == router.loginPage: loginpage.LoginPage(self).login(user=accounts.user, password=accounts.password) else: pass def test_01_homepagePath(self): """验证首页路径正确""" self.homePage.check_homepage_path() def test_02_modulesExist(self): """验证首页相关模块存在""" self.assertTexts(['到期房源', '项目商机', '签约概览'], 'view', '到期房源、项目商机、签约概览模块存在') @minium.ddt_case( (0, '首页—>项目卡片'), (1, '首页—>试算助手'), (2, '首页—>联系人'), (3, '首页—>流程中心') ) def test_03_topAreaPath(self, args): """验证首页金刚位入口正确跳转""" paths = [router.projectinfoPage, router.trialPage, router.contactPage, router.processPage] self.homePage.toparea_hop(index=args[0]) self.page.wait_for(2) try: self.assertEqual(paths[args[0]], self.app.current_page.path, msg=args[1]) self.app.navigate_back() except: self.app.navigate_back() @minium.ddt_case( (0, '首页—>租赁订单列表'), (1, '首页—>租赁合同列表'), (2, '首页—>认购单列表'), (3, '首页—>销售合同列表'), (4, '首页—>企业动态'), (5, '首页—>企业宝典'), (6, '首页—>租赁需求列表'), (7, '首页—>销售需求') ) def test_04_mainAreaPath(self, args): """验证首页功能位入口正确跳转""" paths = [router.rentPage, router.rentPage, router.salePage, router.salePage, router.dynamicPage, router.biblePage, router.demandPage, router.demandPage] self.homePage.mainarea_hop(index=args[0]) self.page.wait_for(2) try: self.assertEqual(paths[args[0]], self.app.current_page.path, msg=args[1]) self.app.navigate_back() except: self.app.navigate_back() def test_05_switchProjectPath(self): """验证项目选择页面路径正确""" self.homePage.project_select_page() try: self.assertEqual(router.selectProjectPage, self.app.current_page.path) self.app.navigate_back() except: self.app.navigate_back() def test_06_switchProject(self): """验证随机切换项目成功""" self.homePage.project_select_page() projects = self.homePage.project_selector() selection = projects[randint(0, len(projects) - 1)] project_name = selection.inner_text selection.click() self.assertEqual(project_name, self.homePage.current_project_name(), msg='当前项目为随机选择项目') def test_07_reset(self): """一个页面测试完成,重置到首页InfiPark项目""" self.homePage.redirect_to_open(router.homePage) self.homePage.project_select_page() self.homePage.project_selector(project_name='InfiPark').click() self.assertEqual(self.homePage.current_project_name(), 'InfiPark') [代码] (2)customerpage_test:客户管理相关的测试用例 [代码]略。。。 [代码] 5. config.json配置 [代码]{ "project_path": "C:/Users/LX/Desktop/mini/cloud-merchant-mini-programs", # 项目路径 "dev_tool_path": "D:/微信开发者工具/微信web开发者工具/cli.bat", # CLI工具路径 "debug_mode": "debug", # 日志打印级别 "test_port": 9420, # 端口号 "platform": "ide", # 测试平台,有ide、Android、IOS "app": "wx", "assert_capture": true, "request_timeout": 60, "remote_connect_timeout": 300, "auto_relaunch": false, "enable_app_log": true } [代码] 6. suite.json测试计划配置 配置需要执行的用例以及用例执行顺序,根据pkg去匹配包名,找到测试类,然后再根据case_list里面的规则去查找测试类的测试用例。 在suite.json中,未明确指定用例的执行顺序时,用例是按照用例名称升序去执行的,所以定义用例名称时,最好在test后加上数字,保证用例按照我们书写的顺序执行 [代码]{ "pkg_list": [ { "case_list": [ "test_*" # 用例名称,*是通配符,意思是执行以test_开关的测试用例 ], "pkg": "tests.homepage_test" # 用例文件名,不需要加.py }, { "case_list": [ "test_*" ], "pkg": "tests.customerpage_test" } ] } [代码] 7. 执行测试计划 [代码]minitest -c config.json -s suite.json -g # 按照suite.json测试计划执行,输出报告 minitest -c config.json -m tests.homepage_test --case test_03_moduleExist # 执行homepage_test里面的test_03_moduleExist用例 [代码] 8. 查看测试报告 用例执行完成后,会自动生成测试报告,相关数据存放在outputs目录下,目录下面有一个index.html文件,但是我们不能直接用浏览器打开这个文件,需要把这个目录放到一个静态服务器上 [代码] 运行命令 python -m http.server 1234 -d outputs 然后在浏览器上访问http://localhost:1234即可查看报告 [代码] [图片] 9. 真机测试 真机上测试其实是通过开发者工具的真机调试来运行的,所以还是要用到开发者工具 (1)电脑上安装adb,之后命令行运行adb devices获取设备号 C:\windows\system32>adb devices List of devices attached 9YS0220820019844 device (2)修改config.json文件 [图片] (3)与模拟器一样命令行运行就可以了 10. 云测 基于minium框架的小程序UI自动化云测 六、ddt数据驱动 1. 什么是数据驱动 数据驱动,指在自动化测试中处理测试数据的方式。 通常测试数据与功能函数分离,存储在功能函数的外部位置。在自动化测试运行时,数据驱动框架会读取数据源中的数据,把数据作为参数传递到功能函数中,并会根据数据的条数多次运行同一个功能函数。 2. 数据驱动的优点 (1)减少重复代码 比如常见的登录场景,登录可以设计两条用例,登录失败与登录成功,区别只是输入(账号、密码)与输出(跳转路径)不同,常规写法就是写两条用例,test_login_success与test_login_fail,但其实他们有很多重复的代码,如果有的场景,设计的用例非常多,那重复代码也将会非常多,所以引入了ddt数据驱动。 (2)测试用例之间数据隔离 其中一条用例失败,不会影响其他用例的执行 3. 使用详解 minium框架中已经封装好了ddt,只需要在类前加上@minium.ddt_class,用例test_login前面加上@minium.ddt_case((arg1), (arg2), (arg3), ……),最后ddt_case里面有多少参数,用例就会被展开成多少条,在test_login(self, args)中,依次读取ddt_case中的参数,第一条用例中,args=arg1,第二条用例中,args=arg2,第三条用例中,args=arg3……,这样,我们就可以只写一个方法,达到完成两条甚至多条测试用例的目的 [代码]from business_test.cases.base.basecase import BaseCase from business_test.cases.pages import loginpage from business_test.cases.base import router, accounts import minium @minium.ddt_class() class LoginPageTest(BaseCase): def __init__(self, methodName='runTest'): super(LoginPageTest, self).__init__(methodName) self.loginPage = loginpage.LoginPage(self) @minium.ddt_case( (0, '登录失败-账号/密码错误', 'aiyumei', '200'), (1, '登录成功-账号:aiyumei', accounts.user, accounts.password) ) def test_login(self, args): if self.app.current_page.path == router.loginPage: paths = [router.loginPage, router.homePage] self.loginPage.login(user=args[2], password=args[3]) self.page.wait_for(3) self.assertEqual(paths[args[0]], self.mini.app.current_page.path, msg=args[1]) elif self.app.current_page.path == router.homePage: pass [代码] 七、遇到的问题 1. page.scroll_to滚动无效 页面滚动方法page.scroll_to无效,有时候开发在滚动的时候,会在上面加一层scroll-view组件,导致页面直接滚动无效,需要先定位到scroll-view,再以该元素进行滚动scroll_to操作 2. 前一条用例失败会影响后一条用例的执行 同一个页面,两条用例之间可能会有关联,比如下面的test_05跟test_06,涉及到两个页面,首页和项目选择页面,test_05跟test_06的初始页面都是首页,如果test_05断言失败的话,就不会执行后面的“self.app.navigate_back()”返回首页操作,导致test_06的初始页面不对,test_06也会执行失败,所以需要用到try…except [代码] def test_05_switchProjectPath(self): """验证项目选择页面路径正确""" self.homePage.project_select_page() try: self.assertEqual(router.selectProjectPage, self.app.current_page.path) self.app.navigate_back() except: self.app.navigate_back() def test_06_switchProject(self): """验证随机切换项目成功""" self.homePage.project_select_page() projects = self.homePage.project_selector() selection = projects[randint(0, len(projects) - 1)] project_name = selection.inner_text selection.click() self.assertEqual(project_name, self.homePage.current_project_name(), msg='当前项目为随机选择项目') [代码] 3. miniddt修改用例名称 参考该文章:修改miniddt生成用例的名称 4. 小程序右上角菜单转发 参考该文章:小程序自动化框架minium——右上角菜单转发分享 源码 附源码链接,有需要可以下载 https://download.csdn.net/download/baguenaudier/85819436
2022-07-25 - 基于Minium框架的小程序自动化落地实践
致谢 本文的技术实现细节,要特别感谢微信团队中的严烨,乃华,冯永鹏等小伙伴的热心帮忙和技术指导。 Minium框架简介 框架优点 微信小程序官方推出的小程序自动化框架,是为小程序专门开发的自动化框架, 提供了 Python 和 JavaScript 版本。 支持一套脚本,iOS & Android & 模拟器,三端运行 提供丰富的页面跳转方式,看不到也能去得到 可以获取和设置小程序页面数据,让测试不止点点点 支持往 AppSerive 注入代码片段 可以使用 minium 来进行函数的 mock, 可以直接跳转到小程序某个页面并设置页面数据, 做针对性的全面测试 框架缺点 暂不支持H5页面的调试; 暂不支持插件内wx接口调用; 技术选型 minium支持Python 和 JavaScript 版本,而且有专门的团队定期维护,遇到问题可以在微信开发者社区进行提问,因此选择了minium。 Minium框架原理 minium提供一个基于unittest封装好的测试框架,利用这个简单的框架对小程序测试可以起到事半功倍的效果。 测试基类Minitest会根据测试配置进行测试,minitest向上继承了unittest.TestCase,并做了以下改动: 加载读取测试配置 在合适的时机初始化minium.Minium、minium.App和minium.Native 根据配置打开IDE,拉起小程序项目和或自动打开真机调试 拦截assert调用,记录检验结果 记录运行时数据和截图,用于测试报告生成 使用MiniTest可以大大降低小程序测试成本。 Properties 名称类型默认值说明 appminium.AppNoneApp实例,可直接调用minium.App中的方法 miniminium.MiniumNoneMinium实例,可直接调用minium.Minium中的方法 nativeminium.NativeNoneNative实例,可直接调用minium.Native中的方法 环境搭建 搭建Minium Doc 微信技术团队伙伴已告知,无需进行本地搭建,本地搭建的都是错的并且还是旧的文档) 无需构建,直接访问官方文档(https://minitest.weixin.qq.com/#/) 安装软件 下载并安装python 3.8+,地址:https://www.python.org/downloads/release/python-390/ 下载并安装微信开发者工具,地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html 安装minium 下载 https://minitest.weixin.qq.com/minium/Python/dist/minium-latest.zip,然后执行 第一步:解压,解压路径自己记住(我这里解压到 C:\minium-1.2.6) [图片] 第二步:切换到文件目录下 第三步:执行即可 (如下图) [图片] 第四步:测试 [图片] 特别说明 如果新手通过本地解压再执行,遇到以下问题 [图片] 请直接使用以下命令,即可一次性成功安装 pip3 install https://minitest.weixin.qq.com/minium/Python/dist/minium-latest.zip [图片] [图片] 配置微信开发者工具 安装微信开发者工具(我本机使用的版本是1.06.2205312),并打开安全模式: 设置 -> 安全设置 -> 服务端口: 打开 [图片] 在工具栏菜单中点击设置,选择项目设置,切换到“本地设置”,将调试基础库选择大于2.7.3的库; [图片] 开启微信工具安全设置中的 CLI/HTTP (提供了命令行和HTTP两种调用方式)调用功能。 开启被测试项目的自动化端口号 [代码]"path/to/cli" auto --project "path/to/project" --auto-port 9420 [代码] 路径说明 path/to/project: 指代填写存放小程序源码的目录地址,文件夹中需要包含有 project.config.json 文件 [图片] path/to/cli: 指代开发者工具cli命令路径 [图片] 与下图一致证明开启成功 [代码]"C:/Program Files (x86)/Tencent/微信web开发者工具/cli" auto --project "C:/WeChatProjects/miniprogram-1" --auto-port 9420 [代码] [图片] 配置信息 代码结构目录 [图片] 模拟器Config.json [代码]{ "project_path": "C:\\WeChatProjects\\miniprogram-1", "dev_tool_path": "C:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat", "debug_mode": "debug", "test_port": 9420, "platform": "ide", "app": "wx", "assert_capture": true, "request_timeout":60, "remote_connect_timeout": 300, "auto_relaunch": true } [代码] 真机Config.json [代码]{ "project_path": "C:\\WeChatProjects\\xxx_chinamobile-pmc_migration2\\unpackage\\dist\\build\\mp-weixin", "dev_tool_path": "C:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat", "debug_mode": "debug", "test_port": 9420, "platform": "Android", "app": "wx", "enable_app_log": true, "device_desire": { "serial": "d310bf55" }, "assert_capture": true, "request_timeout":60, "remote_connect_timeout": 300, "auto_relaunch": true } [代码] Suite.json [代码]{ "pkg_list": [ { "case_list": [ "test_*" ], "pkg": "testcase.*_test" } ] } [代码] 测试用例 first_test.py [代码]# !/usr/bin/python # -*- coding: utf-8 -*- """ @File : first_test.py @Create Time: 2022-06-01 16:17 @Description: """ import minium class FirstTest(minium.MiniTest): def test_get_system_info(self): sys_info = self.mini.get_system_info() print("FirstTest: ", sys_info) self.assertIn("SDKVersion", sys_info) if __name__ == "__main__": import unittest loaded_suite = unittest.TestLoader().loadTestsFromTestCase(FirstTest) result = unittest.TextTestRunner().run(loaded_suite) print(result) [代码] 主程序入口 run.py [代码]# !/usr/bin/python # -*- coding: utf-8 -*- """ @File : run.py @Create Time: 2022-06-01 17:21 @Description: """ import os # 运行执行class文件中的指定用例 cmd0 = "minitest -m testcase.first_test --case test_get_system_info -c config.json -g" # 运行执行testcase文件中的指定用例 cmd1 = "minitest -m testcase.first_test -c config.json -g" # 按照suite配置执行用例 cmd2 = "minitest -s suite.json -c config.json -g" os.system(cmd0) [代码] 执行用例 [图片] 测试报告 测试结果存储在outputs下,运行命令 [代码]python -m http.server 12345 -d outputs [代码] 然后在浏览器上访问 http://localhost:12345 即可查看报告 [图片] 下载项目源代码 申请GitLab项目代码管理权限,下载小程序源代码 [图片] [图片] [图片] 可选项(注意!此部分内容只适用于我司内部产品) 下载Node.js 地址:http://nodejs.cn/download/current/ [图片] 下载并安装编译软件HBuilderX 下载HBuilderX 地址:https://dcloud.io/hbuilderx.html 安装HBuilderX [图片] [图片] 编译源代码 [图片] 报错的解决方案,进入源代码目录下,执行NPM INSTALL [图片] 编译并发布小程序 因为公司的小程序是通过Vue.js进行编写,发布项目需要先通过HBuilderX工具进行编译。 [图片] [图片] [图片] [图片] [图片] 执行测试脚本 [图片] 输出测试报告 用例执行成功 [图片] 用例执行失败 [图片] [图片] 用例执行异常 [图片] [图片] Nginx简介 Nginx (engine x) 是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP服务器。Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler.ru 站点开发的。 它也是一种轻量级的Web服务器,可以作为独立的服务器部署网站(类似Tomcat)。它高性能和低消耗内存的结构受到很多大公司青睐,如淘宝网站架设。 Nginx配置 Minium框架的Nginx配置 [代码] server { listen 80; server_name your.domain.com; location / { alias /path/to/dir/of/report; index index.html; } } [代码] [图片] Nginx下载 下载Nginx 下载地址:https://nginx.org/en/download.html [图片] Nginx安装部署 下载完成后,解压缩,运行cmd,使用命令进行操作,不要直接双击nginx.exe,不要直接双击nginx.exe,不要直接双击nginx.exe 使用命令到达nginx的加压缩后的目录 [代码]cd C:\nginx-1.22.0 [代码] [图片] 启动nginx服务,启动时会一闪而过是正常的 [代码]start nginx [代码] [图片] 打开任务管理器在进程中看不到nginx.exe的进程(双击nginx.exe时会显示在这里),需要打开详细信息里面能看到隐藏的nginx.exe进程 [图片] 如果都没有可能是启动报错了查看一下日志,在nginx目录中的logs文件夹下error.log是日志文件 [图片] [图片] 修改配置文件,进入解压缩目录,直接文件夹点击进去即可,不需要从dos操作 [图片] 在conf目录下找到nginx.conf使用txt文本打开即可,找到server这个节点,修改端口号,如果有需求可以修改主页目录没有就不用修改 [图片] [图片] 修改完成后保存,使用以下命令检查一下配置文件是否正确,后面是nginx.conf文件的路径,successful就说明正确了 [代码]nginx -t -c /nginx-1.22.0/conf/nginx.conf [代码] [图片] 如果程序没启动就直接start nginx启动,如果已经启动了就使用以下命令重新加载配置文件并重启 [代码]nginx -s reload [代码] [图片] 关闭nginx服务使用以下命令,同样也是一闪而过是正常的,看一下是否进程已消失即可。 [代码]nginx -s stop #强制停止Nginx服务:常用 nginx -s quit #优雅地停止Nginx服务(即处理完所有请求后再停止服务) [代码] Nginx常见命令 [代码]nginx -s reopen #重启Nginx nginx -s reload #重新加载Nginx配置文件,然后以优雅的方式重启Nginx nginx -s stop #强制停止Nginx服务:常用 nginx -s quit #优雅地停止Nginx服务(即处理完所有请求后再停止服务) nginx -t #检测配置文件是否有语法错误,然后退出 nginx -?,-h #打开帮助信息 nginx -v #显示版本信息并退出 nginx -V #显示版本和配置选项信息,然后退出 nginx -t #检测配置文件是否有语法错误,然后退出 nginx -T #检测配置文件是否有语法错误,转储并退出 [代码] 在线查看 打开浏览器,访问 http://ip:port/,在线查看报告是否可以正常展示 [图片] 常见问题 (1)端口号被占用 解决方法:https://blog.csdn.net/qq_32265203/article/details/110088489 (2)nginx文件夹路径含中文 解决方法:中文路径改为英文路径,或者换一个不包含中文的路径 其他错误就详细看log中的描述 解决方法:百度 优化配置 [代码]#user nobody; #==工作进程数,一般设置为cpu核心数 worker_processes 1; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { #==最大连接数,一般设置为cpu*2048 worker_connections 1024; } http { include mime.types; default_type application/octet-stream; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; sendfile on; #tcp_nopush on; #keepalive_timeout 0; #==客户端链接超时时间 keepalive_timeout 65; #gzip on; #当配置多个server节点时,默认server names的缓存区大小就不够了,需要手动设置大一点 server_names_hash_bucket_size 512; #server表示虚拟主机可以理解为一个站点,可以配置多个server节点搭建多个站点 #每一个请求进来确定使用哪个server由server_name确定 server { #站点监听端口 listen 8800; #站点访问域名 server_name localhost; #编码格式,避免url参数乱码 charset utf-8; #access_log logs/host.access.log main; #location用来匹配同一域名下多个URI的访问规则 #比如动态资源如何跳转,静态资源如何跳转等 #location后面跟着的/代表匹配规则 location / { #站点根目录,可以是相对路径,也可以使绝对路径 root html; #默认主页 index index.html index.htm; #转发后端站点地址,一般用于做软负载,轮询后端服务器 #proxy_pass http://10.11.12.237:8080; #拒绝请求,返回403,一般用于某些目录禁止访问 #deny all; #允许请求 #allow all; add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Credentials' 'true'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; #重新定义或者添加发往后端服务器的请求头 #给请求头中添加客户请求主机名 proxy_set_header Host $host; #给请求头中添加客户端IP proxy_set_header X-Real-IP $remote_addr; #将$remote_addr变量值添加在客户端“X-Forwarded-For”请求头的后面,并以逗号分隔。 如果客户端请求未携带“X-Forwarded-For”请求头,$proxy_add_x_forwarded_for变量值将与$remote_addr变量相同 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #给请求头中添加客户端的Cookie proxy_set_header Cookie $http_cookie; #将使用代理服务器的主域名和端口号来替换。如果端口是80,可以不加。 proxy_redirect off; #浏览器对 Cookie 有很多限制,如果 Cookie 的 Domain 部分与当前页面的 Domain 不匹配就无法写入。 #所以如果请求 A 域名,服务器 proxy_pass 到 B 域名,然后 B 服务器输出 Domian=B 的 Cookie, #前端的页面依然停留在 A 域名上,于是浏览器就无法将 Cookie 写入。 #不仅是域名,浏览器对 Path 也有限制。我们经常会 proxy_pass 到目标服务器的某个 Path 下, #不把这个 Path 暴露给浏览器。这时候如果目标服务器的 Cookie 写死了 Path 也会出现 Cookie 无法写入的问题。 #设置“Set-Cookie”响应头中的domain属性的替换文本,其值可以为一个字符串、正则表达式的模式或一个引用的变量 #转发后端服务器如果需要Cookie则需要将cookie domain也进行转换,否则前端域名与后端域名不一致cookie就会无法存取 #配置规则:proxy_cookie_domain serverDomain(后端服务器域) nginxDomain(nginx服务器域) proxy_cookie_domain localhost .testcaigou800.com; #取消当前配置级别的所有proxy_cookie_domain指令 #proxy_cookie_domain off; #与后端服务器建立连接的超时时间。一般不可能大于75秒; proxy_connect_timeout 30; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } #当需要对同一端口监听多个域名时,使用如下配置,端口相同域名不同,server_name也可以使用正则进行配置 #但要注意server过多需要手动扩大server_names_hash_bucket_size缓存区大小 server { listen 80; server_name www.abc.com; charset utf-8; location / { proxy_pass http://localhost:10001; } } server { listen 80; server_name aaa.abc.com; charset utf-8; location / { proxy_pass http://localhost:20002; } } } [代码] 创建数据库和数据表 数据库:XXXXX [图片] 数据表:device [图片] 存储数据信息 [图片] SQL 语句如下 [代码]CREATE TABLE `device` ( `id` int(11) NOT NULL AUTO_INCREMENT, `execid` int(11) NOT NULL, `device_number` int(11) DEFAULT NULL, `project_name` varchar(255) DEFAULT NULL, `case_field` varchar(255) DEFAULT NULL, `case_content` varchar(255) DEFAULT NULL, `triggertime` datetime DEFAULT NULL, `showtime` datetime DEFAULT NULL, `offlinetime` datetime DEFAULT NULL, `starttime` datetime DEFAULT NULL, `endtime` datetime DEFAULT NULL, `status` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=gb18030; [代码] 创建 Flask 服务 新建 db_func.py [代码]# !/usr/bin/python # -*- coding: utf-8 -*- """ @Author : Charles @File : db_func.py @Create Time: 2022-06-21 09:18 @Description: """ import time import pymysql def connect_db(): url = "127.0.0.1" name = "root" pwd = "123456" dataBase = "XXXXX" return pymysql.connect(host=url, port=3306, user=name, passwd=pwd, db=dataBase) def query_database(db, sql): cursor = db.cursor() try: if isinstance(sql, str): cursor.execute(sql) result = list(cursor.fetchall()) else: result = [] for sq in sql: cursor.execute(sq) result.append(list(cursor.fetchall())) except Exception as err: result = ''.join(('An db query exception happened: ', str(err))) # db.close() # 关闭数据库连接 return result def update_db(db, sql): cursor = db.cursor() try: if isinstance(sql, str): cursor.execute(sql) db.commit() else: print('sql 不是一个合格的字符串:{}'.format(sql)) except Exception as err: result = ''.join(('An db update exception happened: ', str(err))) db.rollback() print(result) # 数据库数据插入更新 def db_insert(db, sql): cursor = db.cursor() i = 0 try: cursor.execute(sql) db.commit() result = 'db insert success' except Exception as err: db.rollback() result = 'An db insert exception happened: ' + str(err) + ' ' + str(i) db.close() # 关闭数据库连接 return result def close_db(db): try: db.close() except Exception as err: result = ''.join(('An db closed exception happened: ', str(err))) def get_current_time(): return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) [代码] 新建 project_index.py [代码]# !/usr/bin/python # -*- coding: utf-8 -*- """ @Author : Charles @File : project_index.py @Create Time: 2022-06-21 09:53 @Description: """ import datetime import time from flask import * from common import db_func app = Flask(__name__) @app.route('/') def display(): """ 最新一轮回归测试的整体页面展示 :return: """ db = db_func.connect_db() # 如果实时数据正常显示,就把设备掉线时间默认设置为"1970-01-01 08:00:00" ,实时数据加载时长=数据显示时间-设备被触发时间 # 如果设备显示掉线页面,就把实时数据时间默认设置为"1970-01-01 08:00:00" ,实时数据加载时长=设备掉线时间-设备被触发时间 sql_all = 'SELECT device_number, project_name, case_field, case_content, ' \ '(CASE WHEN offlinetime ="1970-01-01 08:00:00" THEN TIMESTAMPDIFF(SECOND,triggertime,showtime) ' \ 'WHEN showtime ="1970-01-01 08:00:00" THEN TIMESTAMPDIFF(SECOND,triggertime,offlinetime) else 0 ' \ 'END) as duration, triggertime, showtime, offlinetime, starttime, endtime, status FROM device ' \ 'where execid in (select max(execid) from device) ;' # 如果实时数据正常显示,就把设备掉线时间默认设置为"1970-01-01 08:00:00" ,实时数据加载时长=数据显示时间-设备被触发时间 # 如果设备显示掉线页面,就把实时数据时间默认设置为"1970-01-01 08:00:00" ,实时数据加载时长=设备掉线时间-设备被触发时间 sql_fail = 'SELECT device_number, project_name, case_field, case_content, ' \ '(CASE WHEN offlinetime ="1970-01-01 08:00:00" THEN TIMESTAMPDIFF(SECOND,triggertime,showtime) ' \ 'WHEN showtime ="1970-01-01 08:00:00" THEN TIMESTAMPDIFF(SECOND,triggertime,offlinetime) else 0 ' \ 'END) as duration, starttime, endtime, status FROM device ' \ 'where status="fail" and execid in (select max(execid) from device) ;' pass_total_duration = 'SELECT CAST(SUM(TIMESTAMPDIFF(SECOND,triggertime,showtime)) AS CHAR) AS duration FROM device ' \ 'WHERE case_field="当前楼层" AND STATUS="pass" AND TIMESTAMPDIFF(SECOND,triggertime,showtime) <= "10" ' \ 'AND execid IN (SELECT MAX(execid) FROM device) ' fail_total_duration = 'SELECT CAST(SUM(TIMESTAMPDIFF(SECOND,triggertime,offlinetime)) AS CHAR) AS duration FROM device ' \ 'WHERE case_field="当前楼层" AND STATUS="fail" AND TIMESTAMPDIFF(SECOND,triggertime,offlinetime) > "10" ' \ 'AND execid IN (SELECT MAX(execid) FROM device) ' device_total_number = 'SELECT COUNT(*) FROM device WHERE case_field="当前楼层" AND execid IN (SELECT MAX(execid) FROM device) ' device_pass_number = 'SELECT COUNT(*) FROM device WHERE case_field="当前楼层" AND STATUS="pass" AND execid IN (SELECT MAX(execid) FROM device) ' device_fail_number = 'SELECT COUNT(*) FROM device WHERE case_field="当前楼层" AND STATUS="fail" AND execid IN (SELECT MAX(execid) FROM device) ' db_res_all = db_func.query_database(db, sql_all) de_res_fail = db_func.query_database(db, sql_fail) db_pass_total_duration = db_func.query_database(db, pass_total_duration) db_device_total_number = db_func.query_database(db, device_total_number) db_device_pass_number = db_func.query_database(db, device_pass_number) db_device_fail_number = db_func.query_database(db, device_fail_number) # Average Loading Time if db_device_pass_number[0][0] == 0: average_loading_time = "%.2f" % 0 else: average_loading_time = "%.2f" % (int(db_pass_total_duration[0][0]) / int(db_device_pass_number[0][0])) print("Average_loading_time: ", average_loading_time) # Averavge Loading Time Less Than 10s if db_device_pass_number[0][0] == 0: average_less_than_10s = "%.2f" % 0 else: average_less_than_10s = "%.2f" % (int(db_pass_lessthan_total_duration[0][0])/int(db_device_pass_number[0][0])) print("Averavge Loading Time Less Than 10s: ", average_less_than_10s) # Percentage of Loading Time Less Than 10s if db_device_pass_number[0][0] == 0: percentage_less_than_10s = "{:.2%}".format(0) else: percentage_less_than_10s = "{:.2%}".format(int(db_device_lessthan_pass_number[0][0])/int(db_device_pass_number[0][0])) print("Percentage of Loading Time Less Than 10s: ", percentage_less_than_10s) # Percentage of Loading Time Over 10s if db_device_pass_number[0][0] == 0: percentage_over_10s = "{:.2%}".format(0) else: percentage_over_10s = "{:.2%}".format(int(db_device_over_pass_number[0][0])/int(db_device_pass_number[0][0])) print("Percentage of Loading Time Over 10s: ", percentage_over_10s) # 获得SQL语句查询内容 db_res = [average_loading_time, average_less_than_10s, percentage_less_than_10s, percentage_over_10s, db_res_fail, db_res_pass_over_10s, db_res_all] if db_res: return render_template("/koneview_page.html", content=db_res) else: pass if __name__ == "__main__": app.run(host='xx.xx.xx.xx', port=5500, debug=True) [代码] 创建 HMTL 网页 新建 project_page.html [代码]<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>MyProject Monitor Platform</title> <link rel="icon" href="https://www.myproject.cn/zh/Images/myproject-logo-76x52_tcm156-8930.png"> <script src="static/lib/jquery-1.11.1.min.js" type="text/javascript"></script> <div class="navi-brand"> <a class="logo" onclick="zhugeTrack('顶栏按钮点击',{'按钮名称':'LOGO'});" href="https://www.myproject.com/en/" target="_blank"> <img src="/static/images/myprojectLOGO.png" style="text-align: left; width: 265px; height: 50px; position: absolute; left: 62px; top: 30px; margin-left: -60px;margin-top: -20px;"></a> </div> <style type="text/css"> .bg{ background-image: url("https://pic4.zhimg.com/80/v2-052324a9dae7a6feb2e5ddf6f68ad8c6_720w.jpg"); background-repeat: repeat; } em{ color: red; font-style: normal; } table{ border-spacing: 0; width: 100%; border: 1px solid black; } thead th { border: 1px solid black; /*text-align: center;*/ width: auto; color: blueviolet; } th{ border: 1px solid black; text-align: left; width: auto; background-color: #69ABD6; } td{ border: 1px solid black; text-align: left; } </style> </head> <body class="bg"> <table style="margin-bottom: -1px" class="left" id="table_top_header"> <h1 align="center" style="color:#0071B9 ; font-size:30px">MyProject Monitor Platform</h1> <h3 align="left" style="color:#0071B9 ; font-size:20px">Basic Information</h3> <tr> <th>Owner</th> <th>Project Name</th> <th>Running Environment</th> <th>Running System</th> <th>Running Equipment</th> <th>Testing Phase</th> <th>Monitor Frequency</th> <th>Test Report</th> </tr> <tr> <td>Charles</td> <td>MyProject</td> <td>Production</td> <td>Windows 10 64bit</td> <td>Wechat DevTools</td> <td>Regression Test</td> <td>One day/time</td> <td><a style="background: #eee4a6" href="http://ip:port/" target="_blank">Minium</a></td> </tr> </table> <table style="margin-bottom: -1px" class="left" id="table_analysis"> <tbody> <h3 align="left" style="color:#0071B9 ; font-size:20px">LiveData Analysis</h3> <tr> <th>Average Loading Time(sec)</th> <th>Percentage of Loading Time Less Than 10s</th> <th>Percentage of Loading Time Over 10s</th> </tr> <div > <div id="ChangelistTable0"> <tr> <td class="text-center" > <div> {% if content[2] %} <span style="background: #e8e272"> <a>{{content[2]}}</a> </span> {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[3] %} <span style="background: #7ad9f4"> <a>{{content[3]}}</a> </span> {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[4] %} <span style="background: #e999d4"> <a>{{content[4]}}</a> </span> {% else %} {% endif %} </div> </td> </tr> </div> </div> </tbody> </table> <table style="margin-bottom: -1px" class="left" id="table_header"> <tbody> <h3 align="left" style="color:#0071B9 ; font-size:20px">TestCase Failed</h3> <tr> <th>No</th> <th>Device Number</th> <th>Project Name</th> <th>Monitor Field</th> <th>Live Data</th> <th>Duration(s)</th> <th>Latest Case Executed Time</th> <th>Latest Case Ended Time</th> <th>Latest Case Result</th> </tr> <div > <div id="ChangelistTable1"> {% for i in range(content[1]| length) %} <tr> <td> {{ i+1 }} </td> <td class="text-center" > <div> {% if content[1][i][0] %} {{ content[1][i][0]| safe }} {% else %} {% endif %} </div> </td> <td> <div> {% if content[1][i][1] %} {{ content[1][i][1]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[1][i][2] %} {{ content[1][i][2]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[1][i][3] %} {{ content[1][i][3]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[1][i][4] %} {% if content[1][i][4]<=10 %} <span style="background: lightseagreen"> <a>{{content[1][i][4]}}</a> </span> {% else %} <span style="background: lightcoral"> <a>{{content[1][i][4]}}</a></span> {% endif %} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[1][i][5] %} {{ content[1][i][5]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[1][i][6] %} {{ content[1][i][6]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[1][i][7] %} {% if content[1][i][7]=='pass' %} <span style="background: mediumseagreen"> <a>PASS</a> </span> <!-- <span style="background: #50d2c8"> <a href={{content[0][i][6]}} target="_blank">Pass</a> </span>--> {% elif content[1][i][7]=='fail' %} <span style="background: orangered"> <a>FAIL</a> </span> <!-- <span style="background: red"> <a href={{content[0][i][6]}} target="_blank">Fail</a> </span>--> {% else %} <span style="background: sandybrown"> <a>Scheduled</a> </span> {% endif %} {% else %} {% endif %} </div> </td> </tr> {% endfor %} </div> </div> </tbody> </table> <table style="margin-bottom: -1px" class="left" id="table_list"> <tbody> <h3 align="left" style="color:#0071B9 ; font-size:20px">TestCase Description</h3> <tr> <th>No</th> <th>Device Number</th> <th>Project Name</th> <th>Monitor Field</th> <th>Live Data</th> <th>Duration(s)</th> <th>Latest Triggered Time</th> <th>Latest Showed Time</th> <th>Latest Dropped Time</th> <th>Latest Case Executed Time</th> <th>Latest Case Ended Time</th> <th>Latest Case Result</th> </tr> <div > <div id="ChangelistTable"> {% for i in range(content[0]| length) %} <tr> <td> {{ i+1 }} </td> <td class="text-center" > <div>{{ content[0][i][0]| safe }}</div> </td> <td> <div>{{ content[0][i][1]| safe }}</div> </td> <td class="text-center" > <div>{{ content[0][i][2]| safe }}</div> </td> <td class="text-center" > <div> {% if content[0][i][3] %} {{ content[0][i][3]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[0][i][4] %} {% if content[0][i][4]<=10 %} <span style="background: lightseagreen"> <a>{{content[0][i][4]}}</a> </span> {% else %} <span style="background: lightcoral"> <a>{{content[0][i][4]}}</a></span> {% endif %} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[0][i][5] %} {{ content[0][i][5]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[0][i][6] %} {{ content[0][i][6]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[0][i][7] %} {{ content[0][i][7]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[0][i][8] %} {{ content[0][i][8]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[0][i][9] %} {{ content[0][i][9]| safe }} {% else %} {% endif %} </div> </td> <td class="text-center" > <div> {% if content[0][i][10] %} {% if content[0][i][10]=='pass' %} <span style="background: mediumseagreen"> <a>PASS</a> </span> <!-- <span style="background: #50d2c8"> <a href={{content[0][i][6]}} target="_blank">Pass</a> </span>--> {% elif content[0][i][10]=='fail' %} <span style="background: orangered"> <a>FAIL</a> </span> <!-- <span style="background: red"> <a href={{content[0][i][6]}} target="_blank">Fail</a> </span>--> {% else %} <span style="background: sandybrown"> <a>Scheduled</a> </span> {% endif %} {% else %} {% endif %} </div> </td> </tr> {% endfor %} </div> </div> </tbody> </table> </body> </html> [代码] 在线查看报告 打开http://IP:PORT/,在线查看网页报告 [图片] 项目迁移到Jenkins平台 [图片] 创建一个新的Node节点 [图片] 节点名称为Application,标签为Monitoring [图片] 标签为Monitoring下的所有节点列表 [图片] 节点Application上的项目列表 [图片] 节点Monitoring_01上的项目列表 [图片] 测试机上部署Agent服务 将Jenkins平台上自动生成的jar包,放入对应的测试机上 [图片] 输入Jenkins平台上自动生成的命令,进行命令行启动 [代码]java -jar agent.jar -jnlpUrl http://XX.XX.XXX.XXX:8080/computer/Applications/jenkins-agent.jnlp -secret XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -workDir "C:\JenkinsWorkSpace\247" [代码] 验证Agent服务,是否成功连到Jenkins平台上 [图片] Jenkins上进行项目的配置部署 [图片] [图片] [图片] [图片] [图片] 通过Publish HTML reports插件,查看报告 [图片] [图片] 通过http://x.x.x.x:xxxx/ ,查看报告 [图片] 参考资料 https://git.weixin.qq.com/groups/minitest https://blog.csdn.net/jiangjunsss/article/details/120228371 https://blog.csdn.net/baguenaudier/article/details/124478687 https://blog.csdn.net/weixin_49546967/article/details/119858529 https://minitest.weixin.qq.com/#/minium/Python/readme https://developers.weixin.qq.com/community/develop/article/doc/0000cae3a58748ed7f2c8975351413 https://www.cnblogs.com/taiyonghai/p/9402734.html https://www.cnblogs.com/zpcdbky/p/15339213.html https://blog.csdn.net/qq_32265203/article/details/110088489 https://blog.csdn.net/qq_43813373/article/details/123116268
2022-08-22