在开发与外部 api 交互的 python 应用时,进行单元测试和集成测试至关重要。然而,直接调用外部 api 不仅会增加测试时间、消耗网络资源,还可能受到 api 速率限制或不可预测的网络波动影响,导致测试结果不稳定。为了解决这些问题,模拟(mocking)外部 http 请求成为一种标准实践。
本教程将聚焦于如何使用 requests-mock 库来模拟 requests 库的 HTTP 请求。我们将特别关注处理动态 URL 的场景,例如分页 API 调用,并演示如何精确控制模拟响应的序列,以确保测试逻辑的完整性。
requests-mock 是一个功能强大且易于使用的 Python 库,专门用于模拟 requests 库发出的 HTTP 请求。它通过在 requests 内部注册适配器来实现请求的拦截和响应的模拟,而无需修改任何应用代码。
首先,确保你已经安装了 requests-mock:
pip install requests-mock
在许多 API 调用场景中,URL 并非固定不变,而是包含动态参数,例如分页 API 的页码 (?page=1, ?page=2)。requests-mock 允许使用正则表达式来匹配这类动态 URL,从而用一个规则覆盖多个可能的请求。
考虑以下用于从 SWAPI(星球大战 API)获取人物列表的函数:
import requests from http import HTTPStatus def consuming_api_swapi_index_page(initial_page: int = 1): """Swapi index page.""" check = HTTPStatus.OK results = [] current_page = initial_page while check == HTTPStatus.OK: response = requests.get( f'https://swapi.dev/api/people/?page={current_page}' ) # 这里的 response.url 是关键,它会是实际请求的 URL results.append(url := response.url) print(url) check = response.status_code current_page += 1 return results
这个函数会循环调用 API,直到收到非 200 的状态码。在测试中,我们需要模拟一系列请求,每个请求的 URL 中的 page 参数都会递增。
我们可以使用 re.compile 来创建一个正则表达式模式,以匹配所有页码的 URL:
import re import requests_mock # 定义匹配所有页码的正则表达式 matcher = re.compile(r"https://swapi.dev/api/people/\?page=\d+") # 在测试中,requests-mock 会拦截所有匹配此模式的 GET 请求 # m是一个requests_mock.Mocker对象,通常通过pytest的fixture提供 # m.register_uri('GET', matcher, text='Mocked Response')
关键点: 当 requests-mock 拦截到一个匹配 matcher 的请求时,它会使用你提供的模拟响应。更重要的是,response.url 属性会自动设置为实际发起请求的 URL,而不是匹配模式本身。这意味着如果请求的是 https://swapi.dev/api/people/?page=1,那么 response.url 就会是 https://swapi.dev/api/people/?page=1,这正是我们函数逻辑所需要的。
对于像 consuming_api_swapi_index_page 这样依赖于连续请求状态(如循环终止条件)的函数,仅仅提供一个静态响应是不够的。我们需要模拟一系列不同的响应,例如前几页返回成功状态,最后一页返回非成功状态以终止循环。
requests-mock 提供了 side_effect 参数,它允许你提供一个响应字典列表。每次匹配到请求时,requests-mock 会按顺序从列表中取出一个响应来返回。当列表耗尽后,后续的请求将不再被模拟(除非有其他规则匹配)。
为了让上述 consuming_api_swapi_index_page 函数在测试中正确执行并终止,我们需要模拟 10 个成功的请求(对应页码 1 到 10),然后模拟一个非 200 的请求来结束循环。
以下是一个完整的测试案例:
import requests import requests_mock import re from http import HTTPStatus # 假设 consuming_api_swapi_index_page 函数已定义如上 def test_consuming_api_swapi_index_page(requests_mock) -> None: """Test it.""" # 期望的 URL 列表 expected_urls = [ 'https://swapi.dev/api/people/?page=1', 'https://swapi.dev/api/people/?page=2', 'https://swapi.dev/api/people/?page=3', 'https://swapi.dev/api/people/?page=4', 'https://swapi.dev/api/people/?page=5', 'https://swapi.dev/api/people/?page=6', 'https://swapi.dev/api/people/?page=7', 'https://swapi.dev/api/people/?page=8', 'https://swapi.dev/api/people/?page=9', 'https://swapi.dev/api/people/?page=10', ] # 定义匹配所有页码的正则表达式 matcher = re.compile(r"https://swapi.dev/api/people/\?page=\d+") # 准备 side_effect 响应列表 # 前10个请求返回 HTTPStatus.OK (200) # 第11个请求返回 HTTPStatus.NOT_FOUND (404) 以终止循环 mock_responses = [] for _ in range(10): mock_responses.append({'status_code': HTTPStatus.OK, 'json': {'results': []}}) # 模拟一个空results,如果函数需要解析json mock_responses.append({'status_code': HTTPStatus.NOT_FOUND}) # 终止循环的响应 # 注册 URI,使用 side_effect 提供响应序列 requests_mock.get(matcher, side_effect=mock_responses) # 调用被测试函数 actual_urls = consuming_api_swapi_index_page() # 断言结果是否符合预期 assert actual_urls == expected_urls # 注意:如果使用 pytest,requests_mock 会作为 fixture 自动提供。 # 如果不使用 pytest,你需要手动创建 Mocker 实例并使用 with 语句: # import requests_mock # with requests_mock.Mocker() as m: # m.get(matcher, side_effect=mock_responses) # actual_urls = consuming_api_swapi_index_page() # assert actual_urls == expected_urls
在这个例子中:
requests-mock 是 Python 中进行 HTTP 请求模拟的强大工具。通过结合正则表达式进行灵活的 URL 匹配,并利用 side_effect 参数来模拟序列化的响应,我们可以精确地控制外部 API 调用的行为,从而编写出隔离性好、执行快速且结果可靠的测试。掌握这些技巧将显著提升你的测试效率和代码质量。
以上就是掌握 requests-mock:高效模拟动态 URL 请求与响应序列的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 //m.sbmmt.com/ All Rights Reserved | php.cn | 湘ICP备2023035733号