如果对你有帮助,就点个赞吧~
本文主要介绍如果编写Python的单元测试,包括如何使用断言,如何考虑测试哪些情况,如何避免外部依赖对测试的影响,如果用数据驱动的方式简化重复测试的编写等等等等
文章目录
- 单元测试相关说明
- 1. 测试的基础:使用断言
- 2. 多组测试数据的简洁写法
- 3. 测试程序崩溃的情况
- 4. 简化测试代码的组合技
- 5. 使用Mock对象及方法以解除外部依赖(如网页接口或数据库请求,Redis请求,时间戳生成,随机数生成等)
- 6. pytest的fixtrue
- 7. pytest常用的marker
- 8. pytest的常用运行参数
- 9. pytest的配置以及pre-process和post-process相同流程的自动化
- Appendix A
Code without tests is bad code. It doesn’t matter how well written it is; it doesn’t matter how pretty or object-oriented
or well-encapsulated it is. With tests, we can change the behavior of our code quickly and verifiably. Without them,
we really don’t know if our code is getting better or worse. ---- Michael C. Feathers Working Effective with Legacy Code
缺少测试的代码不能称之为好代码。这无关于代码写得多好,也无关于代码是否简洁,是否是面对对象,是否进行了良好的封装。
通过测试,我们可以迅速且可验证地改变代码的行为。而缺少测试的情况下,我们无从得知我们的代码是否得到优化,抑或是变得更糟。
---- 迈克尔·C·费瑟斯 《修改代码的艺术》
from typing import Union
def is_leap_year(cls, year: Union[str, int]):
""" 检验某个年份是否是闰年
校验规则: 四年一润,百年不润,四百年再润
Args:
year (int): 要校验的年份
Notices:
1. 存在对传入参数的检测, 不允许传入参数类型为str且以0开头
2. 当前没有对传入参数为负数的情况进行限制
Returns:
bool: True - 闰年
False - 平年
"""
if isinstance(year, str):
assert len(str(int(year))) == len(year), f"Invalid parameter: year, value: {year}"
year = int(year)
if year % 400 == 0:
return True
elif year % 100 == 0:
return False
elif year % 4 == 0:
return True
return False
假设要测试4种情况:
测试样例如下
def test_is_leap_year_true_input_int():
""" 输入为int类型,且对应闰年
预期: 返回True
"""
assert is_leap_year(2008) == True
def test_is_leap_year_false_input_int():
""" 输入为int类型,且对应平年
预期: 返回True
"""
assert is_leap_year(2007) == False
def test_is_leap_year_false_input_str():
""" 输入为str类型,且对应闰年
预期: 返回True
"""
assert is_leap_year("2007") == False
def test_is_leap_year_true_input_str():
""" 输入为str类型,且对应平年
预期: 返回True
"""
assert is_leap_year("2008") == True
上述4个测试函数是根据函数传入参数的类型以及值是否对应闰年两两组合得到的四个测试样例,也分别对应四个测试等价类
由于测试需要划分测试等价类,于是会有多组测试用例. 比如对于一个判断分数是否大于等于60从而给出是否及格的判断函数,小于60分与大于等于60分就是该方法的测试等价类
一个测试方法对于一个测试等价类得到的结果应该是相同的,而测试样例需要从每一个测试等价类中取一个具体的数据作为测试该等价类的测试用例
上述is_leap_year的测试等价类自然不止4种,但划分等价测试类不是我们这部分要讨论的重点
这里要讨论的是对于此类传入参数极其简单,测试描述可以完全直接反映在测试函数名称,测试数据以及测试预期结果的情况,有如下方法可以简化代码
极其不推荐,一个测试函数中最好只有一个assert, 否则测试不通过时定位错误需要额外定位是哪组测试数据通过)
(适合一个测试函数多个assert的情况:为提高可读性将一个assert拆分成多个assert,本质上是对一次函数测试进行多层次的assert)
def test_is_leap_year():
test_data = (
(2008, True),
(2007, False),
("2008", True),
("2007", False),
)
for test_input, expected_result in test_data:
assert is_leap_year(test_input) == expected_result
(与方法1的差别在于,pytest会自动对测试数据进行分拆且以类似测试函数后缀的形式区分不同样例, 同时可以通过传入ids参数给@pytest.mark.parametrize()
为用例名称赋值)
import pytest
@pytest.mark.parametrize("test_year,expected_result", [("2000", True),
("2008", True),
(2008, True),
(400, True),
("1900", False)])
def test_is_leap_year(test_year, expected_result):
assert is_leap_year(test_year) == expected_result
在上述is_leap_year()
中存在一种异常情况,即当传入参数为字符串且以0开头时, 会直接抛出异常, 这种情况程序会在运行时报错。而我们可以在单元测试中测试在异常发生时,程序的处理行为是否与预期一致。
测试程序按预期抛出异常或处理异常的方法如下:
def test_is_leap_year():
try:
is_leap_year("0400")
except Exception as excep:
assert isinstance(excep,AssertionError)
assert str(excep) == "Invalid parameter: year, value: 0400"
import unittest
class TestExceptionCase(unittest.TestCase):
def test_invalid_input(self):
self.assertRaises(AssertionError, is_leap_year, "0400")
self.assertRaisesRegex(AssertionError, "Invalid parameter: year, value: 0400", is_leap_year, "0400")
import pytest
@pytest.mark.xfail(raises=AssertionError)
def test_invalid_input():
is_leap_year("0400")
def test_invalid_input_2():
with pytest.raises(AssertionError, match='Invalid parameter: year, value: 0400'):
is_leap_year("0400")
pytest.mark.paramtrize
装饰器来进行多组测试数据的输入pytest.param()
, 并将marks参数传入pytest.markd.xfail
来预期该组测试数据将使单元测试的结果不通过,即assert语句抛出AssertError@pytest.mark.parametrize("test_year,expected_result", [("2000", True),
("2008", True),
(2008, True),
(400, True),
(-400, True),
("-400", True),
pytest.param("0400", False, marks=pytest.mark.xfail),
pytest.param("0400", True, marks=pytest.mark.xfail),
("1900", False)])
def test_is_leap_year(test_year, expected_result):
assert is_leap_year(test_year) == expected_result
以下例程为不进行Mock,完全依赖于httpbin.org这个网站返回情况的情况
"""
对https://httpbin.org/get进行请求会返回所有请求中的参数,在返回体的args中
https://httpbin.org/get?text=Hello World的返回如下
{
"args": {
"text": "Hello World"
},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Host": "httpbin.org",
"Sec-Ch-Ua": "\"Google Chrome\";v=\"87\", \" Not;A Brand\";v=\"99\", \"Chromium\";v=\"87\"",
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36",
"X-Amzn-Trace-Id": "Root=1-6017d21d-395b4a050a9c34904fccf3ea"
},
"origin": "108.166.207.187",
"url": "https://httpbin.org/get?text=Hello World"
}
"""
import requests
def test_request_parameters():
parameters = {'Hello': 'World'}
parameter_pairs = [f"{param_name}={param_value}" for param_name, param_value in parameters.items()]
resp = requests.get(f"https://httpbin.org/get?{'&'.join(parameter_pairs)}")
assert resp.status_code == 200, f"Expect Response Status Code 200, But Got {resp.status_code}"
# assert resp.status_code == 500, f"Expect Response Status Code 500, But Got {resp.status_code}"
assert resp.json()["args"] == parameters
def test_request_parameters_poor_readability():
parameters = {'text': 'Hello World'}
resp = requests.get(f"https://httpbin.org/get?{'&'.join([key+'='+value for key, value in parameters.items()])}")
assert resp.json()["args"] == parameters
test_request_parameters()
上述测试的问题在于: 其Assert语句是否为真完全依赖于httpbin.org的实际返回
当我们希望测试请求成功,返回状态码为200的情况时,我们写下:
assert resp.status_code == 200, f"Expect Response Status Code 200, But Got {resp.status_code}
当该网站服务正常时,我们的测试将顺利通过。但当网站的服务出现异常的时候,我们的单元测试将不通过。
从另外一个角度,如果我们试图测试返回异常的情况,我们写下:
assert resp.status_code == 500, f"Expect Response Status Code 500, But Got {resp.status_code}
则该测试只能在网站请求异常时通过。
而我们希望的时我们的测试在任何时间,任何物理地点以及任何软件环境下能有相同的行为,能保证其测试的独立性, 于是我们需要使用Mock来实现这一点
顾名思义,mock的本质是通过模仿指定对象的行为来实现对其解除依赖的目的。我们能自由定义用来替换外部依赖的mock对象的一切行为,包括且不限于属性,方法,方法的返回值。
注意: python2.x中mock没有被纳入标准库, 需要pip install mock安装, 且是以import mock的方式导入,而不是from unittest import mock
from unittest import mock
mock_example = mock.Mock()
print(mock_example)
print(mock_example())
# 输出:
#
#
在python中, 利用unittest中的mock可以Mock出对象
在我们为对其行为进行定义前,其行为都为各种mock对象,如上述mock_example,我们未对其定义__call__方法而直接调用的话将返回另一个mock对象
而我们可以对其行为进行如下定义:
mock_example.return_value = "Result when call mock_example"
mock_example()
# 输出:
# 'Result when call mock_example'
mock_example.status_code = 200
mock_example.status_code
# 输出
# 200
需要注意的是, 由于我们定义的是方法,即当我们定义方法mock_method
后,将以mock_method()
的形式调用它, 这与刚才我们直接将mock对象当作函数调用时的形式一致。
故我们刚才通过定义mock对象的return_value
来定义函数返回值, 则此时也是通过定义mock_method.return_value
来定义这个方法的返回值
mock_example.say_hi.return_value = "Hi!"
mock_example.say_hi()
# 输出
# 'Hi!'
有了对mock的基本了解,重新分析5.1中模拟并替换requests库的get方法,则可以有如下实现:
import requests
from unittest import mock
def test_request_parameters_mock_get():
parameters = {'text': 'Hello World'}
mock_response = mock.Mock(status_code=200)
mock_response.json.return_value = {"args": parameters}
requests = mock.Mock()
requests.get.return_value = mock_response
parameter_pairs = [f"{param_name}={param_value}" for param_name, param_value in parameters.items()]
resp = requests.get(f"https://httpbin.org/get?{'&'.join(parameter_pairs)}")
assert resp.status_code == 200, f"Expect Response Status Code 200, But Got {resp.status_code}"
assert resp.json()["args"] == parameters
利用pytest,我们可以提升一下上述代码的简洁性和可读性
import requests
from unittest import mock
""" usage: @mock.patch("the_import_path_of_the_object_you_wanna_mock")
like: @mock.patch("src.utils_base.redis_client.RedisHub") # 对应 from src.utils_base.redis_client import RedisHub
like: @mock.patch("src.utils_base.redis_client.RedisHub.rpush") # 对应 from src.utils_base.redis_client import RedisHub; RedisHub.rpush()
将用来以假乱真的mock对象的变量名作为传入参数添加进被测试函数. 如上一个代码块中的requests对象.
由于我们只需要用到requests的get方法, 可以直接mock该方法, 而不用去mock外层的requests库
即我们将用requests_get来替换request.get方法, 以resp = requests_get(f"https://httpbin.org/get?{'&'.join(parameter_pairs)}")的形式调用
为了避免混淆, 增加可读性, 我们增加一个mock_前缀给变量名来表示其性质, 故以下代码中变量名为mock_requests_get
"""
@mock.patch("requests.get")
def test_request_parameters_mock_get(mock_requests_get):
parameters = {'text': 'Hello World'}
mock_response = mock.Mock(status_code=200)
mock_response.json.return_value = {"args": parameters}
mock_requests_get.return_value = mock_response
parameter_pairs = [f"{param_name}={param_value}" for param_name, param_value in parameters.items()]
resp = requests.get(f"https://httpbin.org/get?{'&'.join(parameter_pairs)}")
assert resp.status_code == 200, f"Expect Response Status Code 200, But Got {resp.status_code}"
assert resp.json()["args"] == parameters
假设我们有一个代码块需要对当前运行程序的环境进行判断并对应不同环境执行不同操作,如下:
import platform
import socket
import os
def detect_environment():
ENV_DEV = "DEV"
ENV_TEST = "TEST"
ENV_PROD = "PROD"
OS_TYPE = platform.system().upper()
HOST_NAME = socket.gethostname()
IP_ADDR = socket.gethostbyname(HOST_NAME)
if OS_TYPE == "WINDOWS" or HOST_NAME.lower().startswith('万物皆可MOCK'):
ENV = ENV_DEV
elif HOST_NAME.lower().find("sit") > -1 or IP_ADDR == "10.80.62.161":
ENV = ENV_TEST
else:
ENV = ENV_PROD
return ENV
则在不进行mock的情况下, 我们的OS类型, 主机名, IP地址将根据实际情况变化
比如当前我在Windows操作系统中, IP地址为192.168.21.84
则程序运行结果如下:
print(f"Current ENV: {detect_environment()}")
# 输出
# Current ENV: DEV
假设我们希望测试其他环境的程序行为,则可以通过mock操作系统, IP, 主机名来实现
platform = mock.Mock()
platform.system.return_value = "linux"
print(platform.system())
print(platform.system().upper()) # uppper()方法是字符串自带的不需要MOCK
print(f"Current ENV: {detect_environment()}")
通过detect_environment()
的实现逻辑, 我们知道就算当前的操作系统是linux, 但只要我们的主机名以"万物皆可MOCK"开头, 当前环境也会被识别为"DEV"
于是,我们现在就将我们的主机名改为以"万物皆可MOCK"作为前缀的主机名
socket = mock.Mock()
socket.gethostname.return_value = "万物皆可MOCK,连MOCK都可以MOCK"
print(f"Current HostName: {socket.gethostname()}")
print(f"Current ENV: {detect_environment()}")
# 输出
# Current HostName: 万物皆可MOCK,连MOCK都可以MOCK
# Current ENV: DEV
如果我们只是希望detect_environment()能够将我们当前环境识别为TEST, 则除了mock当前IP、操作系统、主机名之外, 其实我们可以……直接mockdetect_environment, 令其return_value为"TEST"即可
假设我们利用了redis来存储少数SVIP用户ID, 并在程序中通过检查用户ID是否存在于Redis中来决定是否执行SVIP行为, 如下:
import redis
def check_whether_SVIP(user_id: str):
redis_con = redis.StrictRedis(host="localhost", port=6039, db=0, password="nopassword")
target_key = "the_place_stored_svip_id"
if redis_con.sismember(target_key, user_id):
print(f"{user_id} is SVIP.")
else:
print(f"{user_id} is not SVIP.")
目前为止, 我们mock函数或方法时都是定义其return_value使其在被调用时固定返回我们希望的值
如果我们希望调用时根据传入参数的不同而有不同的返回,也即我们希望定义函数对参数进行判断后决定该返回何值时, 我们就可以使用「side_effect」, 如下:
import pytest
import redis
def test_check_whether_SVIP():
def judge_logic(user_id):
return True if user_id == "ef30c771-eaa8-4bab-b666-8061eee53610" else False
check_whether_SVIP = mock.Mock()
check_whether_SVIP.side_effect = judge_logic
assert check_whether_SVIP("ef30c771-eaa8-4bab-b666-8061eee53610") == True
test_check_whether_SVIP()
当然,也可以通过对redis连接以及redis对其数据结构的操作来达到我们的目的。但我们只希望能够在传入"ef30c771-eaa8-4bab-b666-8061eee53610"
给check_whether_SVIP
得到True,其他情况返回False, 只对check_whether_SVIP
进行mock就足够了。
import pytest
from unittest import mock
import redis
def test_check_whether_SVIP():
""" 测试check_whether_SVIP是否只被调用一次
同时测试check_whether是否以参数「ef30c771-eaa8-4bab-b666-8061eee53610」被调用
"""
def judge_logic(user_id):
return True if user_id == "ef30c771-eaa8-4bab-b666-8061eee53610" else False
check_whether_SVIP = mock.Mock()
check_whether_SVIP.side_effect = judge_logic
check_whether_SVIP("ef30c771-eaa8-4bab-b666-8061eee53610")
# check_whether_SVIP("ef30c771-eaa8-4bab-b666-8061eee53610")
check_whether_SVIP.assert_called_once()
check_whether_SVIP.assert_called_once_with("ef30c771-eaa8-4bab-b666-8061eee53610")
test_check_whether_SVIP()
assert_called_once()
来判断check_whether_SVIP()
是否只被调用一次
assert_called_once_with("ef30c771-eaa8-4bab-b666-8061eee53610")
来判断check_whether_SVIP()
只被调用一次,且被调用时,传入参数是否为"ef30c771-eaa8-4bab-b666-8061eee53610"
需要注意的是: assert_called_once()
这类断言函数,本身自带assert
的功能,check_whether_SVIP.assert_called_once()
在断言成立时没有返回值(也即返回None),故该check_whether_SVIP.assert_called_once()
不可以加assert
关键字,否则会在check_whether_SVIP.assert_called_once()
成立时外部抛出AssertError.
我们可以尝试多次调用check_whether_SVIP
,使得assert_called_once()
失败来查看该情况下的报错信息:
import pytest
from unittest import mock
import redis
def test_check_whether_SVIP():
def judge_logic(user_id):
return True if user_id == "ef30c771-eaa8-4bab-b666-8061eee53610" else False
check_whether_SVIP = mock.Mock()
check_whether_SVIP.side_effect = judge_logic
check_whether_SVIP("ef30c771-eaa8-4bab-b666-8061eee53610")
check_whether_SVIP("ef30c771-eaa8-4bab-b666-8061eee53610")
check_whether_SVIP.assert_called_once()
check_whether_SVIP.assert_called_once_with("ef30c771-eaa8-4bab-b666-8061eee53610")
test_check_whether_SVIP()
# 输出
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
<ipython-input-47-3674c6a2e28f> in <module>
13 check_whether_SVIP.assert_called_once_with("ef30c771-eaa8-4bab-b666-8061eee53610")
14
---> 15 test_check_whether_SVIP()
<ipython-input-47-3674c6a2e28f> in test_check_whether_SVIP()
10 check_whether_SVIP("ef30c771-eaa8-4bab-b666-8061eee53610")
11 check_whether_SVIP("ef30c771-eaa8-4bab-b666-8061eee53610")
---> 12 check_whether_SVIP.assert_called_once()
13 check_whether_SVIP.assert_called_once_with("ef30c771-eaa8-4bab-b666-8061eee53610")
14
E:\zhangzhaobin\software\anaconda3\lib\unittest\mock.py in assert_called_once(self)
890 self.call_count,
891 self._calls_repr()))
--> 892 raise AssertionError(msg)
893
894 def assert_called_with(self, /, *args, **kwargs):
AssertionError: Expected 'mock' to have been called once. Called 2 times.
Calls: [call('ef30c771-eaa8-4bab-b666-8061eee53610'),
call('ef30c771-eaa8-4bab-b666-8061eee53610')].
我们也可以尝试断言check_whether_SVIP
以与实际调用参数不一致的参数被调用,使得assert_called_once_with()
失败来查看这种情况下的报错信息:
import pytest
from unittest import mock
import redis
def test_check_whether_SVIP():
""" 由于我们调用check_whether_SVIP()时传入参数为"ef30c771-eaa8-4bab-b666-8061eee53610"而不是"XXXXXXXX"
所以当我们断言heck_whether_SVIP以参数为"XXXXXXXX"被调用时将抛出AssertError
"""
def judge_logic(user_id):
return True if user_id == "ef30c771-eaa8-4bab-b666-8061eee53610" else False
check_whether_SVIP = mock.Mock()
check_whether_SVIP.side_effect = judge_logic
check_whether_SVIP("ef30c771-eaa8-4bab-b666-8061eee53610")
check_whether_SVIP.assert_called_once()
check_whether_SVIP.assert_called_once_with("XXXXXXXXXXXXXXXXXXX")
test_check_whether_SVIP()
# 输出
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
<ipython-input-48-f2f2a5b20e3d> in <module>
12 check_whether_SVIP.assert_called_once_with("XXXXXXXXXXXXXXXXXXX")
13
---> 14 test_check_whether_SVIP()
<ipython-input-48-f2f2a5b20e3d> in test_check_whether_SVIP()
10 check_whether_SVIP("ef30c771-eaa8-4bab-b666-8061eee53610")
11 check_whether_SVIP.assert_called_once()
---> 12 check_whether_SVIP.assert_called_once_with("XXXXXXXXXXXXXXXXXXX")
13
14 test_check_whether_SVIP()
E:\zhangzhaobin\software\anaconda3\lib\unittest\mock.py in assert_called_once_with(self, *args, **kwargs)
923 self._calls_repr()))
924 raise AssertionError(msg)
--> 925 return self.assert_called_with(*args, **kwargs)
926
927
E:\zhangzhaobin\software\anaconda3\lib\unittest\mock.py in assert_called_with(self, *args, **kwargs)
911 if expected != actual:
912 cause = expected if isinstance(expected, Exception) else None
--> 913 raise AssertionError(_error_message()) from cause
914
915
AssertionError: expected call not found.
Expected: mock('XXXXXXXXXXXXXXXXXXX')
Actual: mock('ef30c771-eaa8-4bab-b666-8061eee53610')
假设我们有一个测试数据会被用在多个格式化方法的测试中,如下:
def clean_role(role: str) -> str:
""" 对表示角色的文本进行数据清洗.
规则: 去除文本末尾的?
"""
return role.rstrip("?")
def clean_role_type(role_type: str) -> str:
""" 对表示角色类型的文本进行数据清洗.
规则: 去除文本中的⚜
"""
return role_type.replace("⚜", "")
def test_clean_role():
role_info = {
"name": "游戏角色⚜",
"role": "不死族???"
}
assert clean_role(role_info["role"]) == "不死族"
def test_clean_role_type():
role_info = {
"name": "游戏角色⚜",
"role": "不死族???"
}
assert clean_role_type(role_info["name"]) == "游戏角色"
test_clean_role()
test_clean_role_type()
test_clean_role()
和test_clean_role_type()
两个测试中使用了相同的测试数据,但是需要在两个测试函数中存在相同的局部变量来表示该测试数据。
这种情况我们可以通过将上述role_info提取出来作为全局变量,但全局变量的存在意味变量的值中途变化导致测试输入与预期不一致的风险。
为了避免测试输入发生意料之外的变化的同时减少重复代码,可以使用pytest的fixture
,其创建语法及在上述例子中的替换方法如下:
import pytest
@pytest.fixture
def role_info():
return { "name": "游戏角色⚜", "role": "不死族???" }
def clean_role(role: str) -> str:
""" 对表示角色的文本进行数据清洗.
规则: 去除文本末尾的?
"""
return role.rstrip("?")
def clean_role_type(role_type: str) -> str:
""" 对表示角色类型的文本进行数据清洗.
规则: 去除文本中的⚜
"""
return role_type.replace("⚜", "")
def test_clean_role(role_info):
assert clean_role(role_info["role"]) == "不死族"
def test_clean_role_type(role_info):
assert clean_role_type(role_info["name"]) == "游戏角色"
fixture的默认作用范围是function级别,这意味着test_clean_role(role_info)与test_clean_role_type(role_info)这两个函数传入的参数是独立的,可以认为各自是role_info()返回值的一个深拷贝。
fixture的可选级别有: function, class, module, package, session. 根据情况可以选择不同的作用域。
常用的marker有: skip
, skipif
, xfail
, parametrize
skip
的作用是直接跳过指定的单元测试(加了@pytest.mark.skip(reason="why this test would be skipped")
的测试都会被跳过)
skipif
的作用是在if条件满足时跳过指定的单元测试,例如在python版本小于3.8.0时不运行指定测试,在项目版本小于2.0时不运行指定测试等等
xfail
的作用是标记预期失败的测试,标记了@pytest.mark.xfail(True, reason="Observe XFAIL Behaviors", raises=AssertionError, strict=True)
的测试需要不通过测试才算通过测试(这句话可真有意思Σ(゚Д゚))
parametrize
的作用是将测试数据集参数化,即我们可以将测试输入和测试输出作为一组参数传入,该装饰器将自动将输入输出映射到测试函数的参数中并运行测试。通过该装饰器也可以实现文件输入,如下:
current_dir = os.path.dirname(os.path.abspath(__file__))
df = pd.read_csv(f"{current_dir}/change_item_to_change_type_statistics.csv")
df = df.fillna("")
"""
change_item_to_change_type_statistics.csv内容如下:
"change_item","type","cnt"
"经营范围变更","经营范围变更","19655404"
"","","16487298"
"其他事项备案","其他变更","14084373"
"章程备案","其他变更","10999454"
"经营范围变更(含业务范围变更)","经营范围变更","10552502"
"经营范围","经营范围变更","10148886"
"""
@pytest.mark.parametrize("change_item, expected_result", list(zip(df.change_item, df.type)))
def test_classify_change_item(change_item, expected_result):
result = ChangeRecFormatter.classify_change_item(change_item)
assert result == expected_result
选项 | 作用 | 示例 |
---|---|---|
-k EXPRESSION | 只运行匹配给定表达式的测试. 匹配指的是传入关键词为单元测试函数名称或类名称的子串. 表达式中可以使用python语法的与或非。关键字匹配不区分大小写。 | -k 'test_method or test_other' 将匹配并执行所有名称中包含test_method 或包含test_other 的单元测试-k 'not test_method' 将运行名称中不包含test_method 的单元测试-k 'not test_method and not test_other' 将运行名称中既不包含test_method 也不包含test_other 的单元测试. |
-m MARKEXPR | 只运行带有给定marker的单元测试。 | -m 'mark1 and not mark2' . 只运行带有mark1 且不带有mark2 的单元测试 |
-x, --exitfirst | 当出现第一个FAILED的单元测试时直接结束测试。 | |
–pdb | 在出现第一个FAILED的地方或收到KeyboardInterrupt进入python的交互式调试模式 | |
–trace | 在测试最开始处直接进入pdb模式 | |
-s | 捕获标准输入,即显示程序中print()等一类标准输出的内容 | |
–cache-clear | 运行测试前清空测试缓存内容 | |
-q | 精简模式运行测试 | |
-v | 可视化模式运行测试 | |
-vv | 详细模式运行测试 |
创建pytest的配置文件pytest.ini
, pytest运行时会检查是否存在pytest.ini
, 若存在该配置文件则会以配置文件中规定的配置运行测试。以下示例为每次以指定选项运行测试:
[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True
addopts = -vv -s -k ""
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True
用于将测试输出信息中的unicode编码转义为其显式形式,即unicode转中文
[pytest]
filterwarnings =
ignore:.*U.*mode is deprecated:DeprecationWarning
addopts = -v -s --disable-warnings
**filterwarnings = ignore:.*U.*mode is deprecated:DeprecationWarning**用于隐藏
python2.x中由于使用了即将废弃的方法等原因输出的depreciated warning信息。
conftest.py用于存在多个测试文件共用的代码,比如测试数据,测试用的fixture等等,可以视为package的__init__.py
conftest.py的共享程度由该文件所在路径决定。若在整个代码库的根目录下,则根目录下的所有单元测试都可以共享其内容。若放在单个维度的测试目录下,则仅为该目录下的测试文件所共享。