js快速排序 深度图像 bam 自动化部署 video mysqli concurrency grep base64 NEJ lazyloadjs 支付网站建设 jquery获取元素宽度 mac安装hadoop kafka默认端口 pcie转sata pcie高速固态硬盘 idea格式化代码设置 河南普通话报名入口 python平方函数 python基础教程 python开发 python搭建网站 python写入txt文件 java时间戳转换成时间 java案例 java环境安装 java的正则表达式 java搭建 java环境部署 java八大基本数据类型 java注释规范 java停止线程 java删除数组中的某个元素 java中的map java中的泛型 linux教程 心理学与生活txt 高等数学同济第七版 java分布式开发
当前位置: 首页 > 学习教程  > 编程语言

python的测试框架 - pytest 简版教程

2020/12/5 10:07:13 文章标签:

文章目录1.unittest2. nose3. pytest3.1 进阶 - fixture13.2 进阶 - fixture2 参数化3.3 进阶3 - 生成 xml格式得测试报告3.4 进阶4 - 标记3.5 进阶5 - 跳过测试3.6 进阶6 - 外部传参3.7 进阶7 - 测试前运行函数和测试后运行函数3.8 进阶8 - 设置环境变量4. Pytest 使用方法简版…

文章目录

  • 1.unittest
  • 2. nose
  • 3. pytest
    • 3.1 进阶 - fixture1
    • 3.2 进阶 - fixture2 参数化
    • 3.3 进阶3 - 生成 xml格式得测试报告
    • 3.4 进阶4 - 标记
    • 3.5 进阶5 - 跳过测试
    • 3.6 进阶6 - 外部传参
    • 3.7 进阶7 - 测试前运行函数和测试后运行函数
    • 3.8 进阶8 - 设置环境变量
  • 4. Pytest 使用方法简版实战
    • 0. 测试等级参考
    • 1. 测试样例 - pytest_test.py
    • 1. 双击测试代码
    • 2. 命令行使用
  • 5. 参考文献

背景:最近在开发RT-Thread 的一键部署AI模型框架,可以无缝的将AI模型一键移植到RT-Thread 系统上面。待开源…

用 python 写了一堆代码,每次老大问起的时候

“小陈啊,进度怎么样了啊?”

我:“快了快了,不出bug的话今天下午能搞完”

过了两天,“小陈啊,进度怎么样了啊?”

我:“功能还差最后一部分啊”

“测试写了吗?”

我:…

后来我才知道,老大的意思是,问我测试完成几个部分了,而不是功能完成多少了,哎,吃了书读少了的亏

测试驱动开发!万岁!

简单介绍了一下三个python常用的框架,

  • unittest
  • nose
  • [pytest](# 3. pytest)

最终选择的是pytest, 理由是集成了unittest 和 强大的社区生态、插件

以及 [pytest 的简版使用教程](# 4. Pytest 使用方法简版实战)

快速上手 pytest 不是梦,看完自己能编写一个例程!

1.unittest

unittest支持自动化测试,测试用例的初始化和关闭,测试用例的聚合等功能。unittest有一个很重要的特性:它通过类(class)的方式,将测试用例组织在一起。

import unittest

class TestStringMethods(unittest.TestCase):
    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

if __name__ == '__main__':
    unittest.main()

运行单元测试:

# 1. 上述代码保存为 test.py
python test.py

# 2. (推荐)一次批量运行很多单元测试
python -m unittest test

# 输出结果
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

2. nose

先安装:

pip3 install nose

任何函数和类,只要名称匹配一定的条件(例如,以test开头或以test结尾等),都会被自动识别为测试用例,兼容unittest

一个简单的nose单元测试示例如下:

import nose
 
def test_example ():
    pass
 
if __name__ == '__main__':
    nose.runmodule()
~ » python -m unittest test

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

3. pytest

3A 原则:

  1. Arrange 测试数据

  2. Act 调用被测方法

  3. Assert 断言

pytest是Python另一个第三方单元测试库。它的目的是让单元测试变得更容易,并且也能扩展到支持应用层面复杂的功能测试

pytest的特性有:

1)支持用简单的assert语句实现丰富的断言,无需复杂的self.assert*函数

2)自动识别测试模块和测试函数

3)兼容unittest和nose测试集

4)支持Python3和PyPy3

5)丰富的插件生态,已有300多个各式各样的插件,和活跃的社区

pytest一个简单的示例如下:

def inc(x):
    return x +1
 
def test_answer():
    assert inc(3) ==5

文件储存为 test_xpytest.py,在当前目录下执行 pytest

执行结果如下:

~/Templates » pytest
============================= test session starts ==============================
platform linux -- Python 3.7.6, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /home/lebhoryi/Templates
plugins: doctestplus-0.5.0, remotedata-0.3.2, astropy-header-0.1.2, arraydiff-0.3, hypothesis-5.5.4, openfiles-0.4.0
collected 1 item

test_answer.py F                                                         [100%]

=================================== FAILURES ===================================
_________________________________ test_answer __________________________________

    def test_answer():
>       assert inc(3) ==5
E       assert 4 == 5
E        +  where 4 = inc(3)

test_answer.py:5: AssertionError
============================== 1 failed in 0.02s ===============================

以下是测试功能的可能结果:

  • PASSED (.):测试成功。

  • FAILED (F):测试失败(或XPASS + strict)。

  • SKIPPED (s): 测试被跳过。

    你可以使用@pytest.mark.skip()或 pytest.mark.skipif()修饰器告诉pytest跳过测试

  • xfail (x):预期测试失败。@pytest.mark.xfail()

  • XPASS (X):测试不应该通过。

  • ERROR (E):错误

3.1 进阶 - fixture1

准备测试数据和初始化测试对象

假设外部存在user.dev.json 文件,内容如下:

[
  {"name":"jack","password":"Iloverose"},
  {"name":"rose","password":"Ilovejack"},
  {"name":"tom","password":"password123"}
]

新建名为 test_user_password.py 的文件:

import pytest
import json

class TestUserPassword(object):
    @pytest.fixture
    def users(self):
        return json.loads(open('./users.dev.json', 'r').read()) # 读取当前路径下的users.dev.json文件,返回的结果是dict

    def test_user_password(self, users):
        # 遍历每条user数据
        for user in users:
            passwd = user['password']
            assert len(passwd) >= 6
            msg = "user %s has a weak password" %(user['name'])
            assert passwd != 'password', msg
            assert passwd != 'password123', msg

运行:

pytest test_user_password.py

结果:

~/Templates » pytest test_user_password.py
============================= test session starts ==============================
platform linux -- Python 3.7.6, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /home/lebhoryi/Templates
plugins: doctestplus-0.5.0, remotedata-0.3.2, astropy-header-0.1.2, arraydiff-0.3, hypothesis-5.5.4, openfiles-0.4.0
collected 1 item

test_user_password.py F                                                  [100%]

=================================== FAILURES ===================================
_____________________ TestUserPassword.test_user_password ______________________

self = <test_user_password.TestUserPassword object at 0x7fd261cb5cd0>
users = [{'name': 'jack', 'password': 'Iloverose'}, {'name': 'rose', 'password': 'Ilovejack'}, {'name': 'tom', 'password': 'password123'}]

    def test_user_password(self, users):
        # 遍历每条user数据
        for user in users:
            passwd = user['password']
            assert len(passwd) >= 6
            msg = "user %s has a weak password" %(user['name'])
            assert passwd != 'password', msg
>           assert passwd != 'password123', msg
E           AssertionError: user tom has a weak password
E           assert 'password123' != 'password123'

test_user_password.py:16: AssertionError
============================== 1 failed in 0.02s ===============================

数据清理:

import smtplib
import pytest

@pytest.fixture(scope="module")
def smtp():
    smtp = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
    yield smtp  # provide the fixture value
    print("teardown smtp")
    smtp.close()

3.2 进阶 - fixture2 参数化

3.1 进阶 - fixture1 中的 fixture 遇到一个错误案例就弹出,现在改进,执行所有的测试案例

假设外部存在user.dev.json 文件,内容如下:

[
  {"name":"jack","password":"Iloverose"},
  {"name":"rose","password":"Ilovejack"}
  {"name":"tom","password":"password123"},
  {"name":"mike","password":"password"},
  {"name":"james","password":"AGoodPasswordWordShouldBeLongEnough"}
]

新建文件test_user_password_with_params.py,内容如下:

import pytest
import json

class TestUserPasswordWithParam(object):
    @pytest.fixture(params=json.loads(open('./users.test.json', 'r').read()))
    def user(self, request):
        return request.param

    def test_user_password(self, user):
        passwd = user['password']
        assert len(passwd) >= 6
        msg = "user %s has a weak password" %(user['name'])
        assert passwd != 'password', msg
        assert passwd != 'password123', msg

注意:

  1. @pytest.fixture(params=list) 存在,会使得程序执行len(params)
  2. 通过 request.param 来读取参数

~/Templates » pytest test_user_password_with_params.py
============================= test session starts ==============================
platform linux -- Python 3.7.6, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /home/lebhoryi/Templates
plugins: doctestplus-0.5.0, remotedata-0.3.2, astropy-header-0.1.2, arraydiff-0.3, hypothesis-5.5.4, openfiles-0.4.0
collected 5 items

test_user_password_with_params.py ..FF.                                  [100%]

=================================== FAILURES ===================================
_____________ TestUserPasswordWithParam.test_user_password[user2] ______________

self = <test_user_password_with_params.TestUserPasswordWithParam object at 0x7f141f423190>
user = {'name': 'tom', 'password': 'password123'}

    def test_user_password(self, user):
        passwd = user['password']
        assert len(passwd) >= 6
        msg = "user %s has a weak password" %(user['name'])
        assert passwd != 'password', msg
>       assert passwd != 'password123', msg
E       AssertionError: user tom has a weak password
E       assert 'password123' != 'password123'

test_user_password_with_params.py:14: AssertionError
_____________ TestUserPasswordWithParam.test_user_password[user3] ______________

self = <test_user_password_with_params.TestUserPasswordWithParam object at 0x7f141f55c850>
user = {'name': 'mike', 'password': 'password'}

    def test_user_password(self, user):
        passwd = user['password']
        assert len(passwd) >= 6
        msg = "user %s has a weak password" %(user['name'])
>       assert passwd != 'password', msg
E       AssertionError: user mike has a weak password
E       assert 'password' != 'password'

test_user_password_with_params.py:13: AssertionError
========================= 2 failed, 3 passed in 0.03s ==========================
~/Templates »

另外一种执行所有测试案例得方法:

# test_parametrize.py

@pytest.mark.parametrize('passwd',
                      ['123456',
                       'abcdefdfs',
                       'as52345fasdf4'])
def test_passwd_length(passwd):
    assert len(passwd) >= 8

3.3 进阶3 - 生成 xml格式得测试报告

生成junit格式的xml报告

pytest test_quick_start.py --junit-xml=report.xml

3.4 进阶4 - 标记

标记已经完成开发得功能函数和未完成得函数

# test_no_mark.py
def test_func1():
    assert 1 == 1

def test_func2():
    assert 1 != 1
  1. 第一种,显式指定函数名,通过 :: 标记

    pytest tests/test-function/test_no_mark.py::test_func1
    
  2. 第二种,使用模糊匹配,使用 -k 选项标识。

    pytest -k func1 tests/test-function/test_no_mark.py
    
  3. 第三种,使用 pytest.mark 在函数上进行标记。

    # test_with_mark.py
    
    @pytest.mark.finished
    def test_func1():
        assert 1 == 1
    
    @pytest.mark.unfinished
    def test_func2():
        assert 1 != 1
    

    测试时使用 -m 选择标记的测试函数:

    $ pytest -m finished tests/test-function/test_with_mark.py
    

3.5 进阶5 - 跳过测试

Pytest 使用特定的标记 pytest.mark.skip 完美的解决了这个问题。

# test_skip.py

@pytest.mark.skip(reason='out-of-date api')
def test_connect():
    pass

Pytest 还支持使用 pytest.mark.skipif 为测试函数指定被忽略的条件。

@pytest.mark.skipif(conn.__version__ < '0.2.0',
                    reason='not supported until v0.2.0')
def test_api():
    pass

3.6 进阶6 - 外部传参

新建coftest.py文件

  1. conftest.py文件名字是固定的,不可以做任何修改

  2. 文件和用例文件在同一个目录下,那么conftest.py作用于整个目录

  3. conftest.py文件所在目录必须存在__init__.py文件

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

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

# content of conftest.py
import pytest

def pytest_addoption(parser):
    parser.addoption(
        "--cmdopt", action="store", default="type1", help="my option: type1 or type2"
    )

@pytest.fixture
def cmdopt(request):
    return request.config.getoption("--cmdopt")

编写测试用例

import pytest

# content of test_sample.py
def test_answer(cmdopt):
    if cmdopt == "type1":
        print("first")
    elif cmdopt == "type2":
        print("second")
    assert 0  # to see what was printed


if __name__ == '__main__':
    # 使用参数
    pytest.main(['-s', '-q', __file__])
    input()

3.7 进阶7 - 测试前运行函数和测试后运行函数

在某些情况下,需要创建临时工作文件夹,就需要用到这个代码了

def setup_function():
    print("setip_function(): 每个函数之前执行")

def teardown_function():
    print("\nteardown_function(): 每个函数之后执行\n")

3.8 进阶8 - 设置环境变量

在某些情况下,需要自定义一些参数,比如mdk5 路径,

但是,在测试中,使用外部传参的方法是及其不可取的,正确的做法是:

设置环境变量:插件 pytest-env

  1. 新建一个 pytest.ini 的文件

  2. 写入相关环境变量

    [pytest]
    env =
        MDK_PATH=D:/Program Files (x86)/Keil_v5
    
  3. 在对应的测试文件中使用

    os.getenv("MDK_PATH")
    # or
    os.environ["MDK_PATH"]
    

掌握了上述几个技巧差不多就可以使用pytest编写测试案例了,战斗吧,骚年!

(剩下你只需要实战即可)

4. Pytest 使用方法简版实战

update 2020/11/25

  1. add setup and teardown
  2. split test case

0. 测试等级参考

  1. Level1:正常流程可用,即一个函数在输入正确的参数时,会有正确的输出
  2. Level2:异常流程可抛出逻辑异常,即输入参数有误时,不能抛出系统异常,而是用自己定义的逻辑异常通知上层调用代码其错误之处
  3. Level3:极端情况和边界数据可用,对输入参数的边界情况也要单独测试,确保输出是正确有效的
  4. Level4:所有分支、循环的逻辑走通,不能有任何流程是测试不到的
  5. Level5:输出数据的所有字段验证,对有复杂数据结构的输出,确保每个字段都是正确的

作者:gashero
链接:各位都是怎么进行单元测试的? - gashero的回答

1. 测试样例 - pytest_test.py

先安装pytest:

$ pip install pytest pytest-env pytest-html -i https://pypi.doubanio.com/simple
'''
@ Summary: pytest 简版教程
@ Update:  

@ file:    pytest_test.py
@ version: 1.0.0

@ Author:  Lebhoryi@gmail.com
@ Date:    2020/11/19 15:54
'''

import pytest

# def setup_function():
#    print("setip_function(): 每个函数之前执行")

# def teardown_function():
#     print("\nteardown_function(): 每个函数之后执行\n")

def division(a, b):
    return int(a / b)

@pytest.mark.parametrize('a, b, c',
                         [(4, 2, 2), (0, 2, 0), (1, 0, 0), (6, 8, 0), (4, 2, 3)],
                         ids=['整除', '被除数为0', '除数为0', '非整除', '整除失败'])
def test_1(a, b, c):
    '''使用 try...except... 接收异常'''
    try:
        res = division(a, b)
    except Exception as e:
        print('\n发生异常,异常信息{},进行异常断言\n'.format(e))
        assert str(e) == 'division by zero'
    else:
        assert res == c


@pytest.mark.parametrize('a, b, c',
                         [(4, 2, 2), (0, 2, 0), (1, 0, 0), (6, 8, 0), (4, 2, 3)],
                         ids=['整除', '被除数为0', '除数为0', '非整除', '整除失败'])
def test_2(a, b, c):
    '''使用 pytest.raises 接收异常'''
    if b == 0:
        with pytest.raises(ZeroDivisionError) as e:
            division(a, b)
        # 断言异常 type
        assert e.type == ZeroDivisionError
        # 断言异常 value,value 是 tuple 类型
        assert "division by zero" in e.value.args[0]
    else:
        assert division(a, b) == c


def test_read_ini(tmpdir):
    print(tmpdir)      # /private/var/folders/ry/z60xxmw0000gn/T/pytest-of-gabor/pytest-14/test_read0


if __name__ == '__main__':
    pytest.main(['-s', '-q', f"{__file__}"])
    # 倒计时60s
    import time
    for i in range(60, 0, -1):
        print("\r{}秒之后自动关闭窗口!".format(i), end="", flush=False)
        time.sleep(1)
  • 测试数据:
('a, b, c', 
[(4, 2, 2), (0, 2, 0), (1, 0, 0), (6, 8, 0), (4, 2, 3)], 
ids=['整除', '被除数为0', '除数为0', '非整除', '整除失败'])
  • 测试函数:
def division(a, b):
    return int(a / b)
  • 测试代码解读:
    • test_1:

      执行测试函数division(a, b),

      如果有异常,且异常内容为 ‘division by zero’, 则测试通过

      否则断言测试函数结果是否等于c,若等于,则测试通过,若不等于,测试失败

    • test_2:

      b == 0 时,测试函数division(a, b) 抛出ZeroDivisionError,且division by zero 字符串在异常中时,测试通过

      b != 0 时,断言测试函数结果是否等于c,若等于,则测试通过,若不等于,测试失败

1. 双击测试代码

对于不同的测试代码文件,直接双击代码文件。

比如当前文件夹下面存在 pytest_test.py,直接双击,会出现如下界面:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LiaLX5CN-1607132458177)(https://gitee.com/lebhoryi/PicGoPictureBed/raw/master/img/20201119160837.png ‘pytest.py::test_1’)]

仅测试 pytest_test.py 中的 test_1
  • 测试结果:(F 表示失败,. 表示测试用例通过,在下方会显示失败的结果)

    可以看到,前四个测试案例,尽管有一个案例中除数为0,但是测试代码中指定了异常,所以仍能通过测试,

    可是最后一个案例测试失败说明代码写的不完美,可能是异常情况考虑不周全,

完美的理想状态是通过所有的测试用例,即有异常也在指定范围内

这里仅为pytest 使用教程,所以不展开讨论

2. 命令行使用

pytest pytest_test.py::test_1  # :: 是指定测试test_1

运行结果和上面一样,

执行pytest 会自动运行该路径下面所有test开头或者结尾的文件。

这里要说的是第二点,查看详细报告导出html 或者 xml 测试报告

  • 查看详细报告

    pytest pytest_test.py::test_1 -v
    
    ================================================= test session starts =================================================
    platform win32 -- Python 3.7.9, pytest-6.1.2, py-1.9.0, pluggy-0.13.1 -- c:\users\12813\appdata\local\programs\python\python37\python.exe
    cachedir: .pytest_cache
    metadata: {'Python': '3.7.9', 'Platform': 'Windows-10-10.0.18362-SP0', 'Packages': {'pytest': '6.1.2', 'py': '1.9.0', 'pluggy': '0.13.1'}, 'Plugins': {'html': '3.0.0', 'metadata': '1.10.0'}}
    rootdir: D:\Project\utils\test
    plugins: html-3.0.0, metadata-1.10.0
    collected 5 items
    
    pytest_test.py::test_1[\u6574\u9664] PASSED                                                                      [ 20%]
    pytest_test.py::test_1[\u88ab\u9664\u6570\u4e3a0] PASSED                                                         [ 40%]
    pytest_test.py::test_1[\u9664\u6570\u4e3a0] PASSED                                                               [ 60%]
    pytest_test.py::test_1[\u975e\u6574\u9664] PASSED                                                                [ 80%]
    pytest_test.py::test_1[\u6574\u9664\u5931\u8d25] FAILED                                                          [100%]
    
    ====================================================== FAILURES =======================================================
    __________________________________________ test_1[\u6574\u9664\u5931\u8d25] ___________________________________________
    
    a = 4, b = 2, c = 3
    
        @pytest.mark.parametrize('a, b, c',
                                 [(4, 2, 2), (0, 2, 0), (1, 0, 0), (6, 8, 0), (4, 2, 3)],
                                 ids=['整除', '被除数为0', '除数为0', '非整除', '整除失败'])
        def test_1(a, b, c):
            '''使用 try...except... 接收异常'''
            try:
                res = division(a, b)
            except Exception as e:
                print('\n发生异常,异常信息{},进行异常断言\n'.format(e))
                assert str(e) == 'division by zero'
            else:
    >           assert res == c
    E           assert 2 == 3
    E             +2
    E             -3
    
    pytest_test.py:31: AssertionError
    =============================================== short test summary info ===============================================
    FAILED pytest_test.py::test_1[\u6574\u9664\u5931\u8d25] - assert 2 == 3
    ============================================= 1 failed, 4 passed in 0.06s =============================================
    
  • xml

    pytest pytest_test.py::test_1 --junitxml=test.xml
    
  • html

    # install
    pip install pytest-html
    
    pytest pytest_test.py::test_1 --html=test.html
    

5. 参考文献

  • Pytest测试框架

  • Pytest 使用手册

  • Python Pytest 教程

最后的最后,欢迎各位来参加我们的开发者大会呀
在这里插入图片描述


本文链接: http://www.dtmao.cc/news_show_450156.shtml

附件下载

相关教程

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?