├── .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 | 
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 | 
186 |
187 | [测试报告2](https://github.com/zhangfei19841004/ztest)
188 |
189 | 
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 |
203 |
204 |
205 |
206 |
207 | battle.test_tattle.TestBattle |
208 | 5 |
209 | 5 |
210 | 0 |
211 | 0 |
212 | 0 |
213 | 详细 |
214 |
215 |
216 |
217 |
218 | test_00001_start_battle(测试开始战斗)
219 | |
220 |
221 |
222 |
223 |
227 | |
228 |
229 |
230 |
231 |
232 | test_00002_skill_buff(测试技能buff)
233 | |
234 |
235 |
236 |
237 |
238 | 测试技能buff
239 |
240 |
241 | |
242 |
243 |
244 |
245 |
246 | test_00003_normal_attack(测试普通攻击)
247 | |
248 |
249 |
250 |
251 |
255 | |
256 |
257 |
258 |
259 |
260 | test_00004_get_battle_reward_00001_1000_100(测试领取战斗奖励)
261 | |
262 |
263 |
264 |
265 |
266 | {'gold': 1000, 'diamond': 100}
267 | 测试领取战斗奖励,获得的钻石数量是:100
268 |
269 |
270 | |
271 |
272 |
273 |
274 |
275 | test_00005_get_battle_reward_00002_2000_200(测试领取战斗奖励)
276 | |
277 |
278 |
279 |
280 |
281 | {'gold': 2000, 'diamond': 200}
282 | 测试领取战斗奖励,获得的钻石数量是:200
283 |
284 |
285 | |
286 |
287 |
288 |
289 | chat.test_chat.TestChat |
290 | 3 |
291 | 0 |
292 | 0 |
293 | 1 |
294 | 2 |
295 | 详细 |
296 |
297 |
298 |
299 | test_00006_chat_in_world_channel(测试世界聊天) |
300 |
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 | |
318 |
319 |
320 |
321 |
322 | test_00007_chat_in_personal_channel(测试私聊)
323 | |
324 |
325 | 跳过
326 | |
327 |
328 |
329 |
330 |
331 | test_00008_chat_in_union_channel(测试公会聊天)
332 | |
333 |
334 | 跳过
335 | |
336 |
337 |
338 |
339 | legion.test_legion.TestLegion |
340 | 7 |
341 | 3 |
342 | 1 |
343 | 0 |
344 | 3 |
345 | 详细 |
346 |
347 |
348 |
349 |
350 | test_00009_create_legion(测试创建军团)
351 | |
352 |
353 |
354 |
355 |
356 | 运行setUp方法
357 | 运行tearDown方法
358 |
359 |
360 | |
361 |
362 |
363 |
364 |
365 | test_00010_bless_00001_gold_100(测试公会祈福)
366 | |
367 |
368 |
369 |
370 |
371 | 运行setUp方法
372 | gold
373 | 100
374 | 运行tearDown方法
375 |
376 |
377 | |
378 |
379 |
380 |
381 |
382 | test_00011_bless_00002_diamond_500(测试公会祈福)
383 | |
384 |
385 |
386 |
387 |
388 | 运行setUp方法
389 | diamond
390 | 500
391 | 运行tearDown方法
392 |
393 |
394 | |
395 |
396 |
397 |
398 |
399 | test_00012_receive_bless_box_00001_10001(测试领取祈福宝箱)
400 | |
401 |
402 | 跳过
403 | |
404 |
405 |
406 |
407 |
408 | test_00013_receive_bless_box_00002_10002(测试领取祈福宝箱)
409 | |
410 |
411 | 跳过
412 | |
413 |
414 |
415 |
416 |
417 | test_00014_receive_bless_box_00003_10003(测试领取祈福宝箱)
418 | |
419 |
420 | 跳过
421 | |
422 |
423 |
424 |
425 |
426 | test_00015_quit_legion(测试退出军团)
427 | |
428 |
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 | |
448 |
449 |
450 |
451 |
452 | 总计 |
453 | 15 |
454 | 8 |
455 | 1 |
456 | 1 |
457 | 5 |
458 | 通过率:53.33% |
459 |
460 |
461 |
462 |
463 |
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 |
208 |
209 |
210 | {case_info}
211 |
212 |
213 | 总计 |
214 | {test_all} |
215 | {test_pass} |
216 | {test_fail} |
217 | {test_error} |
218 | {test_skip} |
219 | 通过率:{test_pass_per} |
220 |
221 |
222 |
223 |
224 |
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 |
278 | |
279 |
280 | """
281 |
282 | html_case_fail = """
283 |
284 |
285 | {method_name}
286 | |
287 |
288 |
291 |
292 |
293 |
294 |
297 | |
298 |
299 | """
300 |
301 | html_case_error = """
302 |
303 | {method_name} |
304 |
305 |
308 |
309 |
310 |
311 |
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 |
--------------------------------------------------------------------------------