unittest and pytest¶
- Author
RYefccd
- Date
2019-08-15T09:41:59.179550+08:00
测试¶
单元测试¶
1 2 3 4 5 6 7 | def my_demo_func(a, b):
tmp = []
if a > 6 and b > 9:
tmp.append("F")
else:
tmp.append("T")
print(tmp)
|
语句覆盖(Statement Coverage)¶
executable statements
in the source code at least once. It is used to calculate and
measure the number of statements in the source code which can be executed given the requirements.传入参数 a, b。 观察语句覆盖情况。
假设给定 a=7, b=10, 代码执行覆盖如下:
1 2 3 4 5 6 7
def my_demo_func(a, b): tmp = [] if a > 6 and b > 9: tmp.append("F") else: tmp.append("T") print(tmp)
Statement Coverage: 5/7 = 71%
Unused Statements
Dead Code
Unused Branches
判定覆盖(Decision Coverage)¶
Boolean expression
. In this coverage, expressions can sometimes get complicated. Therefore, it is very hard to achieve 100% coverage.对 a > 6 and b > 9
整个 Boolean expression 构造整体表达式为真或者为假的判定逻辑。
假设给定 a=7, b=10, 代码执行覆盖如下: (
a > 6 and b > 9
is True)1 2 3 4 5 6 7
def my_demo_func(a, b): tmp = [] if a > 6 and b > 9: tmp.append("F") else: tmp.append("T") print(tmp)
Statement Coverage: 5/7 = 71%
假设给定 a=1, b=10, 代码执行覆盖如下:(
a > 6 and b > 9
is False)1 2 3 4 5 6 7
def my_demo_func(a, b): tmp = [] if a > 6 and b > 9: tmp.append("F") else: tmp.append("T") print(tmp)
Statement Coverage: 6/7 = 85%
分支覆盖(Branch Coverage)¶
<table border="1" class="docutils"> <thead> <tr> <th>In the branch coverage, every outcome from a code module is tested. For example, if the outcomes are binary, you need to test both True and False outcomes.</th> </tr> </thead> <tbody> <tr> <td>It helps you to ensure that every possible branch from each decision condition is executed at least a single time.</td> </tr> <tr> <td></td> </tr> <tr> <td>By using Branch coverage method, you can also measure the fraction of independent code segments. It also helps you to find out which is sections of code don't have any branches.</td> </tr> <tr> <td></td> </tr> <tr> <td>The formula to calculate Branch Coverage:</td> </tr> </tbody> </table>
1 2 3 4 5 6 7 8 9 | def my_demo_func(a, b):
tmp = []
if a > 6 and b > 9:
tmp.append("F")
elif: a > 2:
pass
else:
tmp.append("T")
print(tmp)
|
在实际测试中, 分支覆盖是我们最为关注的. 哪些分支没有被覆盖, 是因为什么原因没有被覆盖......
Allows you to validate-all the branches in the code
Helps you to ensure that no branched lead to any abnormality of the program's operation
Branch coverage method removes issues which happen because of statement coverage testing
Allows you to find those areas which are not tested by other testing methods
It allows you to find a quantitative measure of code coverage
Branch coverage ignores branches inside the Boolean expressions
条件覆盖(Condition Coverage)¶
对于 a > 6 and b > 9
整个 Boolean expression, 我们有两个条件 a > 6 和 b > 9.
test |
a > 6 |
b > 9 |
---|---|---|
a=3, b=3 |
F |
F |
a=3, b=13 |
F |
T |
a=9, b=3 |
T |
F |
a=9, b=13 |
T |
T |
条件覆盖
pytest¶
方便的 assert 语句(不需要记忆各种 self.assert* 断言函数)
自动发现测试模块和测试函数
模块化的 fixture, 可以更加容易组织测试结构。
兼容 unittest 测试用例, 无缝对接原有测试用例。
example¶
(server18) ryefccd@fccd:~/workspace/pytest_demo$ tree -L 2
.
├── myproject
│ ├── handler.py
│ ├── __init__.py
│ ├── mathexample.py
│ └── __pycache__
├── requirement_dev.txt
└── tests
├── __init__.py
├── __pycache__
├── test_math_opration.py
└── test_tornado_client.py
4 directories, 7 files
普通模块¶
功能代码
1 2 3 4 5 6 7 8 9 10 11 12 13
''' Created on 2019年8月15日 @author: ryefccd ''' def add_two(num1, num2): return num1 + num2 def sub_two(num1, num2): return num1 - num2
功能测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
''' Created on 2019年8月15日 @author: ryefccd ''' import pytest from myproject import mathexample @pytest.fixture(scope='module') def resource_a_setup(request): print('\nresources_a_setup()') def resource_a_teardown(): print('\nresources_a_teardown()') request.addfinalizer(resource_a_teardown) # 可以在这里回收数据库连接 return 1234567 def test_add_two(resource_a_setup): add = mathexample.add_two(resource_a_setup, 2) assert add == 1234567 + 2 def test_sub_two(): substract = mathexample.sub_two(1, 2) assert substract == -1 print("fccdny") # @pytest.mark.skip(msg='failure') def test_add_two_failure(): add = mathexample.add_two(1, 2) assert add == 4 if __name__ == '__main__': pass
执行测试
(server18) ryefccd@fccd:~/workspace/pytest_demo$ pytest tests/test_math_opration.py Test session starts (platform: linux, Python 3.5.2, pytest 5.0.1, pytest-sugar 0.9.2) rootdir: /home/ryefccd/workspace/pytest_demo plugins: sugar-0.9.2, metadata-1.8.0, allure-pytest-2.7.1, xdist-1.29.0, cov-2.7.1, forked-1.0.2, tornado-0.8.0, html-1.20.0 collecting ... tests/test_math_opration.py ✓✓ 67% ██████▋ ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― test_add_two_failure ――――――――――――――――――― def test_add_two_failure(): add = mathexample.add_two(1, 2) > assert add == 4 E assert 3 == 4 tests/test_math_opration.py:35: AssertionError tests/test_math_opration.py ⨯ 100% ██████████ Results (0.12s): 2 passed 1 failed - tests/test_math_opration.py:33 test_add_two_failure
web 框架¶
依赖 pytest-tornado
pip install pytest-tornado
功能代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
''' Created on 2018年8月19日 @author: ryefccd ''' import json import asyncio import aioredis import tornado.web from myproject.mathexample import add_two, sub_two SERVER_REDIS_ADDRESS = ['192.168.1.200', 6379] class MainHandler(tornado.web.RequestHandler): def get(self): a = int(self.get_argument("a", "6")) b = int(self.get_argument("b", "2")) num_sum = add_two(a, b) num_delta = sub_two(a, b) res = {"sum": num_sum, "delta": num_delta, "a": a, "b": b} self.write(json.dumps(res)) application = tornado.web.Application([ (r"/", MainHandler), ]) def init_app(ioloop, application): redis_conn = std_loop.run_until_complete( aioredis.create_redis_pool(SERVER_REDIS_ADDRESS, db=0)) application.settings["REDIS_CONN"] = redis_conn if __name__ == '__main__': std_loop = asyncio.get_event_loop() init_app(std_loop, application) http_server = tornado.httpserver.HTTPServer(application) http_server.listen(8989) std_loop.run_forever()
功能测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
''' Created on 2019年8月15日 @author: ryefccd ''' import json import pytest from myproject import handler @pytest.fixture def app(): return handler.application @pytest.fixture(scope="function") def db_init(io_loop, app): std_ioloop = io_loop.asyncio_loop handler.init_app(io_loop, app) # 填充数据库数据 yield # 清楚数据库数据 conn = app.settings["REDIS_CONN"] conn.close() std_ioloop.run_until_complete(conn.wait_closed()) @pytest.mark.gen_test def test_tornao_request_success(http_client, base_url): url = base_url + "?a=7&b=2" print("url:", url) print("base_url:", base_url) print("http_client:", http_client) response = yield from http_client.fetch(url) assert response.code == 200 @pytest.mark.gen_test def test_tornao_request_fail(http_client, base_url): url = base_url + "?a=7&b=2" print("url:", url) print("base_url:", base_url) print("http_client:", http_client) response = yield from http_client.fetch(url) res = json.loads(response.body.decode()) print(res) assert response.code == 200 assert res["sum"] == 10 if __name__ == '__main__': pass
执行测试
(server18) ryefccd@fccd:~/workspace/pytest_demo$ pytest tests/test_tornado_client.py Test session starts (platform: linux, Python 3.5.2, pytest 5.0.1, pytest-sugar 0.9.2) rootdir: /home/ryefccd/workspace/pytest_demo plugins: sugar-0.9.2, metadata-1.8.0, allure-pytest-2.7.1, xdist-1.29.0, cov-2.7.1, forked-1.0.2, tornado-0.8.0, html-1.20.0 collecting ... tests/test_tornado_client.py ✓ 50% █████ ―――――――――――――――――――――――――――――――――――――――――――――――――― test_tornao_request_fail ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― http_client = <tornado.simple_httpclient.SimpleAsyncHTTPClient object at 0x7f1393bfd358>, base_url = 'http://localhost:34575' @pytest.mark.gen_test def test_tornao_request_fail(http_client, base_url): url = base_url + "?a=7&b=2" print("url:", url) print("base_url:", base_url) print("http_client:", http_client) response = yield from http_client.fetch(url) res = json.loads(response.body.decode()) print(res) assert response.code == 200 > assert res["sum"] == 10 E assert 9 == 10 tests/test_tornado_client.py:49: AssertionError --------------------------------------------------------------------- Captured stdout call -------------------------------------------------------------------------- url: http://localhost:34575?a=7&b=2 base_url: http://localhost:34575 http_client: <tornado.simple_httpclient.SimpleAsyncHTTPClient object at 0x7f1393bfd358> {'a': 7, 'delta': 5, 'sum': 9, 'b': 2} tests/test_tornado_client.py ⨯ 100% ██████████ Results (0.16s): 1 passed 1 failed - tests/test_tornado_client.py:39 test_tornao_request_fail
pytest 使用技巧¶
pytest --help # 查看帮助
- -v
详细的输出信息
- -s
不捕获标准输出(测试用例中的 print 会打印出来)
- -l
当用例错误时, 打印测试函数内局部变量信息
- -k EXPRESSION
执行用例包含"EXPRESSION"的用例
- -x, --exitfirst
当遇到错误时停止测试(当维护很多测试用例时, 最迫切需要的功能)
- --lf, --last-failed
跑上一次错误的测试用例
- --ff, --failed-first
跑所有的用例, 但是优先上一次错误的用例
- --pdb
错误的测试用例陷入 pdb 调试环境
参考资料: pytest introduction