pip install -U pytest
py.test --version
或者pytest --version
# example
>>>assert 1 == 1
>>>assert 2+2 == 2*2
>>>assert len('hello') < 10
>>>assert len('hello') > 10
Traceback (most recent call last):
File "" , line 1, in
AssertionError
>>>assert len('hello') > 10, '字符串长度小于10'
Traceback (most recent call last):
File "" , line 1, in
AssertionError: 字符串长度小于10
>>>assert range(4) == [0,1,2,3]
通过上面关于assert的例子,可以看出很适合单元测试。
def func(x):
return x+1
def test_func():
assert func(3) == 5
进入终端,在该文件所在目录下,执行pytest
。
执行结果如下:
========================= test session starts =========================
platform linux2 -- Python 2.7.13+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1
rootdir: /home/light/code/study/test_pytest, inifile:
plugins: celery-4.2.1
collected 1 item
test_pytest.py F [100%]
========================= FAILURES =========================
_________________________ test_func ________________________
def test_func():
> assert func(3) == 5
E assert 4 == 5
E + where 4 = func(3)
test_pytest.py:6: AssertionError
========================= 1 failed in 0.39 seconds =========================
pytest会在当前的目录下,寻找以test开头的文件(即测试文件),找到测试文件之后,进入到测试文件中寻找test_开头的测试函数并执行。
通过上面的测试输出,我们可以看到该测试过程中,收集到了一个测试函数,测试结果是失败的(标记为F),并且在FAILURES部分输出了详细的错误信息,帮助我们分析测试原因,我们可以看到”assert func(3) == 5”这条语句出错了,错误的原因是func(3)=4。
class TestClass:
def test_one(self):
x = "this"
assert 'h' in x
def test_two(self):
x = "hello"
assert hasattr(x, 'check')
进入终端,在该文件所在目录下,执行pytest
。
执行结果如下:
========================= test session starts =========================
platform linux2 -- Python 2.7.13+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1
rootdir: /home/light/code/study/test_pytest, inifile:
plugins: celery-4.2.1
collected 3 items
test_pytest.py ..F [100%]
========================= FAILURES =========================
________________________ TestClass.test_three ________________________
self = 0x7f9371aa09d0>
def test_three(self):
x = 'world'
> assert hasattr(x, 'hello')
E AssertionError: assert False
E + where False = hasattr('world', 'hello')
test_pytest.py:20: AssertionError
========================= 1 failed, 2 passed in 0.14 seconds =========================
pytest会在当前的目录下,寻找以test开头的文件(即测试文件),找到测试文件之后,进入到测试文件中寻找Test开头的测试类并执行。
通过观察上面的测试结果,可以看到采集到了三个测试函数,其中前两个成功(.表示),最后一个失败了(F表示),再看FAILURES部分,可以看到False = hasattr(‘world’, ‘hello’),也就是’world’没有‘hello‘属性,所以报错。
通过前面两个例子,可以看出一些规律。
- 一般情况下,只需要在测试文件所在的文件夹下执行pytest就可以开始测试
- 测试文件以test_开头或者以_test结尾
- 测试函数以test_开头
- 测试类以Test开头,并且没有__init__方法
- 断言使用assert
- 满足以上条件,pytest就可以直接运行测试用例
def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
import pytest
@pytest.fixture()
def before():
print '\nbefore each test'
def test_1(before):
print 'test_1()'
def test_2(before):
print 'test_2()'
assert 0
上面的代码用fixture方法装饰了before函数,before函数打印了一行数据。
两个测试函数的参数中有before。
进入终端,在该文件所在目录下,执行pytest -v -s
。(备注:-v指的是将结果.映射为PASSED,将F映射为FAILED;-s是指执行代码过程)
========================= test session starts =========================
platform linux2 -- Python 2.7.13+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1 -- /home/light/.virtualenvs/study/bin/python2.7
cachedir: .pytest_cache
rootdir: /home/light/code/study/test_pytest, inifile:
plugins: celery-4.2.1
collected 2 items
test_pytest.py::test_1
before each test
test_1()
PASSED
test_pytest.py::test_2
before each test
test_2()
FAILED
========================= FAILURES =========================
________________________ test_2 ________________________
before = None
def test_2(before):
print 'test_2()'
> assert 0
E assert 0
test_pytest.py:15: AssertionError
========================= 1 failed, 1 passed in 0.03 seconds =========================
从结果可以看到,两个测试函数在执行前打印了一行数据。
这个例子可能没什么意义,但假设before函数打开了一个文件或者打开了一个session,测试函数就可以直接使用。
import pytest
@pytest.fixture()
def before():
print('\nbefore each test')
@pytest.mark.usefixtures("before")
def test_1():
print('test_1()')
@pytest.mark.usefixtures("before")
def test_2():
print('test_2()')
class Test1:
@pytest.mark.usefixtures("before")
def test_3(self):
print('test_1()')
@pytest.mark.usefixtures("before")
def test_4(self):
print('test_2()')
@pytest.mark.usefixtures("before")
class Test2:
def test_5(self):
print('test_1()')
def test_6(self):
print('test_2()')
import time
import pytest
@pytest.fixture(scope="module", autouse=True)
def mod_header(request):
print('\n-----------------')
print('module : %s' % request.module.__name__)
print('-----------------')
@pytest.fixture(scope="function", autouse=True)
def func_header(request):
print('\n-----------------')
print('function : %s' % request.function.__name__)
print('time : %s' % time.asctime())
print('-----------------')
def test_one():
print('in test_one()')
def test_two():
print('in test_two()')
autouse直译自动调用,它需要和scope进行配合。
- fixture方法原型:def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
scope代表范围,autouse代表是否自动调用。
- scope
function:每个test都运行,默认是function的scope
class:每个class的所有test只运行一次
module:每个module的所有test只运行一次
session:每个session只运行一次,这里的session指的是pytest跑一次的窗口,是最大范围
在上面的例子中,fixture返回值都是默认None,我们可以选择让fixture返回我们需要的东西。如果你的fixture需要配置一些数据,读个文件,或者连接一个数据库,那么你可以让fixture返回这些数据或资源。
如何带参数
fixture还可以带参数,可以把参数赋值给params,默认是None。对于param里面的每个值,fixture都会去调用执行一次,就像执行for循环一样把params里的值遍历一次。
test_fixture_param.py
import pytest
@pytest.fixture(params=[1, 2, 3])
def test_data(request):
return request.param
def test_not_2(test_data):
print('test_data: %s' % test_data)
assert test_data != 2
========================= test session starts =========================
platform linux2 -- Python 2.7.13+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1 -- /home/light/.virtualenvs/study/bin/python2.7
cachedir: .pytest_cache
rootdir: /home/light/code/study/test_pytest, inifile:
plugins: celery-4.2.1
collected 3 items
test_param.py::test_not_2[1] test_data: 1
PASSED
test_param.py::test_not_2[2] test_data: 2
FAILED
test_param.py::test_not_2[3] test_data: 3
PASSED
========================= FAILURES =========================
可以看到三个参数分别执行。
# -*-coding:utf-8 -*-
# 数据库表结构原型
# 存为Models.py
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String
Base = declarative_base()
class User(Base):
__tablename__ = 'User'
id = Column('id', Integer, primary_key=True, autoincrement=True)
name = Column('name', String(50), unique=True)
age = Column('age', Integer)
def __repr__(self):
return "" % (
self.id, self.name, self.age)
class Role(Base):
__tablename__ = 'Role'
id = Column('id', Integer, primary_key=True, autoincrement=True)
name = Column('name', String(50), unique=True)
def __repr__(self):
return "" % (self.id, self.name)
# -*-coding:utf-8 -*-
# fixture打开一个数据库,并做一些操作
# -*- coding:utf-8 -*-
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from Models import Base, User, Role
import pytest
@pytest.fixture(scope='module', autouse=True)
def connect():
"""
连接到数据库,并且返回一个session连接
启动pytest时自动调用
"""
print('conn...')
conn_str = 'mysql+mysqlconnector://root:password@localhost:3306/pytest'
engine = create_engine(conn_str, echo=True)
db_session = sessionmaker(bind=engine)
Base.metadata.create_all(engine)
session = db_session()
return session
@pytest.fixture(scope='function', autouse=True)
def start_test():
print('\nstart test...')
def test_delete_data(connect):
"""使用第一种方式调用fixture"""
session = connect
session.query(User).delete()
session.query(Role).delete()
assert session.query(User).all() == []
assert session.query(Role).all() == []
def test_add_data(connect):
u1 = User(name='Light', age=2)
u2 = User(name='Ash', age=20)
r = Role(name='user')
session = connect
session.add(u1)
session.add(u2)
session.add(r)
session.commit()
assert session.query(User).all() != []
assert session.query(Role).all() != []
def test_select_data(connect):
session = connect
data1 = session.query(User).filter(User.id == '1').first()
data2 = session.query(User).filter(User.id == '2').first()
data3 = session.query(Role).filter(Role.id == '1').first()
assert data1.name == 'Light' and data1.age == 23
assert data2.name == 'Ash' and data2.age == 20
assert data3.name == 'user'
def test_drop_table(connect):
session = connect
session.execute("drop table User")
session.execute("drop table Role")
assert session.execute('show tables').rowcount == 0
注意上面的代码,fixture使用了module级别的,并且autouse=True,通过执行过的结果可以看出,只在做第一个测试的时候连接了数据库,之后就没有连接过数据库。
进入终端对应文件夹,执行pytest -v test_session.py
========================= test session starts =========================
platform linux2 -- Python 2.7.13+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1 -- /home/light/.virtualenvs/study/bin/python2.7
cachedir: .pytest_cache
rootdir: /home/light/code/study/test_pytest, inifile:
plugins: celery-4.2.1
collected 4 items
test_session.py::test_delete_data PASSED [ 25%]
test_session.py::test_add_data PASSED [ 50%]
test_session.py::test_select_data PASSED [ 75%]
test_session.py::test_drop_table PASSED [ 100%]
========================= 4 passed in 1.06 seconds =========================
import pytest
@pytest.fixture(scope='module', autouse=True)
def openfile():
print('open the file...')
f = open('test.txt', 'a')
return f
def test_write1(openfile):
f = openfile
old_tell = f.tell()
data = '1111111111\n'
f.write(data)
new_tell = f.tell()
assert new_tell - old_tell == len(data)
def test_write2(openfile):
f = openfile
old_tell = f.tell()
data = '2222222222\n'
f.write(data)
new_tell = f.tell()
assert new_tell - old_tell == len(data)
def test_close(openfile):
openfile.close()
with pytest.raises(ValueError) as e:
openfile.write('3')
pytest -v -s test_file.py
=========================================== test session starts ===========================================
platform linux2 -- Python 2.7.13+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1 -- /home/light/.virtualenvs/study/bin/python2.7
cachedir: .pytest_cache
rootdir: /home/light/code/study/test_pytest, inifile:
plugins: celery-4.2.1
collected 3 items
test_file.py::test_write1 open the file...
PASSED
test_file.py::test_write2 PASSED
test_file.py::test_close PASSED
======================================== 3 passed in 0.07 seconds =========================================
可以看到文件只打开了一次。
import pytest
@pytest.mark.a
def test_a_1():
print('this is a')
@pytest.mark.a
def test_a_2():
print('this is a')
@pytest.mark.b
def test_b_1():
print('this is b')
上面的代码里,分别标了两个a测试,一个b测试。
`pytest -v test_mark1.py -m a`
...
collected 3 items / 1 deselected
test_mark1.py::test_a_1 PASSED [ 50%]
test_mark1.py::test_a_2 PASSED [100%]
...
`pytest -v test_,ark1.py -m b`
...
collected 3 items / 2 deselected
test_mark1.py::test_b_1 PASSED [100%]
...
可以看出, -m后面指定的字符会对应标记执行测试函数。
`pytest -v test_mark1.py -m "not a"`
...
collected 3 items / 2 deselected
test_mark1.py::test_b_1 PASSED [100%]
...
`pytest -v test_mark1.py -m "not b"`
...
collected 3 items / 1 deselected
test_mark1.py::test_a_1 PASSED [ 50%]
test_mark1.py::test_a_2 PASSED [100%]
...
`pytest -v test_mark1.py -m "a or b"`
...
collected 3 items
test_mark1.py::test_a_1 PASSED [ 33%]
test_mark1.py::test_a_2 PASSED [ 66%]
test_mark1.py::test_b_1 PASSED [100%]
...
`pytest -v test_mark1.py -m "a and b"`
...
collected 3 items / 3 deselected
...
通过上述命令行执行后的结果,可以看出pytest -m后面的参数可以被逻辑判断(not,and,or)。
这点很重要,因为-k也是如此。
假设只想执行某个测试函数,除此之外不执行,那么可以通过命令行指定。
pytest -v test_mark1.py::test_a_1
`pytest -v test_mark1.py::test_a_1`
...
collected 1 item
test_mark1.py::test_a_1 PASSED [100%]
...
k指的是keyword,测试函数的关键字。假设不使用mark标记测试函数,也可以通过-k直接指定想测试的函数的名字或者部分名字。
`pytest -v test_mark1.py -k a_`
...
collected 3 items / 1 deselected
test_mark1.py::test_a_1 PASSED [ 50%]
test_mark1.py::test_a_2 PASSED [100%]
...
`pytest -v test_mark1.py -k "a_ or b_"`
...
collected 3 items
test_mark1.py::test_a_1 PASSED [ 33%]
test_mark1.py::test_a_2 PASSED [ 66%]
test_mark1.py::test_b_1 PASSED [100%]
...
`pytest -v test_mark1.py -k "not a_"`
...
collected 3 items / 2 deselected
test_mark1.py::test_b_1 PASSED [100%]
...
理解了-m的使用,-k的参数也是可以逻辑判断。