pytest 的简单学习
一、介绍及下载安装
- pytest 是 python 的一种单元测试框架,与 python 自带的 unittest 测试框架类似,但是比 unittest 框架使用起来更简洁,效率更高。
- 其主要使用 assert 断言对单元方法记性测试
- 其有个 fixture 类,可以减少资源占用,资源的统一调度
- 安装:
pip install -U pytest
- 测试:
py.test --version
或者pytest --version
二、基础单元测试(assert 断言)
- assert 断言是 python 标准语法里的东西
- asset 后是一个返回布尔值的表达式
- 若为真,则通过;若为假,抛出异常
# example
>>>assert 1 == 1
>>>assert 2+2 == 2*2
>>>assert len('hello') < 10
>>>assert len('hello') > 10
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
>>>assert len('hello') > 10, '字符串长度小于10'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: 字符串长度小于10
>>>assert range(4) == [0,1,2,3]
通过上面关于 assert 的例子,可以看出很适合单元测试。
1、测试函数的例子
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。
2、测试类的例子
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 = <test_pytest.test_pytest.TestClass object at 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‘ 属性,所以报错。
3、如何编写测试样例
通过前面两个例子,可以看出一些规律。
- 一般情况下,只需要在测试文件所在的文件夹下执行 pytest 就可以开始测试
- 测试文件以 test_ 开头或者以 _test 结尾
- 测试函数以 test_ 开头
- 测试类以 Test 开头,并且没有 __init__ 方法
- 断言使用 assert
- 满足以上条件,pytest 就可以直接运行测试用例
三、fixture 方法
- pytest 的 fixture 方法,本身可以理解为装饰器,它装饰一个函数,装饰后的函数可以被测试函数(test_开头的函数)作为参数使用。
- 举个例子:有一个函数功能是打开某个文件,用 fixture 装饰。其他的测试函数将打开文件函数作为参数传入,就可以直接使用文件。
- 通过上面的例子,可以把 fixture 看做是资源,在你的测试用例执行之前需要去配置这些资源,执行完后需要去释放资源。
- 通过 fixture 方法,减少了资源的调用,通过配置 scope、autouse 等参数,可以更加简化测试流程。
- fixture 方法原型:
def fixture(scope="function", params=None, autouse=False, ids=None, name=None)
1、fixture 简单实例
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,测试函数就可以直接使用。
2、三种调用 fixture 方式
测试函数直接调用
通过 mark.usefixtures() 调用
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()')
通过 autouse 调用
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跑一次的窗口,是最大范围
3、fixture 返回值
在上面的例子中,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 =========================
可以看到三个参数分别执行。
4、测试数据库连接的例子
# -*-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 "<User(id='%s' name='%s' age='%s')>" % (
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 "<Role(id='%s' name='%s')>" % (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 =========================
5、测试文件写入的例子
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 =========================================
可以看到文件只打开了一次。
四、mark 类
- mark 就是标记,也可以理解为一个装饰器。可以通过命令行参数指定特定的测试函数执行。
- 在这里要说到 pytest 的命令行参数。pytest 命令行参数非常多,在前面已经提到了 -v(指的是将结果.映射为PASSED,将F映射为FAILED)和 -s(指执行代码过程)。在这里先说 -m 和 -k。
- 也就是说,mark 类主要的作用就是控制每个测试函数是否执行
1、简单的 -m
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 也是如此。
2、唯一 ID 执行
假设只想执行某个测试函数,除此之外不执行,那么可以通过命令行指定。
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%]
...
3、简单的 -k
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 的参数也是可以逻辑判断。