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

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新软件测试全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上软件测试知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注软件测试)
img

正文

test_idna_without_version_attributetest_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。

功能
TestRequestsrequests业务测试
TestCaseInsensitiveDict大小写不敏感的字典测试
TestMorselToCookieExpirescookie过期测试
TestMorselToCookieMaxAgecookie大小
TestTimeout响应超时的测试
TestPreparingURLsURL预处理
一些零碎的测试用例

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

TARPIT = ‘http://10.255.255.1’

class TestTimeout:

def test_stream_timeout(self, httpbin):
try:
requests.get(httpbin(‘delay/10’), timeout=2.0)
except requests.exceptions.Timeout as e:
assert ‘Read timed out’ in e.args[0].args[0]

@pytest.mark.parametrize(
‘timeout’, (
(0.1, None),
Urllib3Timeout(connect=0.1, read=None)
))
def test_connect_timeout(self, timeout):
try:
requests.get(TARPIT, timeout=timeout)
pytest.fail(‘The connect() request should time out.’)
except ConnectTimeout as e:
assert isinstance(e, ConnectionError)
assert isinstance(e, Timeout)

test_stream_timeout利用httpbin创建了一个延迟10s响应的接口,然后请求本身设置成2s,这样可以收到一个本地timeout的错误。test_connect_timeout则是访问一个不存在的服务,捕获连接超时的错误。

TestRequests都是对requests的业务进程测试,可以看到至少是2种:

class TestRequests:

def test_basic_building(self):
req = requests.Request()
req.url = ‘http://kennethreitz.org/’
req.data = {‘life’: ‘42’}

pr = req.prepare()
assert pr.url == req.url
assert pr.body == ‘life=42’

def test_path_is_not_double_encoded(self):
request = requests.Request(‘GET’, “http://0.0.0.0/get/test case”).prepare()

assert request.path_url == '/get/test%20case

def test_HTTP_200_OK_GET_ALTERNATIVE(self, httpbin):
r = requests.Request(‘GET’, httpbin(‘get’))
s = requests.Session()
s.proxies = getproxies()

r = s.send(r.prepare())

assert r.status_code == 200

ef test_set_cookie_on_301(self, httpbin):
s = requests.session()
url = httpbin(‘cookies/set?foo=bar’)
s.get(url)
assert s.cookies[‘foo’] == ‘bar’

对url进行校验,只需要对request进行prepare,这种情况下,请求并未发送,少了网络传输,测试用例会更迅速

需要响应数据的情况,需要使用httbin构建真实的请求-响应数据

四、底层API测试

testserver构建一个简单的基于线程的tcp服务,这个tcp服务具有__enter____exit__方法,还可以当一个上下文环境使用。

class TestTestServer:

def test_basic(self):
“”“messages are sent and received properly”“”
question = b"success?"
answer = b"yeah, success"

def handler(sock):
text = sock.recv(1000)
assert text == question
sock.sendall(answer)

with Server(handler) as (host, port):
sock = socket.socket()
sock.connect((host, port))
sock.sendall(question)
text = sock.recv(1000)
assert text == answer
sock.close()

def test_text_response(self):
“”“the text_response_server sends the given text”“”
server = Server.text_response_server(
“HTTP/1.1 200 OK\r\n” +
“Content-Length: 6\r\n” +
“\r\nroflol”

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

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注软件测试)
img

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

def test_text_response(self):
“”“the text_response_server sends the given text”“”
server = Server.text_response_server(
“HTTP/1.1 200 OK\r\n” +
“Content-Length: 6\r\n” +
“\r\nroflol”

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

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注软件测试)
[外链图片转存中…(img-YuSgMN7n-1713552250651)]

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

  • 15
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值