我去,requests使用pytest进行单元测试尽然怎么简单?_pytest request(2)

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

Coverage XML written to file coverage.xml


结果显示requests项目总体覆盖率61%,每个模块的覆盖率也清晰可见。



> 
> 单元测试覆盖率使用代码行数进行判断,`Stmts`显示模块的有效行数,`Miss`显示未执行到的行。如果生成html的报告,还可以定位到具体未覆盖到的行;pycharm的coverage也有类似功能。
> 
> 
> 


tests下的文件及测试类如下表:




| 文件 | 描述 |
| --- | --- |
| compat | python2和python3兼容 |
| conftest | pytest配置 |
| test\_help,test\_packages,test\_hooks,test\_structures | 简单测试类 |
| utils.py | 工具函数 |
| test\_utils | 测试工具函数 |
| test\_requests | 测试requests |
| testserver\server | 模拟服务 |
| test\_testserver | 模拟服务测试 |
| test\_lowlevel | 使用模拟服务测试模拟网络测试 |


### 二、简单工具类如何测试


#### test\_help 实现分析


先从最简单的test\_help上手,测试类和被测试对象命名是对应的。先看看被测试的模块`help.py`。这个模块主要是2个函数 *info* 和 *\_implementation*:



import idna

def _implementation():

def info():

system_ssl = ssl.OPENSSL_VERSION_NUMBER
system_ssl_info = {
‘version’: ‘%x’ % system_ssl if system_ssl is not None else ‘’
}
idna_info = {
‘version’: getattr(idna, ‘version’, ‘’),
}

return {
‘platform’: platform_info,
‘implementation’: implementation_info,
‘system_ssl’: system_ssl_info,
‘using_pyopenssl’: pyopenssl is not None,
‘pyOpenSSL’: pyopenssl_info,
‘urllib3’: urllib3_info,
‘chardet’: chardet_info,
‘cryptography’: cryptography_info,
‘idna’: idna_info,
‘requests’: {
‘version’: requests_version,
},
}


info提供系统环境的信息,\_implementation是其内部实现,以下划线\*\_\*开头。再看测试类test\_help:



from requests.help import info

def test_system_ssl():
“”“Verify we’re actually setting system_ssl when it should be available.”“”
assert info()[‘system_ssl’][‘version’] != ‘’

class VersionedPackage(object):
def init(self, version):
self.version = version

def test_idna_without_version_attribute(mocker):
“”“Older versions of IDNA don’t provide a version attribute, verify
that if we have such a package, we don’t blow up.
“””
mocker.patch(‘requests.help.idna’, new=None)
assert info()[‘idna’] == {‘version’: ‘’}

def test_idna_with_version_attribute(mocker):
“”“Verify we’re actually setting idna version when it should be available.”“”
mocker.patch(‘requests.help.idna’, new=VersionedPackage(‘2.6’))
assert info()[‘idna’] == {‘version’: ‘2.6’}


首先从头部的导入信息可以看到,仅仅对info函数进行测试,这个容易理解。info测试通过,自然覆盖到\_implementation这个内部函数。这里可以得到单元测试的第1个技巧:


1. 仅对public的接口进行测试


`test_idna_without_version_attribute`和`test_idna_with_version_attribute`均有一个mocker参数,这是pytest-mock提供的功能,会自动注入一个mock实现。使用这个mock对idna模块进行模拟



模拟空实现

mocker.patch(‘requests.help.idna’, new=None)

模拟版本2.6

mocker.patch(‘requests.help.idna’, new=VersionedPackage(‘2.6’))


可能大家会比较奇怪,这里patch模拟的是 `requests.help.idna` , 而我们在help中导入的是 `inda` 模块。这是因为在`requests.packages`中对inda进行了模块名重定向:



for package in (‘urllib3’, ‘idna’, ‘chardet’):
locals()[package] = import(package)
# This traversal is apparently necessary such that the identities are
# preserved (requests.packages.urllib3.* is urllib3.*)
for mod in list(sys.modules):
if mod == package or mod.startswith(package + ‘.’):
sys.modules[‘requests.packages.’ + mod] = sys.modules[mod]


使用mocker后,idna的\_\_version\_\_信息就可以进行控制,这样info中的idna结果也就可以预期。那么可以得到第2个技巧:


2.使用mock辅助单元测试


#### test\_hooks 实现分析


我们继续查看hooks如何进行测试:



from requests import hooks

def hook(value):
return value[1:]

@pytest.mark.parametrize(
‘hooks_list, result’, (
(hook, ‘ata’),
([hook, lambda x: None, hook], ‘ta’),
)
)
def test_hooks(hooks_list, result):
assert hooks.dispatch_hook(‘response’, {‘response’: hooks_list}, ‘Data’) == result

def test_default_hooks():
assert hooks.default_hooks() == {‘response’: []}


hooks模块的2个接口default\_hooks和dispatch\_hook都进行了测试。其中default\_hooks是纯函数,无参数有返回值,这种函数最容易测试,仅仅检查返回值是否符合预期即可。dispatch\_hook会复杂一些,还涉及对回调函数(hook函数)的调用:



def dispatch_hook(key, hooks, hook_data, **kwargs):
“”“Dispatches a hook dictionary on a given piece of data.”“”
hooks = hooks or {}
hooks = hooks.get(key)
if hooks:
# 判断钩子函数
if hasattr(hooks, ‘call’):
hooks = [hooks]
for hook in hooks:
_hook_data = hook(hook_data, **kwargs)
if _hook_data is not None:
hook_data = _hook_data
return hook_data


pytest.mark.parametrize提供了2组参数进行测试。第一组参数hook和ata很简单,hook是一个函数,会对参数裁剪,去掉首位,ata是期望的返回值。test\_hooks的response的参数是Data,所以结果应该是ata。第二组参数中的第一个参数会复杂一些,变成了一个数组,首位还是hook函数,中间使用一个匿名函数,匿名函数没有返回值,这样覆盖到 `if _hook_data is not None:` 的旁路分支。执行过程如下:


        hook函数裁剪*Data*首位,剩余*ata*


        匿名函数不对结果修改,剩余*ata*


        hook函数继续裁剪*ata*首位,剩余*ta*


经过测试可以发现dispatch\_hook的设计十分巧妙,使用pipeline模式,将所有的钩子串起来,这是和事件机制不一样的地方。细心的话,我们可以发现 `if hooks:` 并未进行旁路测试,这个不够严谨,有违我们的第3个技巧:


3.测试尽可能覆盖目标函数的所有分支


#### test\_structures 实现分析


LookupDict的测试用例如下:



class TestLookupDict:

@pytest.fixture(autouse=True)
def setup(self):
    """LookupDict instance with "bad_gateway" attribute."""
    self.lookup_dict = LookupDict('test')
    self.lookup_dict.bad_gateway = 502

def test_repr(self):
    assert repr(self.lookup_dict) == "<lookup 'test'>"

get_item_parameters = pytest.mark.parametrize(
    'key, value', (
        ('bad_gateway', 502),
        ('not_a_key', None)
    )
)

@get_item_parameters
def test_getitem(self, key, value):
    assert self.lookup_dict[key] == value

@get_item_parameters
def test_get(self, key, value):
    assert self.lookup_dict.get(key) == value

可以发现使用setup方法配合@pytest.fixture,给所有测试用例初始化了一个lookup\_dict对象;同时pytest.mark.parametrize可以在不同的测试用例之间复用的,我们可以得到第4个技巧:


4.使用pytest.fixture复用被测试对象,使用pytest.mark.parametriz复用测试参数


通过TestLookupDict的test\_getitem和test\_get可以更直观的了解LookupDict的get和\_\_getitem\_\_方法的作用:



class LookupDict(dict):

def getitem(self, key):
# We allow fall-through here, so values default to None
return self.dict.get(key, None)

def get(self, key, default=None):
    return self.__dict__.get(key, default)

        get自定义字典,使其可以使用 *get* 方法获取值


        \_\_getitem\_\_自定义字典,使其可以使用 *[]* 符合获取值


CaseInsensitiveDict的测试用例在test\_structures和test\_requests中都有测试,前者主要是基础测试,后者偏向业务使用层面,我们可以看到这两种差异:



class TestCaseInsensitiveDict:
# 类测试
def test_repr(self):
assert repr(self.case_insensitive_dict) == “{‘Accept’: ‘application/json’}”

def test_copy(self):
    copy = self.case_insensitive_dict.copy()
    assert copy is not self.case_insensitive_dict
    assert copy == self.case_insensitive_dict

class TestCaseInsensitiveDict:
# 使用方法测试
def test_delitem(self):
cid = CaseInsensitiveDict()
cid[‘Spam’] = ‘someval’
del cid[‘sPam’]
assert ‘spam’ not in cid
assert len(cid) == 0

def test_contains(self):
    cid = CaseInsensitiveDict()
    cid['Spam'] = 'someval'
    assert 'Spam' in cid
    assert 'spam' in cid
    assert 'SPAM' in cid
    assert 'sPam' in cid
    assert 'notspam' not in cid

借鉴上面的测试方法,不难得出第5个技巧:


5.可以从不同的层面对同一个对象进行单元测试



> 
> 后面的test\_lowlevel和test\_requests也应用了这种技巧
> 
> 
> 


#### utils.py


utils中构建了一个可以写入env的生成器(由yield关键字提供),可以当上下文装饰器使用:



import contextlib
import os

@contextlib.contextmanager
def override_environ(**kwargs):
save_env = dict(os.environ)
for key, value in kwargs.items():
if value is None:
del os.environ[key]
else:
os.environ[key] = value
try:
yield
finally:
os.environ.clear()
os.environ.update(save_env)


下面是使用方法示例:



test_requests.py

kwargs = {
var: proxy
}

模拟控制proxy环境变量

with override_environ(**kwargs):
proxies = session.rebuild_proxies(prep, {})

def rebuild_proxies(self, prepared_request, proxies):
bypass_proxy = should_bypass_proxies(url, no_proxy=no_proxy)

def should_bypass_proxies(url, no_proxy):

get_proxy = lambda k: os.environ.get(k) or os.environ.get(k.upper())


6.涉及环境变量的地方,可以使用上下文装饰器进行模拟多种环境变量


#### utils测试用例


utils的测试用例较多,我们选择部分进行分析。先看to\_key\_val\_list函数:



对象转列表

def to_key_val_list(value):
if value is None:
return None

if isinstance(value, (str, bytes, bool, int)):
    raise ValueError('cannot encode objects that are not 2-tuples')

if isinstance(value, Mapping):
    value = value.items()

return list(value)

对应的测试用例TestToKeyValList:



class TestToKeyValList:

@pytest.mark.parametrize(
    'value, expected', (
        ([('key', 'val')], [('key', 'val')]),
        ((('key', 'val'), ), [('key', 'val')]),
        ({'key': 'val'}, [('key', 'val')]),
        (None, None)
    ))
def test_valid(self, value, expected):
    assert to_key_val_list(value) == expected

def test_invalid(self):
    with pytest.raises(ValueError):
        to_key_val_list('string')

重点是test\_invalid中使用pytest.raise对异常的处理:


7.使用pytest.raises对异常进行捕获处理


TestSuperLen介绍了几种进行IO模拟测试的方法:



class TestSuperLen:

@pytest.mark.parametrize(
    'stream, value', (
        (StringIO.StringIO, 'Test'),
        (BytesIO, b'Test'),
        pytest.param(cStringIO, 'Test',
                     marks=pytest.mark.skipif('cStringIO is None')),
    ))
def test_io_streams(self, stream, value):
    """Ensures that we properly deal with different kinds of IO streams."""
    assert super_len(stream()) == 0
    assert super_len(stream(value)) == 4

def test_super_len_correctly_calculates_len_of_partially_read_file(self):
    """Ensure that we handle partially consumed file like objects."""
    s = StringIO.StringIO()
    s.write('foobarbogus')
    assert super_len(s) == 0

@pytest.mark.parametrize(
    'mode, warnings_num', (
        ('r', 1),
        ('rb', 0),
    ))
def test_file(self, tmpdir, mode, warnings_num, recwarn):
    file_obj = tmpdir.join('test.txt')
    file_obj.write('Test')
    with file_obj.open(mode) as fd:
        assert super_len(fd) == 4
    assert len(recwarn) == warnings_num

def test_super_len_with_tell(self):
    foo = StringIO.StringIO('12345')
    assert super_len(foo) == 5
    foo.read(2)
    assert super_len(foo) == 3

def test_super_len_with_fileno(self):
    with open(__file__, 'rb') as f:
        length = super_len(f)
        file_data = f.read()
    assert length == len(file_data)

使用StringIO来模拟IO操作,可以配置各种IO的测试。当然也可以使用BytesIO/cStringIO, 不过单元测试用例一般不关注性能,StringIO简单够用。


pytest提供tmpdir的fixture,可以进行文件读写操作测试


可以使用\_\_file\_\_来进行文件的只读测试,\_\_file\_\_表示当前文件,不会产生副作用。


8.使用IO模拟配合进行单元测试


### 三、request-api如何测试


requests的测试需要httpbin和pytest-httpbin,前者会启动一个本地服务,后者会安装一个pytest插件,测试用例中可以得到httpbin的fixture,用来操作这个服务的URL。




| 类 | 功能 |
| --- | --- |
| TestRequests | requests业务测试 |
| TestCaseInsensitiveDict | 大小写不敏感的字典测试 |
| TestMorselToCookieExpires | cookie过期测试 |
| TestMorselToCookieMaxAge | cookie大小 |
| TestTimeout | 响应超时的测试 |
| TestPreparingURLs | URL预处理 |
| ... | 一些零碎的测试用例 |


坦率的讲:这个测试用例内容庞大,达到2500行。看起来是针对各种业务的零散case,我并没有完全理顺其组织逻辑。我选择一些感兴趣的业务进行介绍, 先看TimeOut的测试:



![img](https://img-blog.csdnimg.cn/img_convert/c7299cd62cd607779fabafd0f18418eb.png)
![img](https://img-blog.csdnimg.cn/img_convert/42976b8460bdd18eb222cfeba69adb3d.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

estTimeout | 响应超时的测试 |
| TestPreparingURLs | URL预处理 |
| ... | 一些零碎的测试用例 |


坦率的讲:这个测试用例内容庞大,达到2500行。看起来是针对各种业务的零散case,我并没有完全理顺其组织逻辑。我选择一些感兴趣的业务进行介绍, 先看TimeOut的测试:



[外链图片转存中...(img-RFEdlvRv-1715372992685)]
[外链图片转存中...(img-eLYXkW5H-1715372992685)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值