├── .gitattributes ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── demo ├── __init__.py ├── debug_case.py ├── report │ ├── 2019-03-13-18-46-13-style-1.html │ └── 2019-03-13-18-46-13-style-2.html ├── run.py ├── testcase │ ├── __init__.py │ ├── battle │ │ ├── __init__.py │ │ └── test_tattle.py │ ├── chat │ │ ├── __init__.py │ │ └── test_chat.py │ └── legion │ │ ├── __init__.py │ │ └── test_legion.py └── use_report.py ├── img ├── print_info.png ├── style1.png └── style2.png ├── requirements.txt ├── setup.py └── utx ├── __init__.py ├── core.py ├── log.py ├── report ├── __init__.py ├── style_1.py └── style_2.py ├── runner.py ├── setting.py └── tag.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-language=python -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | #Ipython Notebook 63 | .ipynb_checkpoints 64 | 65 | # pyenv 66 | .python-version 67 | 68 | # pycharm 69 | .idea/ 70 | 71 | # 本地测试 72 | local_test/ 73 | wxglade/ 74 | kivy-2014/ 75 | designer/ 76 | youtube_dl_gui/ 77 | 78 | 79 | # 本地配置 80 | local_config.py 81 | 82 | *.bak 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2017] [jianbing.g] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include requirements.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # utx 2 | 3 | > 支持Python3.6及以上版本 4 | 5 | utx扩展了Python unittest框架的功能,起因是需要控制测试用例的执行顺序,而unittest的默认执行顺序是按照用例函数的名称进行排序,所以想要做一个可以无缝接入unittest的扩展功能库。 6 | 7 | ## 当前版本 8 | 9 | ```python 10 | V0.0.8 11 | ``` 12 | 13 | ## 安装 14 | 15 | ```python 16 | python setup.py install 17 | ``` 18 | 19 | ## 更新 20 | 21 | ```python 22 | pip uninstall utx # 需要先卸载旧的utx 23 | python setup.py install 24 | ``` 25 | 26 | 27 | 28 | ## 功能列表 29 | 30 | 31 | - 用例排序,只需要导入utx库,用例的执行顺序就会和编写顺序一致 32 | 33 | - 用例自定义标签,在 tag.py 里边添加标签,可以对测试用例指定多个标签。全部用例默认带有`ALL`标签。 34 | 35 | ```python 36 | @unique 37 | class Tag(Enum): 38 | ALL = NewTag("完整") # 完整测试标记,可以重命名,不要删除 39 | SMOKE = NewTag("冒烟") # 冒烟测试标记 40 | 41 | # 以下开始为扩展标签,自行调整 42 | V1_0_0 = NewTag("V1.0.0版本") 43 | V2_0_0 = NewTag("V2.0.0版本") 44 | ``` 45 | 46 | ```python 47 | class TestLegion(unittest.TestCase): 48 | 49 | @tag(Tag.SMOKE) 50 | def test_create_legion(self): 51 | pass 52 | 53 | @tag(Tag.V1_0_0, Tag.ALL) 54 | def test_quit_legion(self): 55 | """测试退出军团 56 | 57 | :return: 58 | """ 59 | print("测试退出军团") 60 | assert 1 == 2 61 | ``` 62 | 63 | - 运行指定标签的测试用例 64 | 65 | ```python 66 | from utx import * 67 | 68 | if __name__ == '__main__': 69 | setting.run_case = {Tag.ALL} # 运行全部测试用例 70 | # setting.run_case = {Tag.SMOKE} # 只运行SMOKE标记的测试用例 71 | # setting.run_case = {Tag.SMOKE, Tag.V1_0_0} # 只运行SMOKE和V1_0_0标记的测试用例 72 | 73 | runner = TestRunner() 74 | runner.add_case_dir(r"testcase") 75 | runner.run_test(report_title='接口自动化测试报告') 76 | ``` 77 | 78 | - 数据驱动 79 | ```python 80 | class TestLegion(unittest.TestCase): 81 | 82 | @data(["gold", 100], ["diamond", 500]) 83 | def test_bless(self, bless_type, cost): 84 | """测试公会祈福 85 | 86 | :param bless_type: 祈福类型 87 | :param cost: 消耗数量 88 | :return: 89 | """ 90 | print(bless_type) 91 | print(cost) 92 | 93 | @data(10001, 10002, 10003) 94 | def test_receive_bless_box(self, box_id): 95 | """ 测试领取祈福宝箱 96 | 97 | :return: 98 | """ 99 | print(box_id) 100 | 101 | # 默认会解包测试数据来一一对应函数参数,可以使用unpack=False,不进行解包 102 | 103 | class TestBattle(unittest.TestCase): 104 | @data({"gold": 1000, "diamond": 100}, {"gold": 2000, "diamond": 200}, unpack=False) 105 | def test_get_battle_reward(self, reward): 106 | """ 测试领取战斗奖励 107 | 108 | :return: 109 | """ 110 | print(reward) 111 | print("获得的钻石数量是:{}".format(reward['diamond'])) 112 | ``` 113 | 114 | - 检测用例是否编写了用例描述 115 | ```python 116 | 2017-11-13 12:00:19,334 WARNING legion.test_legion.test_bless没有用例描述 117 | ``` 118 | 119 | - 采集测试用例的print打印的信息到测试报告里边 120 | ```python 121 | @data({"gold": 1000, "diamond": 100}, {"gold": 2000, "diamond": 200}, unpack=False) 122 | def test_get_battle_reward(self, reward): 123 | """ 领取战斗奖励 124 | 125 | :return: 126 | """ 127 | print(reward) 128 | print("获得的钻石数量是:{}".format(reward['diamond'])) 129 | ``` 130 | 131 | ![](https://github.com/jianbing/utx/raw/master/img/print_info.png) 132 | 133 | 134 | - 执行测试时,显示测试进度 135 | ``` 136 | 2019-03-13 18:46:13,810 INFO 开始测试,用例数量总共15个,跳过5个,实际运行10个 137 | 2019-03-13 18:46:13,910 INFO start to test battle.test_tattle.test_start_battle (1/10) 138 | 2019-03-13 18:46:14,010 INFO start to test battle.test_tattle.test_skill_buff (2/10) 139 | 2019-03-13 18:46:14,111 INFO start to test battle.test_tattle.test_normal_attack (3/10) 140 | 2019-03-13 18:46:14,211 INFO start to test battle.test_tattle.test_get_battle_reward (4/10) 141 | 2019-03-13 18:46:14,211 DEBUG 测试领取战斗奖励,获得的钻石数量是:100 142 | 2019-03-13 18:46:14,311 INFO start to test battle.test_tattle.test_get_battle_reward (5/10) 143 | ``` 144 | 145 | - setting类提供多个设置选项进行配置 146 | ```python 147 | class setting: 148 | 149 | # 只运行的用例类型 150 | run_case = {Tag.SMOKE} 151 | 152 | # 开启用例排序 153 | sort_case = True 154 | 155 | # 每个用例的执行间隔,单位是秒 156 | execute_interval = 0.1 157 | 158 | # 开启检测用例描述 159 | check_case_doc = True 160 | 161 | # 显示完整用例名字(函数名字+参数信息) 162 | full_case_name = False 163 | 164 | # 测试报告显示的用例名字最大程度 165 | max_case_name_len = 80 166 | 167 | # 执行用例的时候,显示报错信息 168 | show_error_traceback = True 169 | 170 | # 测试报告样式1 171 | create_report_by_style_1 = True 172 | 173 | # 测试报告样式2 174 | create_report_by_style_2 = True 175 | 176 | # 在控制台显示print打印的内容 177 | show_print_in_console = False 178 | ``` 179 | 180 | 181 | - 集成两种测试报告样式,感谢两位作者的测试报告模版 182 | 183 | [测试报告1](https://github.com/findyou/HTMLTestRunnerCN) 184 | 185 | ![](https://github.com/jianbing/utx/raw/master/img/style1.png) 186 | 187 | [测试报告2](https://github.com/zhangfei19841004/ztest) 188 | 189 | ![](https://github.com/jianbing/utx/raw/master/img/style2.png) 190 | 191 | - 无缝接入unittest项目,导入utx包即可开始使用扩展功能,无需修改之前的代码 192 | 193 | 194 | ## 例子 195 | 196 | demo目录下,有几个例子: 197 | 198 | - ```run.py``` 一个完整使用utx功能的demo小项目 199 | 200 | - ```use_report.py``` 单独使用测试报告组件,不需要utx的其他扩展功能 201 | 202 | - ```debug_case.py``` 使用utx后,单独调试某个用例的例子 -------------------------------------------------------------------------------- /demo/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo/debug_case.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | import utx 5 | from demo.testcase.battle.test_tattle import TestBattle 6 | 7 | utx.run_case(TestBattle, "test_get_battle_reward") 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo/report/2019-03-13-18-46-13-style-1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 接口自动化测试报告 7 | 8 | 9 | 10 | 11 | 12 | 70 | 71 | 72 | 163 | 164 | 165 | 166 | 167 |
168 |

接口自动化测试报告

169 |

开始时间 : 2019-03-13 18:46:13

170 |

合计耗时 : 0:00:01.003057

171 |

测试结果 : 共 15,通过 8,失败 1,错误 1,跳过 5,通过率= 53.33%

172 | 173 |

174 |
175 | 176 | 177 |

178 | 概要{ 53.33% } 179 | 通过{ 8 } 180 | 失败{ 1 } 181 | 错误{ 1 } 182 | 所有{ 15 } 183 |

184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 220 | 228 | 229 | 230 | 231 | 234 | 242 | 243 | 244 | 245 | 248 | 256 | 257 | 258 | 259 | 262 | 271 | 272 | 273 | 274 | 277 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 318 | 319 | 320 | 321 | 324 | 327 | 328 | 329 | 330 | 333 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 352 | 361 | 362 | 363 | 364 | 367 | 378 | 379 | 380 | 381 | 384 | 395 | 396 | 397 | 398 | 401 | 404 | 405 | 406 | 407 | 410 | 413 | 414 | 415 | 416 | 419 | 422 | 423 | 424 | 425 | 428 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 |
用例集/测试用例总计通过失败错误跳过详细
battle.test_tattle.TestBattle55000详细
218 |
test_00001_start_battle(测试开始战斗)
219 |
221 | 222 | 223 |
224 |
测试开始战斗
225 | 
226 |
227 |
232 |
test_00002_skill_buff(测试技能buff)
233 |
235 | 236 | 237 |
238 |
测试技能buff
239 | 
240 |
241 |
246 |
test_00003_normal_attack(测试普通攻击)
247 |
249 | 250 | 251 |
252 |
测试普通攻击
253 | 
254 |
255 |
260 |
test_00004_get_battle_reward_00001_1000_100(测试领取战斗奖励)
261 |
263 | 264 | 265 |
266 |
{'gold': 1000, 'diamond': 100}
267 | 测试领取战斗奖励,获得的钻石数量是:100
268 | 
269 |
270 |
275 |
test_00005_get_battle_reward_00002_2000_200(测试领取战斗奖励)
276 |
278 | 279 | 280 |
281 |
{'gold': 2000, 'diamond': 200}
282 | 测试领取战斗奖励,获得的钻石数量是:200
283 | 
284 |
285 |
chat.test_chat.TestChat30012详细
test_00006_chat_in_world_channel(测试世界聊天)
301 | 304 | 305 | 306 | 307 |
308 |
测试世界聊天
309 | Traceback (most recent call last):
310 |   File "D:\github\utx\utx\core.py", line 101, in wrap
311 |     result = func(*args, **kwargs)
312 |   File "D:\github\utx\demo\testcase\chat\test_chat.py", line 15, in test_chat_in_world_channel
313 |     raise Exception("运行报错了")
314 | Exception: 运行报错了
315 | 
316 |
317 |
322 |
test_00007_chat_in_personal_channel(测试私聊)
323 |
325 | 跳过 326 |
331 |
test_00008_chat_in_union_channel(测试公会聊天)
332 |
334 | 跳过 335 |
legion.test_legion.TestLegion73103详细
350 |
test_00009_create_legion(测试创建军团)
351 |
353 | 354 | 355 |
356 |
运行setUp方法
357 | 运行tearDown方法
358 | 
359 |
360 |
365 |
test_00010_bless_00001_gold_100(测试公会祈福)
366 |
368 | 369 | 370 |
371 |
运行setUp方法
372 | gold
373 | 100
374 | 运行tearDown方法
375 | 
376 |
377 |
382 |
test_00011_bless_00002_diamond_500(测试公会祈福)
383 |
385 | 386 | 387 |
388 |
运行setUp方法
389 | diamond
390 | 500
391 | 运行tearDown方法
392 | 
393 |
394 |
399 |
test_00012_receive_bless_box_00001_10001(测试领取祈福宝箱)
400 |
402 | 跳过 403 |
408 |
test_00013_receive_bless_box_00002_10002(测试领取祈福宝箱)
409 |
411 | 跳过 412 |
417 |
test_00014_receive_bless_box_00003_10003(测试领取祈福宝箱)
418 |
420 | 跳过 421 |
426 |
test_00015_quit_legion(测试退出军团)
427 |
429 | 432 | 433 | 434 | 435 |
436 |
运行setUp方法
437 | 测试退出军团
438 | 运行tearDown方法
439 | Traceback (most recent call last):
440 |   File "D:\github\utx\utx\core.py", line 101, in wrap
441 |     result = func(*args, **kwargs)
442 |   File "D:\github\utx\demo\testcase\legion\test_legion.py", line 50, in test_quit_legion
443 |     assert 1 == 2
444 | AssertionError
445 | 
446 |
447 |
总计158115通过率:53.33%
461 | 462 |
 
463 |
464 |
466 | 467 | 468 | 469 | 470 | -------------------------------------------------------------------------------- /demo/run.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | from utx import * 5 | import logging 6 | 7 | if __name__ == '__main__': 8 | setting.run_case = {Tag.ALL} # 运行全部测试用例 9 | # setting.run_case = {Tag.SMOKE} # 只运行SMOKE标记的测试用例 10 | # setting.run_case = {Tag.SMOKE, Tag.V1_0_0} # 只运行SMOKE和V1_0_0标记的测试用例 11 | 12 | setting.check_case_doc = False # 关闭检测是否编写了测试用例描述 13 | setting.full_case_name = True 14 | setting.max_case_name_len = 80 # 测试报告内,显示用例名字的最大程度 15 | setting.show_error_traceback = True # 执行用例的时候,显示报错信息 16 | setting.sort_case = True # 是否按照编写顺序,对用例进行排序 17 | setting.create_report_by_style_1 = True # 测试报告样式1 18 | setting.create_report_by_style_2 = True # 测试报告样式2 19 | setting.show_print_in_console = True 20 | 21 | log.set_level(logging.DEBUG) # 设置utx的log级别 22 | # log.set_level_to_debug() # 设置log级别的另外一种方法 23 | 24 | runner = TestRunner() 25 | runner.add_case_dir(r"testcase") 26 | runner.run_test(report_title='接口自动化测试报告') 27 | -------------------------------------------------------------------------------- /demo/testcase/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | 5 | if __name__ == '__main__': 6 | pass 7 | 8 | -------------------------------------------------------------------------------- /demo/testcase/battle/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | Created by jianbing on 2017-10-27 5 | """ -------------------------------------------------------------------------------- /demo/testcase/battle/test_tattle.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | import unittest 5 | from utx import * 6 | 7 | 8 | class TestBattle(unittest.TestCase): 9 | def test_start_battle(self): 10 | """测试开始战斗 11 | 12 | :return: 13 | """ 14 | print("测试开始战斗") 15 | 16 | def test_skill_buff(self): 17 | """测试技能buff 18 | 19 | :return: 20 | """ 21 | print("测试技能buff") 22 | 23 | @tag(Tag.V1_0_0) 24 | def test_normal_attack(self): 25 | """测试普通攻击 26 | 27 | :return: 28 | """ 29 | print("测试普通攻击") 30 | 31 | @data({"gold": 1000, "diamond": 100}, {"gold": 2000, "diamond": 200}, unpack=False) 32 | def test_get_battle_reward(self, reward): 33 | """ 测试领取战斗奖励 34 | 35 | :return: 36 | """ 37 | print(reward) 38 | print("测试领取战斗奖励,获得的钻石数量是:{}".format(reward['diamond'])) 39 | log.debug("测试领取战斗奖励,获得的钻石数量是:{}".format(reward['diamond'])) 40 | -------------------------------------------------------------------------------- /demo/testcase/chat/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | Created by jianbing on 2017-10-27 5 | """ -------------------------------------------------------------------------------- /demo/testcase/chat/test_chat.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | import unittest 5 | import utx 6 | 7 | 8 | class TestChat(unittest.TestCase): 9 | def test_chat_in_world_channel(self): 10 | """测试世界聊天 11 | 12 | :return: 13 | """ 14 | print("测试世界聊天") 15 | raise Exception("运行报错了") 16 | 17 | @unittest.skip("跳过此用例") 18 | def test_chat_in_personal_channel(self): 19 | """测试私聊 20 | 21 | :return: 22 | """ 23 | print("测试私聊") 24 | 25 | @utx.skip("跳过此用例") 26 | def test_chat_in_union_channel(self): 27 | """测试公会聊天 28 | 29 | :return: 30 | """ 31 | print("测试公会聊天") 32 | -------------------------------------------------------------------------------- /demo/testcase/legion/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | Created by jianbing on 2017-10-27 5 | """ -------------------------------------------------------------------------------- /demo/testcase/legion/test_legion.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | import unittest 5 | from utx import * 6 | 7 | 8 | class TestLegion(unittest.TestCase): 9 | def setUp(self): 10 | print('运行setUp方法') 11 | 12 | def tearDown(self): 13 | print('运行tearDown方法') 14 | 15 | @tag(Tag.SMOKE) 16 | def test_create_legion(self): 17 | """测试创建军团 18 | 19 | :return: 20 | """ 21 | 22 | @tag(Tag.ALL) 23 | @data(["gold", 100], ["diamond", 500]) 24 | def test_bless(self, bless_type, cost): 25 | """测试公会祈福 26 | 27 | :param bless_type: 祈福类型 28 | :param cost: 消耗数量 29 | :return: 30 | """ 31 | print(bless_type) 32 | print(cost) 33 | 34 | @skip("跳过的原因") 35 | @data(10001, 10002, 10003) 36 | def test_receive_bless_box(self, box_id): 37 | """ 测试领取祈福宝箱 38 | 39 | :return: 40 | """ 41 | print(box_id) 42 | 43 | @tag(Tag.V1_0_0, Tag.ALL) 44 | def test_quit_legion(self): 45 | """测试退出军团 46 | 47 | :return: 48 | """ 49 | print("测试退出军团") 50 | assert 1 == 2 51 | -------------------------------------------------------------------------------- /demo/use_report.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | 单独使用测试报告组件,不需要utx的其他扩展功能 5 | """ 6 | import utx 7 | 8 | if __name__ == '__main__': 9 | 10 | utx.stop_patch() 11 | 12 | runner = utx.TestRunner() 13 | runner.add_case_dir(r"testcase\chat") 14 | runner.run_test(report_title='接口自动化测试报告') -------------------------------------------------------------------------------- /img/print_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianbing/utx/43498227162707bc5d3460d8435540033082dfcf/img/print_info.png -------------------------------------------------------------------------------- /img/style1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianbing/utx/43498227162707bc5d3460d8435540033082dfcf/img/style1.png -------------------------------------------------------------------------------- /img/style2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianbing/utx/43498227162707bc5d3460d8435540033082dfcf/img/style2.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colorama 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import io 5 | import os 6 | import sys 7 | from shutil import rmtree 8 | from setuptools import setup, Command, find_packages 9 | 10 | NAME = 'utx' 11 | DESCRIPTION = '对Python unittest的功能进行了扩展' 12 | URL = 'https://github.com/jianbing/utx' 13 | EMAIL = '326333381@qq.com' 14 | AUTHOR = 'jianbing' 15 | VERSION = '0.0.7' 16 | REQUIRED = [ 17 | 'colorama' 18 | ] 19 | 20 | here = os.path.abspath(os.path.dirname(__file__)) 21 | 22 | with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: 23 | long_description = '\n' + f.read() 24 | 25 | 26 | class UploadCommand(Command): 27 | """Support setup.py upload.""" 28 | 29 | description = 'Build and publish the package.' 30 | user_options = [] 31 | 32 | @staticmethod 33 | def status(s): 34 | """Prints things in bold.""" 35 | print('\033[1m{0}\033[0m'.format(s)) 36 | 37 | def initialize_options(self): 38 | pass 39 | 40 | def finalize_options(self): 41 | pass 42 | 43 | def run(self): 44 | try: 45 | self.status('Removing previous builds…') 46 | rmtree(os.path.join(here, 'dist')) 47 | except OSError: 48 | pass 49 | 50 | self.status('Building Source and Wheel (universal) distribution…') 51 | os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) 52 | 53 | self.status('Uploading the package to PyPi via Twine…') 54 | os.system('twine upload dist/*') 55 | 56 | sys.exit() 57 | 58 | 59 | setup( 60 | name=NAME, 61 | version=VERSION, 62 | description=DESCRIPTION, 63 | long_description=long_description, 64 | author=AUTHOR, 65 | author_email=EMAIL, 66 | url=URL, 67 | python_requires='>=3.6.0', 68 | packages=find_packages(), 69 | install_requires=REQUIRED, 70 | include_package_data=True, 71 | license='MIT', 72 | classifiers=[ 73 | 'License :: OSI Approved :: MIT License', 74 | 'Programming Language :: Python', 75 | 'Programming Language :: Python :: 3.6', 76 | 'Programming Language :: Python :: 3.7', 77 | 'Programming Language :: Python :: 3.8', 78 | 'Programming Language :: Python :: Implementation :: CPython', 79 | ], 80 | cmdclass={ 81 | 'upload': UploadCommand, 82 | }, 83 | ) 84 | -------------------------------------------------------------------------------- /utx/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | from utx.tag import Tag 4 | from utx import log 5 | from utx.setting import setting 6 | from utx.core import * 7 | from utx.runner import TestRunner 8 | -------------------------------------------------------------------------------- /utx/core.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | import re 4 | import functools 5 | import time 6 | import unittest 7 | from .setting import setting 8 | from . import log 9 | from .tag import Tag 10 | 11 | CASE_TAG_FLAG = "__case_tag__" 12 | CASE_DATA_FLAG = "__case_data__" 13 | CASE_DATA_UNPACK_FLAG = "__case_data_unpack__" 14 | CASE_ID_FLAG = "__case_id__" 15 | CASE_INFO_FLAG = "__case_info__" 16 | CASE_RUN_INDEX_FlAG = "__case_run_index_flag__" 17 | CASE_SKIP_FLAG = "__unittest_skip__" 18 | CASE_SKIP_REASON_FLAG = "__unittest_skip_why__" 19 | 20 | __all__ = ["skip", "skip_if", "data", "tag", "stop_patch", "run_case"] 21 | 22 | 23 | def skip(reason): 24 | def wrap(func): 25 | return unittest.skip(reason)(func) 26 | 27 | return wrap 28 | 29 | 30 | def skip_if(condition, reason): 31 | def wrap(func): 32 | return unittest.skipIf(condition, reason)(func) 33 | 34 | return wrap 35 | 36 | 37 | def data(*values, unpack=True): 38 | """注入测试数据,可以做为测试用例的数据驱动 39 | 1. 单一参数的测试用例 40 | @data(10001, 10002, 10003) 41 | def test_receive_bless_box(self, box_id): 42 | print(box_id) 43 | 44 | 2. 多个参数的测试用例 45 | @data(["gold", 100], ["diamond", 500]) 46 | def test_bless(self, bless_type, award): 47 | print(bless_type) 48 | print(award) 49 | 50 | 3. 是否对测试数据进行解包 51 | @data({"gold": 1000, "diamond": 100}, {"gold": 2000, "diamond": 200}, unpack=False) 52 | def test_get_battle_reward(self, reward): 53 | print(reward) 54 | print("获得的钻石数量是:{}".format(reward['diamond'])) 55 | 56 | :param values:测试数据 57 | :param unpack: 是否解包 58 | :return: 59 | """ 60 | 61 | def wrap(func): 62 | if hasattr(func, CASE_DATA_FLAG): 63 | log.error("{}的测试数据只能初始化一次".format(func.__name__)) 64 | else: 65 | setattr(func, CASE_DATA_FLAG, values) 66 | setattr(func, CASE_DATA_UNPACK_FLAG, unpack) 67 | return func 68 | 69 | return wrap 70 | 71 | 72 | def tag(*tag_type): 73 | """指定测试用例的标签,可以作为测试用例分组使用,用例默认会有Tag.ALL标签,支持同时设定多个标签,如: 74 | @tag(Tag.V1_0_0, Tag.SMOKE) 75 | def test_func(self): 76 | pass 77 | 78 | :param tag_type:标签类型,在tag.py里边自定义 79 | :return: 80 | """ 81 | 82 | def wrap(func): 83 | if not hasattr(func, CASE_TAG_FLAG): 84 | tags = {Tag.ALL} 85 | tags.update(tag_type) 86 | setattr(func, CASE_TAG_FLAG, tags) 87 | else: 88 | getattr(func, CASE_TAG_FLAG).update(tag_type) 89 | return func 90 | 91 | return wrap 92 | 93 | 94 | def _handler(func): 95 | @functools.wraps(func) 96 | def wrap(*args, **kwargs): 97 | time.sleep(setting.execute_interval) 98 | msg = "start to test {} ({}/{})".format(getattr(func, CASE_INFO_FLAG), 99 | getattr(func, CASE_RUN_INDEX_FlAG), 100 | Tool.actual_case_num) 101 | log.info(msg) 102 | result = func(*args, **kwargs) 103 | return result 104 | 105 | return wrap 106 | 107 | 108 | class Tool: 109 | actual_case_num = 0 110 | total_case_num = 0 111 | 112 | @classmethod 113 | def create_case_id(cls): 114 | cls.total_case_num += 1 115 | return cls.total_case_num 116 | 117 | @classmethod 118 | def create_actual_run_index(cls): 119 | cls.actual_case_num += 1 120 | return cls.actual_case_num 121 | 122 | @staticmethod 123 | def modify_func_name(func): 124 | """修改函数名字,实现排序 eg test_fight ---> test_00001_fight 125 | 126 | :param func: 127 | :return: 128 | """ 129 | case_id = Tool.create_case_id() 130 | setattr(func, CASE_ID_FLAG, case_id) 131 | if setting.sort_case: 132 | func_name = func.__name__.replace("test_", "test_{:05d}_".format(case_id)) 133 | else: 134 | func_name = func.__name__ 135 | return func_name 136 | 137 | @staticmethod 138 | def general_case_name_with_test_data(func_name, index, test_data): 139 | if setting.full_case_name: 140 | params_str = "_".join([str(_) for _ in test_data]).replace(".", "") 141 | func_name += "_{:05d}_{}".format(index, params_str) 142 | else: 143 | func_name += "_{:05d}".format(index) 144 | if len(func_name) > setting.max_case_name_len: 145 | func_name = func_name[:setting.max_case_name_len] + "……" 146 | return func_name 147 | 148 | @staticmethod 149 | def create_case_with_case_data(func): 150 | result = dict() 151 | for index, test_data in enumerate(getattr(func, CASE_DATA_FLAG), 1): 152 | if not hasattr(func, CASE_SKIP_FLAG): 153 | setattr(func, CASE_RUN_INDEX_FlAG, Tool.create_actual_run_index()) 154 | 155 | func_name = Tool.modify_func_name(func) 156 | if isinstance(test_data, list): 157 | func_name = Tool.general_case_name_with_test_data(func_name, index, test_data) 158 | if getattr(func, CASE_DATA_UNPACK_FLAG, None): 159 | result[func_name] = _handler(_feed_data(*test_data)(func)) 160 | else: 161 | result[func_name] = _handler(_feed_data(test_data)(func)) 162 | 163 | elif isinstance(test_data, dict): 164 | func_name = Tool.general_case_name_with_test_data(func_name, index, test_data.values()) 165 | if getattr(func, CASE_DATA_UNPACK_FLAG, None): 166 | result[func_name] = _handler(_feed_data(**test_data)(func)) 167 | else: 168 | result[func_name] = _handler(_feed_data(test_data)(func)) 169 | 170 | elif isinstance(test_data, (int, str, bool, float)): 171 | func_name = Tool.general_case_name_with_test_data(func_name, index, [test_data]) 172 | result[func_name] = _handler(_feed_data(test_data)(func)) 173 | 174 | else: 175 | raise Exception("无法解析{}".format(test_data)) 176 | 177 | return result 178 | 179 | @staticmethod 180 | def create_case_without_case_data(func): 181 | if not hasattr(func, CASE_SKIP_FLAG): 182 | setattr(func, CASE_RUN_INDEX_FlAG, Tool.create_actual_run_index()) 183 | 184 | result = dict() 185 | func_name = Tool.modify_func_name(func) 186 | if len(func_name) > setting.max_case_name_len: 187 | func_name = func_name[:setting.max_case_name_len] + "……" 188 | result[func_name] = _handler(func) 189 | return result 190 | 191 | @staticmethod 192 | def filter_test_case(funcs_dict): 193 | funcs = dict() 194 | cases = dict() 195 | for i in funcs_dict: 196 | if i.startswith("test_"): 197 | cases[i] = funcs_dict[i] 198 | else: 199 | funcs[i] = funcs_dict[i] 200 | 201 | return funcs, cases 202 | 203 | 204 | def _feed_data(*args, **kwargs): 205 | def wrap(func): 206 | @functools.wraps(func) 207 | def _wrap(self): 208 | return func(self, *args, **kwargs) 209 | 210 | return _wrap 211 | 212 | return wrap 213 | 214 | 215 | class Meta(type): 216 | def __new__(cls, clsname, bases, attrs): 217 | funcs, cases = Tool.filter_test_case(attrs) 218 | for test_case in cases.values(): 219 | if not hasattr(test_case, CASE_TAG_FLAG): 220 | setattr(test_case, CASE_TAG_FLAG, {Tag.ALL}) # 没有指定tag的用例,默认带有tag:ALL 221 | 222 | # 注入用例信息 223 | case_info = "{}.{}".format(test_case.__module__, test_case.__name__) 224 | setattr(test_case, CASE_INFO_FLAG, case_info) 225 | 226 | # 检查用例描述 227 | if setting.check_case_doc and not test_case.__doc__: 228 | log.warn("{}没有用例描述".format(case_info)) 229 | 230 | # 过滤不执行的用例 231 | if not getattr(test_case, CASE_TAG_FLAG) & set(setting.run_case): 232 | continue 233 | 234 | # 注入测试数据 235 | if hasattr(test_case, CASE_DATA_FLAG): 236 | funcs.update(Tool.create_case_with_case_data(test_case)) 237 | else: 238 | funcs.update(Tool.create_case_without_case_data(test_case)) 239 | 240 | return super(Meta, cls).__new__(cls, clsname, bases, funcs) 241 | 242 | 243 | class _TestCase(unittest.TestCase, metaclass=Meta): 244 | def shortDescription(self): 245 | """覆盖父类的方法,获取函数的注释 246 | 247 | :return: 248 | """ 249 | doc = self._testMethodDoc 250 | doc = doc and doc.split()[0].strip() or None 251 | return doc 252 | 253 | 254 | TestCaseBackup = unittest.TestCase 255 | unittest.TestCase = _TestCase 256 | 257 | 258 | def stop_patch(): 259 | unittest.TestCase = TestCaseBackup 260 | 261 | 262 | def run_case(case_class, case_name: str): 263 | setting.execute_interval = 0.3 264 | r = re.compile(case_name.replace("test_", "test(_\d+)?_")) 265 | suite = unittest.TestSuite() 266 | for i in unittest.TestLoader().loadTestsFromTestCase(case_class): 267 | if r.match(getattr(i, "_testMethodName")): 268 | suite.addTest(i) 269 | unittest.TextTestRunner(verbosity=0).run(suite) 270 | -------------------------------------------------------------------------------- /utx/log.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | Created by jianbing on 2017-10-30 5 | """ 6 | import sys 7 | import logging.handlers 8 | from colorama import Fore, Style 9 | 10 | _logger = logging.getLogger('utx') 11 | _logger.setLevel(logging.DEBUG) 12 | _handler = logging.StreamHandler(sys.stdout) 13 | _handler.setFormatter(logging.Formatter('%(asctime)s %(message)s')) 14 | _logger.addHandler(_handler) 15 | 16 | 17 | def debug(msg): 18 | _logger.debug("DEBUG " + str(msg)) 19 | 20 | 21 | def info(msg): 22 | _logger.info(Fore.GREEN + "INFO " + str(msg) + Style.RESET_ALL) 23 | 24 | 25 | def error(msg): 26 | _logger.error(Fore.RED + "ERROR " + str(msg) + Style.RESET_ALL) 27 | 28 | 29 | def warn(msg): 30 | _logger.warning(Fore.YELLOW + "WARNING " + str(msg) + Style.RESET_ALL) 31 | 32 | 33 | def _print(msg): 34 | _logger.debug(Fore.BLUE + "PRINT " + str(msg) + Style.RESET_ALL) 35 | 36 | 37 | def set_level(level): 38 | """ 设置log级别 39 | 40 | :param level: logging.DEBUG, logging.INFO, logging.WARN, logging.ERROR 41 | :return: 42 | """ 43 | _logger.setLevel(level) 44 | 45 | 46 | def set_level_to_debug(): 47 | _logger.setLevel(logging.DEBUG) 48 | 49 | 50 | def set_level_to_info(): 51 | _logger.setLevel(logging.INFO) 52 | 53 | 54 | def set_level_to_warn(): 55 | _logger.setLevel(logging.WARN) 56 | 57 | 58 | def set_level_to_error(): 59 | _logger.setLevel(logging.ERROR) 60 | -------------------------------------------------------------------------------- /utx/report/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | Created by Administrator on 2019-03-12 5 | """ -------------------------------------------------------------------------------- /utx/report/style_1.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | html_head = r""" 5 | 6 | 7 | 8 | 9 | {report_name} 10 | 11 | 12 | 13 | 14 | 15 | 73 | 74 | 75 | 166 | """ 167 | 168 | html_body = """ 169 | 170 | 171 | 172 |
173 |

{report_name}

174 |

开始时间 : {begin_time}

175 |

合计耗时 : {total_time}

176 |

测试结果 : 共 {test_all},通过 {test_pass},失败 {test_fail},错误 {test_error},跳过 {test_skip},通过率= {test_pass_per}

177 | 178 |

179 |
180 | 181 | 182 |

183 | 概要{{ {test_pass_per} }} 184 | 通过{{ {test_pass} }} 185 | 失败{{ {test_fail} }} 186 | 错误{{ {test_error} }} 187 | 所有{{ {test_all} }} 188 |

189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | {case_info} 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 |
用例集/测试用例总计通过失败错误跳过详细
总计{test_all}{test_pass}{test_fail}{test_error}{test_skip}通过率:{test_pass_per}
222 | 223 |
 
224 |
225 |
227 | 228 | 229 | 230 | 231 | """ 232 | 233 | html_case_group = """ 234 | 235 | {group_name} 236 | {test_all} 237 | {test_pass} 238 | {test_fail} 239 | {test_error} 240 | {test_skip} 241 | 详细 242 | 243 | """ 244 | 245 | html_case_skip = """ 246 | 247 | 248 |
{method_name}
249 | 250 | 251 | 跳过 252 | 253 | 254 | """ 255 | 256 | html_case_pass_no_log = """ 257 | 258 | 259 |
{method_name}
260 | 261 | 262 | 通过 263 | 264 | 265 | """ 266 | 267 | html_case_pass_with_log = """ 268 | 269 | 270 |
{method_name}
271 | 272 | 273 | 274 | 275 |
276 |
{case_log}
277 |
278 | 279 | 280 | """ 281 | 282 | html_case_fail = """ 283 | 284 | 285 |
{method_name}
286 | 287 | 288 | 291 | 292 | 293 | 294 |
295 |
{case_log}
296 |
297 | 298 | 299 | """ 300 | 301 | html_case_error = """ 302 | 303 |
{method_name}
304 | 305 | 308 | 309 | 310 | 311 |
312 |
{case_log}
313 |
314 | 315 | 316 | """ 317 | 318 | 319 | def build_report(file_path, data): 320 | html = html_head.format(report_name=data['reportName']) 321 | 322 | case_info = "" 323 | from collections import defaultdict 324 | filter_case = defaultdict(list) 325 | for i in data['testResult']: 326 | filter_case[i["className"]].append(i) 327 | 328 | for group_index, group in enumerate(filter_case.keys(), 1): 329 | pass_num = len([i for i in filter_case[group] if i['status'] == '成功']) 330 | fail_num = len([i for i in filter_case[group] if i['status'] == '失败']) 331 | error_num = len([i for i in filter_case[group] if i['status'] == '错误']) 332 | skip_num = len([i for i in filter_case[group] if i['status'] == '跳过']) 333 | case_info += html_case_group.format(group_name=group, 334 | group_index=group_index, 335 | test_all=pass_num + fail_num + error_num + skip_num, 336 | test_pass=pass_num, 337 | test_fail=fail_num, 338 | test_error=error_num, 339 | test_skip=skip_num 340 | ) 341 | cases = filter_case[group] 342 | for case_index, case in enumerate(cases, 1): 343 | if case['status'] == '成功': 344 | if case['log']: 345 | case_info += html_case_pass_with_log.format(case_id="{}_{}".format(group_index, case_index), 346 | method_name="{}({})".format(case['methodName'], 347 | case['description']), 348 | case_log=case['log']) 349 | else: 350 | case_info += html_case_pass_no_log.format(case_id="{}_{}".format(group_index, case_index), 351 | method_name="{}({})".format(case['methodName'], 352 | case['description']), 353 | ) 354 | elif case['status'] == '失败': 355 | case_info += html_case_fail.format(case_id="{}_{}".format(group_index, case_index), 356 | method_name="{}({})".format(case['methodName'], 357 | case['description']), 358 | case_log=case['log']) 359 | elif case['status'] == '错误': 360 | case_info += html_case_error.format(case_id="{}_{}".format(group_index, case_index), 361 | method_name="{}({})".format(case['methodName'], 362 | case['description']), 363 | case_log=case['log']) 364 | elif case['status'] == '跳过': 365 | case_info += html_case_skip.format(case_id="{}_{}".format(group_index, case_index), 366 | method_name="{}({})".format(case['methodName'], 367 | case['description']), 368 | ) 369 | 370 | html += html_body.format(begin_time=data['beginTime'], 371 | total_time=data['totalTime'], 372 | test_all=data['testAll'], 373 | test_pass=data['testPass'], 374 | test_fail=data['testFail'], 375 | test_skip=data['testSkip'], 376 | test_error=data['testError'], 377 | test_pass_per="{:.2%}".format(data['testPass'] / data['testAll']), 378 | report_name=data['reportName'], 379 | case_info=case_info, 380 | ) 381 | 382 | with open(file_path, 'w', encoding='utf-8') as f: 383 | f.write(html) 384 | -------------------------------------------------------------------------------- /utx/runner.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | import time 4 | import os 5 | import io 6 | import datetime 7 | import sys 8 | import unittest 9 | from utx.core import Tool 10 | from . import log, setting 11 | 12 | result_data = dict() 13 | result_data['testResult'] = [] 14 | current_class_name = "" 15 | STATUS = { 16 | 0: 'Pass', 17 | 1: 'Fail', 18 | 2: 'Error', 19 | 3: 'Skip', 20 | } 21 | 22 | 23 | class _TestResult(unittest.TestResult): 24 | def __init__(self, verbosity=1): 25 | super().__init__(verbosity) 26 | self.outputBuffer = io.StringIO() 27 | self.raw_stdout = None 28 | self.raw_stderr = None 29 | self.success_count = 0 30 | self.failure_count = 0 31 | self.skip_count = 0 32 | self.error_count = 0 33 | self.verbosity = verbosity 34 | self.result = [] 35 | self._case_start_time = 0 36 | self._case_run_time = 0 37 | 38 | def startTest(self, test): 39 | self._case_start_time = time.time() 40 | super().startTest(test) 41 | self.raw_stdout = sys.stdout 42 | self.raw_stderr = sys.stderr 43 | sys.stdout = self.outputBuffer 44 | sys.stderr = self.outputBuffer 45 | 46 | def complete_output(self): 47 | self._case_run_time = time.time() - self._case_start_time 48 | if self.raw_stdout: 49 | sys.stdout = self.raw_stdout 50 | sys.stderr = self.raw_stderr 51 | 52 | result = self.outputBuffer.getvalue() 53 | self.outputBuffer.seek(0) 54 | self.outputBuffer.truncate() 55 | if result and setting.show_print_in_console: 56 | log._print(result.strip()) 57 | return result 58 | 59 | def stopTest(self, test): 60 | self.complete_output() 61 | 62 | def addSuccess(self, test): 63 | self.success_count += 1 64 | super().addSuccess(test) 65 | output = self.complete_output() 66 | self.result.append((0, test, output, '', self._case_run_time)) 67 | 68 | def addError(self, test, err): 69 | self.error_count += 1 70 | super().addError(test, err) 71 | _, _exc_str = self.errors[-1] 72 | output = self.complete_output() 73 | self.result.append((2, test, output, _exc_str, self._case_run_time)) 74 | log.error('TestCase Error') 75 | if setting.show_error_traceback: 76 | log.error(_exc_str) 77 | 78 | def addSkip(self, test, reason): 79 | self.skip_count += 1 80 | super().addSkip(test, reason) 81 | self.result.append((3, test, "", "", 0.0)) 82 | 83 | def addFailure(self, test, err): 84 | self.failure_count += 1 85 | super().addFailure(test, err) 86 | _, _exc_str = self.failures[-1] 87 | output = self.complete_output() 88 | self.result.append((1, test, output, _exc_str, self._case_run_time)) 89 | log.error('TestCase Failed') 90 | if setting.show_error_traceback: 91 | log.error(_exc_str) 92 | 93 | 94 | class _TestRunner: 95 | def __init__(self, report_title, report_dir, verbosity=1, description=""): 96 | self.report_dir = report_dir 97 | self.verbosity = verbosity 98 | self.title = report_title 99 | self.description = description 100 | self.start_time = datetime.datetime.now() 101 | self.stop_time = None 102 | 103 | def run(self, test): 104 | msg = "开始测试,用例数量总共{}个,跳过{}个,实际运行{}个" 105 | log.info(msg.format(Tool.total_case_num, 106 | Tool.total_case_num - Tool.actual_case_num, 107 | Tool.actual_case_num)) 108 | result = _TestResult(self.verbosity) 109 | test(result) 110 | self.stop_time = datetime.datetime.now() 111 | self.analyze_test_result(result) 112 | log.info('Time Elapsed: {}'.format(self.stop_time - self.start_time)) 113 | 114 | from utx.report import style_1, style_2 115 | if setting.create_report_by_style_1: 116 | file_path = os.path.join(self.report_dir, 117 | r"{}-style-1.html".format(self.start_time.strftime("%Y-%m-%d-%H-%M-%S"))) 118 | style_1.build_report(file_path, result_data) 119 | 120 | if setting.create_report_by_style_2: 121 | file_path = os.path.join(self.report_dir, 122 | r"{}-style-2.html".format(self.start_time.strftime("%Y-%m-%d-%H-%M-%S"))) 123 | style_2.build_report(file_path, result_data) 124 | 125 | @staticmethod 126 | def sort_result(case_results): 127 | rmap = {} 128 | classes = [] 129 | for n, t, o, e, run_time in case_results: 130 | cls = t.__class__ 131 | if cls not in rmap: 132 | rmap[cls] = [] 133 | classes.append(cls) 134 | rmap[cls].append((n, t, o, e, run_time)) 135 | r = [(cls, rmap[cls]) for cls in classes] 136 | return r 137 | 138 | def analyze_test_result(self, result): 139 | result_data["reportName"] = self.title 140 | result_data["beginTime"] = str(self.start_time)[:19] 141 | result_data["totalTime"] = str(self.stop_time - self.start_time) 142 | 143 | sorted_result = self.sort_result(result.result) 144 | for cid, (cls, cls_results) in enumerate(sorted_result): 145 | pass_num = fail_num = error_num = skip_num = 0 146 | for case_state, *_ in cls_results: 147 | if case_state == 0: 148 | pass_num += 1 149 | elif case_state == 1: 150 | fail_num += 1 151 | elif case_state == 2: 152 | error_num += 1 153 | else: 154 | skip_num += 1 155 | 156 | name = "{}.{}".format(cls.__module__, cls.__name__) 157 | global current_class_name 158 | current_class_name = name 159 | 160 | for tid, (state_id, t, o, e, run_time) in enumerate(cls_results): 161 | 162 | name = t.id().split('.')[-1] 163 | doc = t.shortDescription() or "" 164 | case_data = dict() 165 | case_data['className'] = current_class_name 166 | case_data['methodName'] = name 167 | case_data['spendTime'] = "{:.2}S".format(run_time) 168 | case_data['description'] = doc 169 | case_data['log'] = o + e 170 | if STATUS[state_id] == "Pass": 171 | case_data['status'] = "成功" 172 | if STATUS[state_id] == "Fail": 173 | case_data['status'] = "失败" 174 | if STATUS[state_id] == "Error": 175 | case_data['status'] = "错误" 176 | if STATUS[state_id] == "Skip": 177 | case_data['status'] = "跳过" 178 | result_data['testResult'].append(case_data) 179 | 180 | result_data["testPass"] = result.success_count 181 | result_data["testAll"] = result.success_count + result.failure_count + result.error_count + result.skip_count 182 | result_data["testFail"] = result.failure_count 183 | result_data["testSkip"] = result.skip_count 184 | result_data["testError"] = result.error_count 185 | 186 | 187 | class TestRunner: 188 | 189 | def __init__(self): 190 | self.case_dirs = [] 191 | 192 | def add_case_dir(self, dir_path): 193 | """添加测试用例文件夹,多次调用可以添加多个文件夹,会按照文件夹的添加顺序执行用例 194 | 195 | runner = TestRunner() 196 | runner.add_case_dir(r"testcase\chat") 197 | runner.add_case_dir(r"testcase\battle") 198 | runner.run_test(report_title='接口自动化测试报告') 199 | 200 | :param dir_path: 201 | :return: 202 | """ 203 | if not os.path.exists(dir_path): 204 | raise Exception("测试用例文件夹不存在:{}".format(dir_path)) 205 | if dir_path in self.case_dirs: 206 | log.warn("测试用例文件夹已经存在了:{}".format(dir_path)) 207 | else: 208 | self.case_dirs.append(dir_path) 209 | 210 | def run_test(self, report_title='接口自动化测试报告'): 211 | 212 | if not self.case_dirs: 213 | raise Exception("请先调用add_case_dir方法,添加测试用例文件夹") 214 | 215 | if not os.path.exists("report"): 216 | os.mkdir("report") 217 | 218 | report_dir = os.path.abspath("report") 219 | suite = unittest.TestSuite() 220 | for case_path in self.case_dirs: 221 | suite.addTests(unittest.TestLoader().discover(case_path)) 222 | _TestRunner(report_dir=report_dir, report_title=report_title).run(suite) 223 | 224 | print("测试完成,请查看报告") 225 | os.system("start report") 226 | -------------------------------------------------------------------------------- /utx/setting.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | Created by jianbing on 2017-11-04 5 | """ 6 | from utx.tag import Tag 7 | 8 | 9 | class setting: 10 | # 只运行的用例类型 11 | run_case = {Tag.ALL} 12 | 13 | # 开启用例排序 14 | sort_case = True 15 | 16 | # 每个用例的执行间隔,单位是秒 17 | execute_interval = 0.1 18 | 19 | # 开启检测用例描述 20 | check_case_doc = True 21 | 22 | # 显示完整用例名字(函数名字+参数信息) 23 | full_case_name = False 24 | 25 | # 测试报告显示的用例名字最大程度 26 | max_case_name_len = 80 27 | 28 | # 执行用例的时候,显示报错信息 29 | show_error_traceback = True 30 | 31 | # 测试报告样式1 32 | create_report_by_style_1 = True 33 | 34 | # 测试报告样式2 35 | create_report_by_style_2 = True 36 | 37 | # 在控制台显示print打印的内容 38 | show_print_in_console = False 39 | -------------------------------------------------------------------------------- /utx/tag.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | 测试用例标签类 5 | """ 6 | 7 | from enum import Enum, unique 8 | 9 | 10 | class NewTag: 11 | def __init__(self, desc=""): 12 | self.desc = desc 13 | 14 | 15 | @unique 16 | class Tag(Enum): 17 | SMOKE = NewTag("冒烟") # 冒烟测试标记,可以重命名,不要删除 18 | ALL = NewTag("完整") # 完整测试标记,可以重命名,不要删除 19 | 20 | # 以下开始为扩展标签,自行调整 21 | V1_0_0 = NewTag("V1.0.0版本") 22 | V2_0_0 = NewTag("V2.0.0版本") 23 | 24 | --------------------------------------------------------------------------------