pytest的简单学习

2019/01/17 Python Python模块

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 的参数也是可以逻辑判断。

4、注册标记

五、hook类(留坑)

六、objects(留坑)

七、更多

Search

    Table of Contents