pytest学习0.0
1. easy start
直接运行pytest
# content of test_sample.py
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 5
$ pytest
========== test session starts ========
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 1 item
test_sample.py F [100%]
============= FAILURES =============
___________ test_answer _____________
def test_answer():
> assert inc(3) == 5
E assert 4 == 5
E + where 4 = inc(3)
test_sample.py:6: AssertionError
========== 1 failed in 0.12 seconds ==========
运行多个文件
在没有定制的默认情况下的缺省规则
- pytest会找当前以及递查找子文件夹下面所有的
test_*.py
或*_test.py
的文件,把其当作测试文件 - 在这些文件里,pytest会收集下面的一些函数或方法,当作测试用例
- 不在类定义中的以
test_
开头的函数或方法 - 在以
Test
开头的类中(不能包含__init__
方法),以test_
开头的方法
- 不在类定义中的以
- pytest也支持unittest模式的用例定义
2. Assert
Assert就是断言,每个测试用例都需要断言。
与unittest不同,pytest使用的是python自带的assert关键字来进行断言,大大降低了学习成本。
assert关键字后面可以接一个表达式,只要表达式的最终结果为True,那么断言通过,用例执行成功,否则用例执行失败。
3. Fixture
可以把Fixture理解为准备测试数据和初始化测试对象的阶段。
一般我们对测试数据和测试对象的管理有这样的一些场景
- 所有用例开始之前初始化测试数据或对象
- 所有用例结束之后销毁测试数据或对象
- 每个用例开始之前初始化测试数据或对象
- 每个用例结束之后销毁测试数据或对象
- 在每个/所有module的用例开始之前初始化数据或对象
- 在每个/所有module的用例开始之后销毁数据或对象
- …
举例1
# content of ./test_smtpsimple.py
import pytest
@pytest.fixture
def smtp_connection():
import smtplib
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert 0 # for demo purposes
这里test_ehlo需要smtp_connection 的fixture值,pytest将会发现并且调用@pytest.fixture
标志着smtp_connection fixture函数。
$ pytest test_smtpsimple.py
============ test session starts ===============
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 1 item
test_smtpsimple.py F [100%]
=========== FAILURES =================
___________ test_ehlo ________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
> assert 0 # for demo purposes
E assert 0
test_smtpsimple.py:11: AssertionError
======== 1 failed in 0.12 seconds ========
举例2
考虑一场景,需要判断用户的密码中包含简单密码,密码必须至少6位,满足6位后判断用户的密码不是password123或者password之类的弱密码。
我们将用户的信息导出成名为users.dev.json的文件,该文件如下所示
[
{"name":"jack","password":"Iloverose"},
{"name":"rose","password":"Ilovejack"},
{"name":"tom","password":"password123"},
]
# test_user_password.py
import pytest
import json
class TestUserPassword(object):
@pytest.fixture
def users(self):
return json.loads(open('./users.dev.json', 'r').read()) # 读取当前路径下的users.dev.json文件,返回的结果一个列表,其中是一个个dict
# [{'name': 'jack', 'password': 'Iloverose'}, {'name': 'rose', 'password': 'Ilovejack'}, {'name': 'tom', 'password': 'password123'}, {'name': 'mike', 'password': 'password'}, {'name': 'james', 'password': 'AGoodPasswordWordShouldBeLongEnough'}]
# <class 'list'>
def test_user_password(self, users):
# 遍历每条user数据
for user in users:
passwd = user['password']
assert len(passwd) >= 6
msg = "user %s has a weak password" %(user['name'])
assert passwd != 'password', msg
assert passwd != 'password123', msg
运行
pytest可以通过指定文件名的方式运行单个用例文件,这里我们只运行test_user_password.py文件
pytest test_user_password.py
运行结果
$ pytest test_user_password.py
============= test session starts ================
platform darwin -- Python 2.7.12, pytest-3.2.3, py-1.4.34, pluggy-0.4.0
rootdir: /Users/easonhan/code/testclass.net/src/pytest, inifile:
collected 1 item
test_user_password.py F
================ FAILURES ================
________________ TestUserPassword.test_user_password __________
self = <test_user_password.TestUserPassword object at 0x1046e3290>
users = [{'name': 'jack', 'password': 'Iloverose'}, {'name': 'rose', 'password': 'Ilovejack'}, {'name': 'tom', 'password': 'password123'}]
def test_user_password(self, users):
for user in users:
passwd = user['password']
assert len(passwd) >= 6
msg = "user %s has a weak password" %(user['name'])
assert passwd != 'password', msg
> assert passwd != 'password123', msg
E AssertionError: user tom has a weak password
E assert 'password123' != 'password123'
test_user_password.py:14: AssertionError
=============== 1 failed in 0.03 seconds ============
分析
- 使用@pytest.fixture装饰器可以定义feature
- 在用例的参数中传递fixture的名称以便直接调用fixture,拿到fixture的返回值
- 3个assert是递进关系,前1个assert断言失败后,后面的assert是不会运行的,因此重要的assert放到前面
-
E AssertionError: user tom has a weak password
可以很容易的判断出是哪条数据出了问题,所以定制可读性好的错误信息是很必要的 - 任何1个断言失败以后,for循环就会退出,所以上面的用例1次只能发现1条错误数据,换句话说任何1个assert失败后,用例就终止运行了
执行顺序
pytest是这样运行上面的用例的
- pytest找到以test_开头的方法,也就是
test_user_password
方法,执行该方法时发现传入的参数里有跟fixture users名称相同的参数 - pytest认定users是fixture,执行该fixture,读取json文件解析成dict实例
- test_user_password方法真正被执行,users fixture被传入到该方法
举例3
例2中任何1条测试数据导致断言不通过后测试用例就会停止运行,这样每次只能检查出1条不符合规范的数据,很多时候需要一次性把所有的不符合结果都测出来。
users.test.json文件:
[
{"name":"jack","password":"Iloverose"},
{"name":"rose","password":"Ilovejack"}
{"name":"tom","password":"password123"},
{"name":"mike","password":"password"},
{"name":"james","password":"AGoodPasswordWordShouldBeLongEnough"}
]
参数化fixture
参数化fixture允许我们向fixture提供参数,参数可以是list,该list中有几条数据,fixture就会运行几次,相应的测试用例也会运行几次。
参数化fixture的语法是
@pytest.fixture(params=["smtp.gmail.com", "mail.python.org"])
其中len(params)
的值就是用例执行的次数
在fixture的定义中,可以使用request.param
来获取每次传入的参数,如下:
@pytest.fixture(scope="module",
params=["smtp.gmail.com", "mail.python.org"])
def smtp(request):
smtp = smtplib.SMTP(request.param, 587, timeout=5)
yield smtp
print ("finalizing %s" % smtp)
smtp.close()
- 上面的代码smtp fixture会执行2次
- 第1次
request.param == 'smtp.gmail.com'
- 第2次
request.param == 'mail.python.org'
实现用例
我们现在使用参数化fixtures来实现一次性检查出弱密码的用例。
# test_user_password_with_params.py
import pytest
import json
users = json.loads(open('./users.test.json', 'r').read()) # list
class TestUserPasswordWithParam(object):
@pytest.fixture(params=users)
def user(self, request):
return request.param
def test_user_password(self, user):
passwd = user['password']
assert len(passwd) >= 6
msg = "user %s has a weak password" %(user['name'])
assert passwd != 'password', msg
assert passwd != 'password123', msg
上面的例子里,我们先把所有用户信息读到users变量里,注意users这时候是list类型,可以直接传入到fixture的params
运行
pytest test_user_password_with_params.py
结果
$ pytest test_user_password_with_params.py
================== test session starts ====================
platform darwin -- Python 2.7.12, pytest-3.2.3, py-1.4.34, pluggy-0.4.0
rootdir: /Users/easonhan/code/testclass.net/src/pytest, inifile:
collected 5 items
test_user_password_with_params.py ..FF.
====================== FAILURES =========================
______ TestUserPasswordWithParam.test_user_password[user2] ____
self = <test_user_password_with_params.TestUserPasswordWithParam object at 0x10de1d790>, user = {'name': 'tom', 'password': 'password123'}
def test_user_password(self, user):
passwd = user['password']
assert len(passwd) >= 6
msg = "user %s has a weak password" %(user['name'])
assert passwd != 'password', msg
> assert passwd != 'password123', msg
E AssertionError: user tom has a weak password
E assert 'password123' != 'password123'
test_user_password_with_params.py:15: AssertionError
_____ TestUserPasswordWithParam.test_user_password[user3]_______
self = <test_user_password_with_params.TestUserPasswordWithParam object at 0x10de1df50>, user = {'name': 'mike', 'password': 'password'}
def test_user_password(self, user):
passwd = user['password']
assert len(passwd) >= 6
msg = "user %s has a weak password" %(user['name'])
> assert passwd != 'password', msg
E AssertionError: user mike has a weak password
E assert 'password' != 'password'
test_user_password_with_params.py:14: AssertionError
======== 2 failed, 3 passed in 0.05 seconds =============
总共运行了5个用例,3个成功,2个失败。
举例4
@pytest.mark.parametrize
装饰器可以让我们每次参数化fixture的时候传入多个项目,parametrize每次多个参数,更加灵活。
import pytest
@pytest.mark.parametrize("test_input,expected", [
("3+5", 8),
("2+4", 6),
("6*9", 42),
])
def test_eval(test_input, expected):
assert eval(test_input) == expected
test_eval方法中传入了2个参数,就如同@pytest.mark.parametrize
装饰器中定义的那样,因此简单理解,我们可以把parametrize装饰器想象成是数据表格,有表头(test_input,expected)以及具体的数据。
运行结果
$ pytest
======= test session starts ========
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR, inifile:
collected 3 items
test_expectation.py ..F
======= FAILURES ========
_______ test_eval[6*9-42] ________
test_input = '6*9', expected = 42
@pytest.mark.parametrize("test_input,expected", [
("3+5", 8),
("2+4", 6),
("6*9", 42),
])
def test_eval(test_input, expected):
> assert eval(test_input) == expected
E AssertionError: assert 54 == 42
E + where 54 = eval('6*9')
test_expectation.py:8: AssertionError
======= 1 failed, 2 passed in 0.12 seconds ========
上一篇: Java入门----猜拳小游戏
下一篇: pytest高阶用法二