美文网首页
python3 pytest (1) - 基本介绍

python3 pytest (1) - 基本介绍

作者: halfempty | 来源:发表于2018-12-10 21:36 被阅读0次

1 前言

pytest,作为一款测试框架,并没有继续模仿junit层次分明的工作模式,以至于读完官网文档都感觉是懵的

  • 虽然没有明确定义setup/teardown方法,却有其独特的手段实现相同的功能
  • 流程控制建立在function的基础上,粒度或许更细,但并不容易上手
  • fixture功能过于强大,强大到你都不知道如何使用它
  • 各种插件,还支持自定义

2 Fixtures

不同的unittest中的fixture,pytest中的fixture功能更强大,不仅能实现setup/teardown,还能实现参数化

2-1 setup/teardown

通过指定fixture的scope属性,可以实现session, module, class, function级别的控制;使用yield或者request.addfinalizer,可以在一个方法内实现setup 和 teardown两种行为

yield vs request.addfinalizer

  • addfinalizer可以指定多个方法
  • 即使setup阶段失败,addfinalizer定义的行为依然会执行

assert 0是为了输出日志

import pytest

@pytest.fixture(scope="session")
def session_scope():
    print("session scope setup")
    yield
    print("session scope teardown")

@pytest.fixture(scope="module")
def module_scope():
    print("module scope setup")
    yield
    print("module scope teardown")

@pytest.fixture(scope="function")
def function_scope(request):
    print("function setup")
    request.addfinalizer(lambda : print("function teardown"))

@pytest.mark.usefixtures("session_scope", "module_scope", "function_scope")
class TestClass(object):
    def test_1(self):
        print("run test_1")
        assert 0

    def test_2(self):
        print("run test_2")
        assert 0

if __name__ == "__main__":
    pytest.main()
==================================
session scope setup
module scope setup
function setup
run test_1
function teardown
function setup
run test_2
function teardown
module scope teardown
session scope teardown

2-2 fixture参数化

指定params属性,实现fixture的参数化,引用该fixture的测试方法将遍历全部参数

import pytest

@pytest.fixture(params=["unittest", "pytest"])
def fix1(request):
    print(request.param)

def test_main(fix1):
    assert 0

if __name__ == "__main__":
    pytest.main(["-v"])
===============================
____ test_main[unittest] ______
unittest
____ test_main[pytest] ______
pytest

2-3 fixture嵌套/组合/覆盖

嵌套指一个fixture引用另一个fixture

import pytest

@pytest.fixture
def fix1():
    print("call fix1")

@pytest.fixture
def fix2(fix1):
    print("call fix2")

def test_main(fix2):
    assert 0

if __name__ == "__main__":
    pytest.main()
================================
call fix1
call fix2

组合指多个fixture实现迪卡尔乘积组合

import pytest

@pytest.fixture(params=["unittest", "pytest"])
def fix1(request):
    yield request.param

@pytest.fixture(params=["python", "java"])
def fix2(request):
    yield request.param

def test_main(fix1, fix2):
    print("{} - {}".format(fix1, fix2))
    assert 0

if __name__ == "__main__":
    pytest.main(["-v"])
============================
unittest - python
unittest - java
pytest - python
pytest - java

覆盖类似于变量的就近原则,本地fixture优先级高于上级或全局

这里就不举例子,可以自行尝试

2-4 Request参数

通过request参数,可以获取大量信息,包括config、fixturename,以及module、cls、function等

import pytest

@pytest.fixture
def fix1(request):
    for item in dir(request):
        if item.startswith("_"):
            continue
        print("{:18} => {}".format(item, getattr(request, item)))

def test_1(fix1):
    assert 0

if __name__ == "__main__":
    pytest.main(["-v"])
====================================
addfinalizer       => <bound method SubRequest.addfinalizer of <SubRequest 'fix1' for <Function 'test_1'>>>
applymarker        => <bound method FixtureRequest.applymarker of <SubRequest 'fix1' for <Function 'test_1'>>>
cached_setup       => <bound method FixtureRequest.cached_setup of <SubRequest 'fix1' for <Function 'test_1'>>>
cls                => None
config             => <_pytest.config.Config object at 0x000002A19C0B8588>
fixturename        => fix1
fixturenames       => ['fix1', 'request']
fspath             => E:\code\python\lab\_logging\test_pytest.py
funcargnames       => ['fix1', 'request']
function           => <function test_1 at 0x000002A19C227400>
getfixturevalue    => <bound method FixtureRequest.getfixturevalue of <SubRequest 'fix1' for <Function 'test_1'>>>
getfuncargvalue    => <bound method FixtureRequest.getfuncargvalue of <SubRequest 'fix1' for <Function 'test_1'>>>
instance           => None
keywords           => <NodeKeywords for node <Function 'test_1'>>
module             => <module 'test_pytest' from 'E:\\code\\python\\lab\\_logging\\test_pytest.py'>
node               => <Function 'test_1'>
param_index        => 0
raiseerror         => <bound method FixtureRequest.raiseerror of <SubRequest 'fix1' for <Function 'test_1'>>>
scope              => function
session            => <Session '_logging'>

比如读取module, class中的属性

import pytest

module_var = "module var "

@pytest.fixture
def fix1(request):
    print(getattr(request.module, "module_var"))
    print(getattr(request.cls, "class_var"))

class TestClass(object):
    class_var = "class var"

    def test_1(self, fix1):
        assert 0

if __name__ == "__main__":
    pytest.main(["-v"])
======================================
module var 
class var

3 Mark

3-1 数据驱动

import pytest

@pytest.mark.parametrize(("a", "b", "expected"), [
    [1, 2, 3],
    [10, 11, 21],
    [1, 1, 1],
])
def test_1(a, b, expected):
    assert a + b == expected

if __name__ == "__main__":
    pytest.main(["-v"])
==================================
test_pytest.py::test_1[1-2-3] PASSED        [ 33%]
test_pytest.py::test_1[10-11-21] PASSED     [ 66%]
test_pytest.py::test_1[1-1-1] FAILED        [100%]

3-2 用例标记

自定义用例标签,可以指定运行,类似于group的功能

import pytest

@pytest.mark.hehe
def test_1():
    assert 0

@pytest.mark.haha
def test_2():
    assert 0

if __name__ == "__main__":
    pytest.main(["-m", "haha"])
======================================
//只运行test_2

4 pytest_generate_tests

借用官网描述

Sometimes you may want to implement your own parametrization scheme or implement some dynamism for determining the parameters or scope of a fixture. For this, you can use the pytest_generate_tests hook which is called when collecting a test function. Through the passed in metafunc object you can inspect the requesting test context and, most importantly, you can call metafunc.parametrize() to cause parametrization.

定制加载外部数据,此处可以根据cls名称读取数据文件(txt, csv等),并将结果添加到parametrize方法内

import pytest

def pytest_generate_tests(metafunc):
    if metafunc.cls == TestClass:
        func_args = metafunc.cls.datas[metafunc.function.__name__]
        keys = sorted(func_args[0])
        metafunc.parametrize(keys, [[func_arg[key] for key in keys] for func_arg in func_args])

class TestClass(object):
    datas = {
        "test_1": [
            {"k1": 1, "k2": 1, "ret": 2},
        ],
        "test_2": [
            {"num1": 11, "num2": 12},
            {"num1": 0, "num2":1}
        ]
    }

    def test_1(self, k1, k2, ret):
        assert k1 + k2 == ret

    def test_2(self, num1, num2):
        assert num1 > num2

if __name__ == "__main__":
    pytest.main(["-v"])
==============================
test_pytest.py::TestClass::test_1[1-1-2] PASSED    [ 33%]
test_pytest.py::TestClass::test_2[11-12] FAILED    [ 66%]
test_pytest.py::TestClass::test_2[0-1] FAILED      [100%]

类似的hook方法还有很多,具体可以参考hookspec.py

5 monkeypatch

当待测试对象依赖于网络、数据库时,通过monkeypath可以略过与第三方的交互,直接指定期望的结果,即实现mock操作

import pytest
import requests

def visit(url):
    r = requests.get(url=url)
    return r

def test_1(monkeypatch):
    monkeypatch.setattr(requests, name="get", value=lambda url: "Hello")
    status = visit("https://www.baidu.com")
    assert status == "Hello"

if __name__ == "__main__":
    pytest.main(["-v"])

其实monkeypatch就是一个fixture,内置的还有tmpdir, reports, unittest, runner等,具体可以查看_pytest目录

相关文章

网友评论

      本文标题:python3 pytest (1) - 基本介绍

      本文链接:https://www.haomeiwen.com/subject/elfjhqtx.html