整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

从0到1上手Pytest

从0到1上手Pytest


如果你想快速上手pytest,只关注"Pytest必会知识点"章节就可以了(该章节已经能够解决基础的ui和接口自动化测试需求);

如果你想要详细了解关于Fixture的使用方法,请关注"pytest高级用法"部分。



Pytest必会知识点

基础介绍

pytest 是 python 的第三方单元测试框架,比unittest 更简洁和高效,支持315种以上的插件,同时兼容 nose、unittest 框架。

官网https://docs.pytest.org/en/latest/contents.html

安装 pip install pytest

Pytest的命名规则

命名规则如下:

1. 测试文件应当命名为 test_<something>.py或者<somethins>_test.py

2. 测试函数、测试类方法应当命名为test_<something>

3. 测试类应当命名为Test<Something>.

pytest的setup和teardown函数

1)模块级(setup_module/teardown_module)开始于模块始末

2)类级(setup_class/teardown_class)开始于类的始末

3)类里面的(setup/teardown)(运行在调用函数的前后)

4)功能级(setup_function/teardown_function)开始于功能函数始末(不在类中)

5)方法级(setup_method/teardown_method)开始于方法始末(在类中)

pytest的mark

pytest的mark主要用来标记用例,通过不同的标记实现不同的运行策略

主要用途:

标记和分类用例: @pytest.mark.level1

标记用例执行顺顺序pytest.mark.run(order=1) (需安装pytest-ordering)

标记用例在指定条件下跳过或直接失败 @pytest.mark.skipif()/xfail()

标记使用指定fixture(测试准备及清理方法) @pytest.mark.usefixtures()

参数化 @pytest.mark.parametrize

标记超时时间 @pytest.mark.timeout(60) (需安装pytest-timeout)

标记失败重跑次数@pytest.mark.flaky(reruns=5, reruns_delay=1) (需安装pytest-rerunfailures)

Pytest运行用例

pytest 运行目录下的所有用例

pytest test_reg.py运行指定模块中的所有用例

pytest test_reg.py::TestClass::test_method 运行指定模块指定类指定用例

pytest -m tag 运行包含指定标签的所有用例

pytest -k "test_a and test_b" 运行名称包含指定表达式的用例 (支持and or not)

其他常用参数

-q: 安静模式, 不输出环境信息

-v: 丰富信息模式, 输出更详细的用例执行信息

-s: 显示程序中的print/logging输出


Pytest的插件

pytest的插件也是通过pip install 命令进行安装,常用的插件如下

pytest-rerunfailures 用例重新执行

pytest-html 生成html报告

pytest-timeout 限制超时时间

pytest-parallel 多线程使用,常用配置命令如下:

–workers (optional) *:多进程运行需要加此参数, *是进程数。默认为1。

–tests-per-worker (optional) *:多线程运行, *是每个worker运行的最大并发线程数。默认为1

实例:多线程执行并生成html报告

定义两个py文件test_demo1.py和test_demo2.py并放入同一包中

#test_demo1.py

import pytest
import time
class TestPrint:
    def setup(self):

        print("setup")

    @pytest.mark.smoke
    def test_test1(self):
        time.sleep(2)

        assert (1==1)

    def test_test2(self):
        time.sleep (2)
        assert (1==2)

    @pytest.mark.smoke
    def test_test3(self):
        time.sleep (2)
        assert (1==1)

    def teardown(self):
        print ("teardown")


if __name__=='__main__':
    pytest.main(['-s', 'demo1.py'])

#test_demo2.py

import pytest
import time
class TestPrint:
    def setup(self):

        print("setup")

    def test_test11(self):
        time.sleep(2)

        assert (1==1)

    @pytest.mark.smoke
    def test_test21(self):
        time.sleep (2)
        assert (1==2)

    def test_test31(self):
        time.sleep (2)
        assert (1==1)

    def teardown(self):
        print ("teardown")


if __name__=='__main__':
    pytest.main(['-s', 'demo2.py'])


在包中运行脚本

pytest -v --tests-per-worker 2 --html=report.html

可以看到两个py文件中的用例全部被执行,并在该目录下生成了report.html测试报告,测试结果如下:

============================================================================

test session starts============================================================================

platform win32 -- Python 3.7.4, pytest-5.3.2, py-1.8.1, pluggy-0.13.1 -- c:\python37\python.exe

cachedir: .pytest_cache

metadata: {'Python': '3.7.4', 'Platform': 'Windows-10-10.0.17763-SP0', 'Packages': {'pytest': '5.3.2', 'py': '1.8.1', 'pluggy': '0.13.1'}, 'Plugins': {'forked': '1.1.3', 'html': '2.0.1', 'metadata': '1.8.0', 'parallel': '0.0.10', 'rerunfailures': '8.0', '

xdist': '1.31.0'}, 'JAVA_HOME': 'C:\Program Files\Java\jdk1.8.0_151'}

rootdir: C:\Users\Kevin\NewLesson\pytest_demo2

plugins: forked-1.1.3, html-2.0.1, metadata-1.8.0, parallel-0.0.10, rerunfailures-8.0, xdist-1.31.0

collected 6 items

pytest-parallel: 1 worker (process), 2 tests per worker (threads)


test_demo1.py::TestPrint::test_test1 test_demo1.py::TestPrint::test_test2

test_demo1.py::TestPrint::test_test1 PASSED

test_demo1.py::TestPrint::test_test3

test_demo1.py::TestPrint::test_test2 FAILED

test_demo2.py::TestPrint::test_test11

test_demo1.py::TestPrint::test_test3 PASSED

test_demo2.py::TestPrint::test_test21

test_demo2.py::TestPrint::test_test11 PASSED

test_demo2.py::TestPrint::test_test31

test_demo2.py::TestPrint::test_test21 FAILED

test_demo2.py::TestPrint::test_test31 PASSED


===========================================================================FAILURES===========================================================================

________________________________________________________________________________ TestPrint.test_test2 ________________________________________________________________________________


self=<pytest_demo2.test_demo1.TestPrint object at 0x000002B81DFBDEC8>


def test_test2(self):

time.sleep (2)

> assert (1==2)

E assert 1==2

E -1

E +2


test_demo1.py:16: AssertionError

------------------------------------------------------------------------------------------------------------------- Captured stdout setup ------------------------------------------------------------------------------------------------------------------

setupsetup


------------------------------------------------------------------------------------------------------------------ Captured stdout teardown ------------------------------------------------------------------------------------------------------------------

teardown

______________________________________________________________________________

TestPrint.test_test21 ______________________________________________________________________________

self=<pytest_demo2.test_demo2.TestPrint object at 0x000002B81E02B108>


@pytest.mark.smoke

def test_test21(self):

time.sleep (2)

> assert (1==2)

E assert 1==2

E -1

E +2


test_demo2.py:16: AssertionError

------------------------------------------------------------------------------------------------------------------- Captured stdout setup --------------------------------------------------------------------------------------------------------------------

setup

------------------------------------------------------------------------------------------------------------------ Captured stdout teardown ------------------------------------------------------------------------------------------------------------------

teardown

==============================================================================warnings summary==============================================================================

c:\python37\lib\site-packages\_pytest\mark\structures.py:327

c:\python37\lib\site-packages\_pytest\mark\structures.py:327: PytestUnknownMarkWarning: Unknown pytest.mark.smoke - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html

PytestUnknownMarkWarning,


-- Docs: https://docs.pytest.org/en/latest/warnings.html

--------------------------------------------------------------------------------------- generated html file: file://C:\Users\Kevin\NewLesson\pytest_demo2\report.html ----------------------------------------------------------------------------------------

=============================================================================2 failed, 4 passed, 1 warning in 6.42s=============================================================================

高级用法

Fixture

fixture用途

备注: 这里主要引用 https://www.cnblogs.com/linuxchao/p/linuxchao-pytest-fixture.html

1.做测试前后的初始化设置,如测试数据准备,链接数据库,打开浏览器等这些操作都可以使用fixture来实现

2.测试用例的前置条件可以使用fixture实现

3.支持经典的xunit fixture ,像unittest使用的setup和teardown

fixture区别于unnitest的传统单元测试(setup/teardown)有显著改进:

· 有独立的命名,并通过声明它们从测试函数、模块、类或整个项目中的使用来激活。

· 按模块化的方式实现,每个fixture都可以互相调用。

· fixture的范围从简单的单元测试到复杂的功能测试,可以对fixture配置参数,或者跨函数function,类class,模块module或整个测试session范围。

· fixture可以实现unittest不能实现的功能,比如unittest中的测试用例和测试用例之间是无法传递参数和数据的,但是fixture却可以解决这个问题

fixture的使用

Fixture名字作为用例的参数

fixture的名字直接作为测试用例的参数,上面的实例就这这种方式,再来看一个实例

# test_fixture.py


import pytest


@pytest.fixture()

def fixtureFunc():

return 'fixtureFunc'


def test_fixture(fixtureFunc):

print('我调用了{}'.format(fixtureFunc))


class TestFixture(object):

def test_fixture_class(self, fixtureFunc):

print('在类中使用fixture "{}"'.format(fixtureFunc))


if __name__=='__main__':

pytest.main(['-v', 'test_fixture.py'])


使用@pytest.mark.usefixtures('fixture')装饰器

每个函数或者类前使用@pytest.mark.usefixtures('fixture')装饰器装饰

实例

# test_fixture.py

import pytest

@pytest.fixture()

def fixtureFunc():

print('\n fixture->fixtureFunc')


@pytest.mark.usefixtures('fixtureFunc')

def test_fixture():

print('in test_fixture')


@pytest.mark.usefixtures('fixtureFunc')

class TestFixture(object):

def test_fixture_class(self):

print('in class with text_fixture_class')


if __name__=='__main__':

pytest.main(['-v', 'test_fixture.py'])


使用autouse参数

指定fixture的参数autouse=True这样每个测试用例会自动调用fixture(其实这里说的不是很准确,因为还涉及到fixture的作用范围,那么我们这里默认是函数级别的,后面会具体说fixture的作用范围)

实例

# test_fixture.py

import pytest

@pytest.fixture(autouse=True)

def fixtureFunc():

print('\n fixture->fixtureFunc')


def test_fixture():

print('in test_fixture')


class TestFixture(object):

def test_fixture_class(self):

print('in class with text_fixture_class')


if __name__=='__main__':

pytest.main(['-v', 'test_fixture.py'])

结果

fixture->fixtureFunc

.in test_fixture


fixture->fixtureFunc

.in class with text_fixture_class

[100%]


==========================2 passed in 0.04 seconds===========================

从结果可以看到每个测试用例执行前都自动执行了fixture


fixture作用范围

上面所有的实例默认都是函数级别的,所以测试函数只要调用了fixture,那么在测试函数执行前都会先指定fixture。说到作用范围就不得不说fixture 的第二个参数scope参数。

scope参数可以是session, module,class,function; 默认为function

1.session 会话级别(通常这个级别会结合conftest.py文件使用,所以后面说到conftest.py文件的时候再说)

2.module 模块级别: 模块里所有的用例执行前执行一次module级别的fixture

3.class 类级别 :每个类执行前都会执行一次class级别的fixture

4.function :前面实例已经说了,这个默认是默认的模式,函数级别的,每个测试用例执行前都会执行一次function级别的fixture

下面我们通过一个实例具体看一下 fixture的作用范围

# test_fixture.py

import pytest


@pytest.fixture(scope='module', autouse=True)

def module_fixture():

print('\n-----------------')

print('我是module fixture')

print('-----------------')

@pytest.fixture(scope='class')

def class_fixture():

print('\n-----------------')

print('我是class fixture')

print('-------------------')

@pytest.fixture(scope='function', autouse=True)

def func_fixture():

print('\n-----------------')

print('我是function fixture')

print('-------------------')


def test_1():

print('\n 我是test1')


@pytest.mark.usefixtures('class_fixture')

class TestFixture1(object):

def test_2(self):

print('\n我是class1里面的test2')

def test_3(self):

print('\n我是class1里面的test3')

@pytest.mark.usefixtures('class_fixture')

class TestFixture2(object):

def test_4(self):

print('\n我是class2里面的test4')

def test_5(self):

print('\n我是class2里面的test5')


if __name__=='__main__':

pytest.main(['-v', '--setup-show', 'test_fixture.py'])


fixture实现teardown

其实前面的所有实例都只是做了测试用例执行之前的准备工作,那么用例执行之后该如何实现环境的清理工作呢?这不得不说yield关键字了,相比大家都或多或少的知道这个关键字,他的作用其实和return差不多,也能够返回数据给调用者,唯一的不同是被掉函数执行遇到yield会停止执行,接着执行调用处的函数,调用出的函数执行完后会继续执行yield关键后面的代码(具体原理可以看下我)。看下下面的实例来了解一下如何实现teardown功能

import pytest

from selenium import webdriver

import time

@pytest.fixture()

def fixtureFunc():  '''实现浏览器的打开和关闭'''

driver=webdriver.Firefox()

yield driver

driver.quit()

def test_search(fixtureFunc):

'''访问百度首页,搜索pytest字符串是否在页面源码中'''

driver=fixtureFunc

driver.get('http://www.baidu.com')

driver.find_element_by_id('kw').send_keys('pytest')

driver.find_element_by_id('su').click()

time.sleep(3)

source=driver.page_source

assert 'pytest' in source


if __name__=='__main__':

pytest.main(['--setup-show', 'test_fixture.py'])

这个实例会先打开浏览器,然后执行测试用例,最后关闭浏览器。大家可以试试! 通过yield就实现了 用例执行后的teardown功能


conftest.py

1、可以跨.py文件调用,有多个.py文件调用时,可让conftest.py只调用了一次fixture,或调用多次fixture

2、conftest.py与运行的用例要在同一个pakage下,并且有__init__.py文件

3、不需要import导入 conftest.py,pytest用例会自动识别该文件,放到项目的根目录下就可以全局目录调用了,如果放到某个package下,那就在改package内有效,可有多个conftest.py

4、conftest.py配置脚本名称是固定的,不能改名称

5、conftest.py文件不能被其他文件导入

6、所有同目录测试文件运行前都会执行conftest.py文件


主要用途之一共享变量,代码如下:

import pytest

# conftest.py

@pytest.fixture(scope='session')

def get_token():

token='abcd'

return token

这样,在多个测试用例中使用fixture get_token()时就可以共享 token值了

元测试的概念

  • 单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。
  • 对于单元测试中单元的含义,要根据实际情况去判定其具体含义。
  • 一个单元可能是功能模块、类、方法(函数)等。

单元测试工具

不同的编程语言都有比较成熟的单元测试框架,语法规则有些差别,其核心思想都是相通的。常见的单

元测试框架有:

Java语言:Junit、TestNG

Python语言:UnitTest、Pytest

UnitTest单元测试框架

一、UnitTest框架介绍

UnitTest是Python自带的一个单元测试框架,用它来做单元测试。也经常应用到UI自动化测试和接口自

动化测试中,用来管理和维护测试用例脚本

使用UnitTest框架的好处:

1. 能够组织多个用例去执行(可以把多条测试用例封装成一个测试套件,实现批量执行测试用例)

2. 提供了丰富的断言方法,方便对用例执行的结果进行判断

3. 能够生成HTML格式的测试报告

4. 使用Fixture功能可以减少代码的冗余

UnitTest核心要素:

  • TestCase
  • TestSuite
  • TestRunner
  • TestLoader

二、TestCase

TestCase就是表示测试用例

案例

定义一个实现加法操作的函数,并对该函数进行测试

如何定义测试用例

1.导包:importunittest

2.定义测试类:新建测试类必须继承unittest.TestCase

3.定义测试方法:测试方法名称命名必须以test开头

示例代码:


ytest 框架可以用来做 系统测试 的自动化, 它的特点有

  • 用 Python 编写测试用例,简便易用
  • 可以用 文件系统目录层次 对应 手工测试用例 层次结构
  • 灵活的 初始化清除 机制
  • 可以灵活挑选测试用例执行
  • 利用第三方插件,可以生成不错的报表

----------------------------------------------------------------------------------------------

安装

pip install pytest

我们还需要产生测试报表,所以要安装一个第三方插件 pytest-html ,执行如下命令安装

pip install pytest-html

-----------------------------------------------------------------------------------------

快速上手

pytest 如何知道你哪些代码是自动化的测试用例?

官方文档 给出了 pytest 寻找 测试项 的 具体规则

  • 如果未指定命令行参数,则从 testpath(如果已配置)或当前目录开始收集。如果命令行参数, 指定了 目录、文件名 或 node id 的任何组合,则按参数来找
  • 寻找过程会递归到目录中,除非它们匹配上 norecursedirs。
  • 在这些目录中,搜索由其测试包名称导入的 test_*.py 或 *_test.py 文件。
  • 从这些文件中,收集如下测试项:test为前缀 的 函数Test为前缀的 类 里面的 test为前缀的方法

-----------------------------------------------------------------------------------------

测试用例代码

首先,我们编写的测试用例代码文件, 必须以 test_ 开头,或者以 _test 结尾

比如,我们创建一个 文件名为 test_错误登录.py ,放在目录 autotest\cases\登录 下面。

其中 autotest 是 我们创建的 自动化项目根目录

class Test_错误密码:

    def test_C001001(self):
        print('\n用例C001001')
        assert 1==1
        
    def test_C001002(self):
        print('\n用例C001002')
        assert 2==2
        
    def test_C001003(self):
        print('\n用例C001003')
        assert 3==2

如果我们把测试用例存放在类中, 类名必须以 Test 为前缀的 类 ,用例对应的方法必须以 test 为前缀的方法。
pytest 中用例的检查点 直接用 Python 的 assert 断言。

assert 后面的表达式结果 为 True ,就是 检查点 通过,结果为False ,就是检查点 不通过。

运行测试

执行测试非常简单,打开命令行窗口,进入自动化项目根目录(我们这里就是 autotest),执行命令程序 pytest 即可

显示找到3个测试项,2个执行通过,1个不通过。

通过的用例 是用一个绿色小点表示, 不通过的用例用一个红色的F表示

并且会在后面显示具体不通过的用例 和不通过的检查点 代码细节。


大家可以发现,用例代码中有些打印语句没有显示出内容。

因为pytest 会 截获print打印的内容。

如果我们希望 显示测试代码中print的内容,因为这些打印语句在调试代码时很有用,可以加上命令行参数 -s

如下

pytest -s

如果我们希望得到更详细的执行信息,包括每个测试类、测试函数的名字,可以加上参数 -v,这个参数可以和 -s 合并为 -sv

如下

pytest -sv

执行 pytest 时, 如果命令行没有指定目标目录 或者 文件, 它会自动搜索当前目录下所有符合条件的文件、类、函数。

所以上面,就找到了3个测试方法,对应3个用例。

我们目前 项目根目录 中 只有一个cases 目录用例存放测试用例, 将来还会有其他目录,比如:

lib目录存放库代码、cfg目录存放配置数据 等等。

为了防止 pytest 到其他目录中找测试用例项,执行测试时,我们可以在命令行加上目标目录 cases ,就是这样

pytest cases

--------------------------------------------------------------------------------------------------------------

产生报告

前面在安装pytest,我们也安装了 pytest-html 插件,这个插件就是用来产生测试报告的。

要产生报告,在命令行加上 参数 --html=report.html --self-contained-html ,如下

pytest cases --html=report.html --self-contained-html

这样就会产生名为 report.html 的测试报告文件,可以在浏览器中打开

但是这个工具有个bug,导致测试目录、文件、类名 中,如果有中文,显示为乱码

可以这样修复:

  • 打开该插件对应的代码文件,通常在解释器目录下:site-packages\pytest_html\plugin.py
  • 找到如下代码 class TestResult: def __init__(self, outcome, report, logfile, config): self.test_id=report.nodeid.encode("utf-8").decode("unicode_escape") 改为 class TestResult: def __init__(self, outcome, report, logfile, config): # 白月黑羽修改方法,解决乱码问题 # self.test_id=report.nodeid.encode("utf-8").decode("unicode_escape") self.test_id=report.nodeid

然后再次运行,就可以发现中文乱码问题已经解决了。