断言 assert
编写测试的最基本工具。
assert a == b
assert a <= b
捕获异常
测试的过程中,需要测试是否如期抛出异常,来确定异常处理模块是否生效。
使用 pytest.raises() 来进行异常捕获:
def test_raises():
with pytest.raises(TypeError) as e:
connect('localhost', '6379')
exec_msg = e.value.args[0]
assert exec_msg == 'port type must be int'
该段代码 PASSED。
localhost:6379 尝试连接 redis。
其中 Redis 通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型。
标记函数
pytest 查找测试策略
默认情况下,pytest 会递归查找当前目录下所有以 test 开头或结尾的 python 脚本,并执行文件内的所有以 test 开头或结尾的函数和方法。
# test_no_mark.py
def test_func1():
assert 1 == 1
def test_func2():
assert 1 != 1
直接使用命令行输入 pytest,可以执行所有测试函数。
标记测试函数
由于某种原因(如 test_func2 的功能尚未开发完成),我们只想执行指定的测试函数。在 pytest 中有几种方式可以解决:
一、显式指定函数名,通过 :: 标记
C:\Users\Administrator\Desktop\test\20210407\test_no_mark>pytest test_no_mark.py::test_func1
========================================= test session starts =========================================
platform win32 -- Python 3.9.2, pytest-6.2.3, py-1.10.0, pluggy-0.13.1
rootdir: C:\Users\Administrator\Desktop\test\20210407\test_no_mark
collected 1 item
test_no_mark.py . [100%]
========================================== 1 passed in 0.01s =========================================
二、使用模糊匹配,使用 -k
选项标记
C:\Users\Administrator\Desktop\test\20210407\test_no_mark>pytest -k func1 test_no_mark.py
========================================= test session starts =========================================
platform win32 -- Python 3.9.2, pytest-6.2.3, py-1.10.0, pluggy-0.13.1
rootdir: C:\Users\Administrator\Desktop\test\20210407\test_no_mark
collected 2 items / 1 deselected / 1 selected
test_no_mark.py . [100%]
=================================== 1 passed, 1 deselected in 0.01s =================================
以上两种方法,第一种一次只能指定一个测试函数,当要进行批量测试时无能为力;第二种方法可以批量操作,但需要所有测试的函数名包含相同的模式,也不方便。
三、使用 pytest.mark
在函数上标记
带标记的测试函数为:
# test_with_mark.py
import pytest
@pytest.mark.finished
def test_func1():
assert 1 == 1
@pytest.mark.unfinished
def test_func2():
assert 1 != 1
测试的时候使用 -m
选择标记的测试函数
> pytest -m finished test_with_mark.py
使用 mark 可以给每个函数做上不同标记,测试时指定就可以允许所有被标记的函数。
注意:
一个函数可以打多个标记;多个函数也可以打相同的标记。
运行测试时使用 -m 选项可以加上逻辑,如:
cmd 输入> pytest -m "finished and commit"
cmd 输入> pytest -m "finished and not merged"
跳过测试
我们可以通过标记来使一些函数不被执行,对于那些开发未完成的函数,最好的处理方式为略过而不执行测试。
通过标记指定要跳过的测试
使用特定标记 pytest.mark.skip
实现
# test_skip.py
import pytest
@pytest.mark.skip(reason='out-of-date api')
def test_connect():
pass
命令行输入 pytest test_skip.py 即可
其中,pytest 使用 s 表示测试被跳过
同时,还可以使用 pytest.mark.skipif 来为测试函数指定跳过的条件。
@pytest.mark.skipif(conn.__version__ < '0.2.0',
reason='not supported until v0.2.0')
def test_api():
pass
预见的错误
如果我们事先知道测试函数会执行失败,但又不想直接跳过,而是希望显示的提示。
可使用的是 pytest.mark.xfail
# test_xfail.py
@pytest.mark.xfail(gen.__version__ < '0.2.0',
reason='not supported until v0.2.0')
def test_api():
id_1 = gen.unique_id()
id_2 = gen.unique_id()
assert id_1 != id_2
使用 pytest test_xfail.py 来执行
注意:
pytest 使用 x
表示预见的失败(XFAIL)。
如果预见的是失败,但实际运行测试却成功通过,pytest 使用 X
进行标记(XPASS)。
参数化
当对一个测试函数进行测试时,通常会给函数传递多组参数。比如测试账号登陆,我们需要模拟各种千奇百怪的账号密码。
当然,我们可以把这些参数写在测试函数内部进行遍历。不过虽然参数众多,但仍然是一个测试,当某组参数导致断言失败,测试也就终止了。
通过异常捕获,我们可以保证程所有参数完整执行,但要分析测试结果就需要做不少额外的工作。
在 pytest 中,我们有更好的解决方法,就是参数化测试,即每组参数都独立执行一次测试。使用的工具就是 pytest.mark.parametrize(argnames, argvalues)
。
举个例子:
一个密码长度的测试函数,其中参数名为 passwd,其可选列表包含三个值:
# test_parametrize.py
import pytest
@pytest.mark.parametrize('passwd',
['123456',
'abcdefdds',
'asfdfgfgvfgfg'])
def test_passwd_length(passwd):
assert len(passwd) >= 8
可以看到执行了三次测试,第一次长度没有超过8,所以第一次 Failed
举例2:
多参数的例子,用于校验用户密码
# test_parametrize.py
import pytest
@pytest.mark.parametrize('user, passwd',
[('jack', 'abcdefghijk'),
('tom', 'adgdg23435')])
def test_passwd_md5(user,passwd):
db = {
'jack': '92b9cccc0b98c3a0b8d0df25a421c0e3',
'tom': '78c9713a93986d00f9c489b2e4c53bba'
}
import hashlib
assert hashlib.md5(passwd.encode()).hexdigest() == db[user]
if __name__ == '__main__':
pytest.main('-v test_parametrize.py'.split())
注意:
hashlib 模块
hashlib是一个提供字符串加密功能的模块,包含MD5和SHA的算法,MD5和SHA是摘要算法,也可以称为哈希算法,离散算法。通过一个函数将任意长度的数据转化为一个长度固定的数据串。
比如
import hashlib #导入hashlib模块
md = hashlib.md5() #获取一个md5加密算法对象
md.update('adgdg23435'.encode('utf-8')) #制定需要加密的字符串
print(md.hexdigest()) #获取加密后的16进制字符串
结果:
78c9713a93986d00f9c489b2e4c53bba
***Repl Closed***
这个即为 tom 的加密字符串。
这样得到 jack 和 tom 的加密密码后,放在我们的用户密码验证的例子里,运行结果可以为:
============================= test session starts =============================
platform win32 -- Python 3.9.2, pytest-6.2.3, py-1.10.0, pluggy-0.13.1 -- D:\Python\Python39\python.EXE
cachedir: .pytest_cache
rootdir: C:\Users\Administrator\Desktop\test\20210407\test_parametrize
collecting ... collected 2 items
test_parametrize.py::test_passwd_md5[jack-abcdefghijk] PASSED [ 50%]
test_parametrize.py::test_passwd_md5[tom-adgdg23435] PASSED [100%]
============================== 2 passed in 0.02s ==============================
***Repl Closed***
如果觉得每组测试的默认参数显示不清晰,可以使用 pytest.param 中的 id 参数来自定义。
# test_parametrize.py
import pytest
@pytest.mark.parametrize('user, passwd',
[pytest.param('jack','abcdefghijk',id='User<Jack>'),
pytest.param('tom','adgdg23435',id='User<Tom>')])
def test_passwd_md5(user,passwd):
db = {
'jack': '92b9cccc0b98c3a0b8d0df25a421c0e3',
'tom': '78c9713a93986d00f9c489b2e4c53bba'
}
import hashlib
assert hashlib.md5(passwd.encode()).hexdigest() == db[user]
if __name__ == '__main__':
pytest.main('-v test_parametrize.py'.split())