跳转到主要内容

一个用于模拟`requests` Python库的实用库。

项目描述

https://img.shields.io/pypi/v/responses.svg https://img.shields.io/pypi/pyversions/responses.svg https://img.shields.io/pypi/dm/responses https://codecov.io/gh/getsentry/responses/branch/master/graph/badge.svg

一个用于模拟requests Python库的实用库。

目录

安装

pip install responses

弃用和迁移路径

在这里您将找到已弃用功能列表以及每个功能的迁移路径。请确保根据指南更新您的代码。

弃用和迁移

弃用功能

弃用版本

迁移路径

responses.json_params_matcher

0.14.0

responses.matchers.json_params_matcher

responses.urlencoded_params_matcher

0.14.0

responses.matchers.urlencoded_params_matcher

stream参数在ResponseCallbackResponse

0.15.0

直接在请求中使用stream参数。

match_querystring参数在ResponseCallbackResponse中。

0.17.0

使用responses.matchers.query_param_matcherresponses.matchers.query_string_matcher

responses.assert_all_requests_are_firedresponses.passthru_prefixesresponses.target

0.20.0

使用responses.mock.assert_all_requests_are_firedresponses.mock.passthru_prefixesresponses.mock.target代替。

BETA功能

以下您可以找到BETA功能列表。尽管我们将努力保持API与发布版本的向后兼容性,但我们保留在它们被视为稳定之前更改这些API的权利。请通过GitHub Issues分享您的反馈。

将响应记录到文件中

您可以对服务器执行真实请求,并且responses将自动将输出记录到文件中。记录的数据以YAML格式存储。

@responses._recorder.record(file_path="out.yaml")装饰器应用于执行请求以记录响应到out.yaml文件的任何函数。

以下代码

import requests
from responses import _recorder


def another():
    rsp = requests.get("https://httpstat.us/500")
    rsp = requests.get("https://httpstat.us/202")


@_recorder.record(file_path="out.yaml")
def test_recorder():
    rsp = requests.get("https://httpstat.us/404")
    rsp = requests.get("https://httpbin.org/status/wrong")
    another()

将产生以下输出

responses:
- response:
    auto_calculate_content_length: false
    body: 404 Not Found
    content_type: text/plain
    method: GET
    status: 404
    url: https://httpstat.us/404
- response:
    auto_calculate_content_length: false
    body: Invalid status code
    content_type: text/plain
    method: GET
    status: 400
    url: https://httpbin.org/status/wrong
- response:
    auto_calculate_content_length: false
    body: 500 Internal Server Error
    content_type: text/plain
    method: GET
    status: 500
    url: https://httpstat.us/500
- response:
    auto_calculate_content_length: false
    body: 202 Accepted
    content_type: text/plain
    method: GET
    status: 202
    url: https://httpstat.us/202

从文件中重新播放响应(填充注册表)

您可以从包含记录响应的yaml文件中填充您的活动注册表。(有关如何获取文件,请参阅将响应记录到文件)。为此,您需要在激活的装饰器或上下文管理器中执行responses._add_from_file(file_path="out.yaml")

以下代码示例注册了一个patch响应,然后是out.yaml文件中存在的所有响应以及最后的post响应。

import responses


@responses.activate
def run():
    responses.patch("http://httpbin.org")
    responses._add_from_file(file_path="out.yaml")
    responses.post("http://httpbin.org/form")


run()

基础

responses的核心是通过注册模拟响应和使用responses.activate装饰器覆盖测试函数。 responses提供与requests类似的接口。

主要界面

  • responses.add(ResponseResponse args) - 允许注册Response对象或直接提供Response对象的参数。请参阅响应参数

import responses
import requests


@responses.activate
def test_simple():
    # Register via 'Response' object
    rsp1 = responses.Response(
        method="PUT",
        url="http://example.com",
    )
    responses.add(rsp1)
    # register via direct arguments
    responses.add(
        responses.GET,
        "http://twitter.com/api/1/foobar",
        json={"error": "not found"},
        status=404,
    )

    resp = requests.get("http://twitter.com/api/1/foobar")
    resp2 = requests.put("http://example.com")

    assert resp.json() == {"error": "not found"}
    assert resp.status_code == 404

    assert resp2.status_code == 200
    assert resp2.request.method == "PUT"

如果您尝试获取一个没有匹配的URL,responses 将抛出 ConnectionError

import responses
import requests

from requests.exceptions import ConnectionError


@responses.activate
def test_simple():
    with pytest.raises(ConnectionError):
        requests.get("http://twitter.com/api/1/foobar")

快捷方式

快捷方式提供了 responses.add() 的简短版本,其中方法参数已预先填充。

  • responses.delete(Response args) - 注册 DELETE 响应

  • responses.get(Response args) - 注册 GET 响应

  • responses.head(Response args) - 注册 HEAD 响应

  • responses.options(Response args) - 注册 OPTIONS 响应

  • responses.patch(Response args) - 注册 PATCH 响应

  • responses.post(Response args) - 注册 POST 响应

  • responses.put(Response args) - 注册 PUT 响应

import responses
import requests


@responses.activate
def test_simple():
    responses.get(
        "http://twitter.com/api/1/foobar",
        json={"type": "get"},
    )

    responses.post(
        "http://twitter.com/api/1/foobar",
        json={"type": "post"},
    )

    responses.patch(
        "http://twitter.com/api/1/foobar",
        json={"type": "patch"},
    )

    resp_get = requests.get("http://twitter.com/api/1/foobar")
    resp_post = requests.post("http://twitter.com/api/1/foobar")
    resp_patch = requests.patch("http://twitter.com/api/1/foobar")

    assert resp_get.json() == {"type": "get"}
    assert resp_post.json() == {"type": "post"}
    assert resp_patch.json() == {"type": "patch"}

响应作为上下文管理器

您不必用装饰器包裹整个函数,可以使用上下文管理器。

import responses
import requests


def test_my_api():
    with responses.RequestsMock() as rsps:
        rsps.add(
            responses.GET,
            "http://twitter.com/api/1/foobar",
            body="{}",
            status=200,
            content_type="application/json",
        )
        resp = requests.get("http://twitter.com/api/1/foobar")

        assert resp.status_code == 200

    # outside the context manager requests will hit the remote server
    resp = requests.get("http://twitter.com/api/1/foobar")
    resp.status_code == 404

响应参数

以下属性可以传递给响应模拟

method (str)

HTTP 方法(GET、POST等)。

url (strcompiled regular expression)

完整的资源URL。

match_querystring (bool)

已弃用:使用 responses.matchers.query_param_matcherresponses.matchers.query_string_matcher

在匹配请求时包含查询字符串。如果响应URL包含查询字符串,则默认启用;如果不包含或URL是正则表达式,则禁用。

body (strBufferedReaderException)

响应体。更多信息请参阅 异常作为响应体

json

表示JSON响应体的Python对象。自动配置适当的Content-Type。

status (int)

HTTP状态码。

content_type (content_type)

默认为 text/plain

headers (dict)

响应头。

stream (bool)

已弃用:请在请求中直接使用 stream 参数

auto_calculate_content_length (bool)

默认禁用。自动计算提供的字符串或JSON体的长度。

match (tuple)

一个可迭代的回调(推荐使用元组)列表,用于根据请求属性匹配请求。当前模块提供多个匹配器,您可以使用它们来匹配

  • JSON格式的请求体内容

  • URL编码数据的请求体内容

  • 请求查询参数

  • 请求查询字符串(与查询参数类似,但输入为字符串)

  • 传递给请求的kwargs,例如 streamverify

  • 请求中的“multipart/form-data”内容和头信息

  • 请求头

  • 请求片段标识符

或者用户可以创建自定义匹配器。更多信息请参阅 匹配请求

异常作为响应体

您可以将一个 Exception 作为主体传递,以在请求上触发错误

import responses
import requests


@responses.activate
def test_simple():
    responses.get("http://twitter.com/api/1/foobar", body=Exception("..."))
    with pytest.raises(Exception):
        requests.get("http://twitter.com/api/1/foobar")

匹配请求

匹配请求体内容

对于发送请求数据的端点,添加响应时,您可以添加匹配器以确保您的代码正在发送正确的参数,并根据请求体内容提供不同的响应。responses 提供了用于JSON和URL编码请求体的匹配器。

URL编码数据

import responses
import requests
from responses import matchers


@responses.activate
def test_calc_api():
    responses.post(
        url="http://calc.com/sum",
        body="4",
        match=[matchers.urlencoded_params_matcher({"left": "1", "right": "3"})],
    )
    requests.post("http://calc.com/sum", data={"left": 1, "right": 3})

JSON编码数据

可以使用 matchers.json_params_matcher() 来匹配JSON编码数据。

import responses
import requests
from responses import matchers


@responses.activate
def test_calc_api():
    responses.post(
        url="http://example.com/",
        body="one",
        match=[
            matchers.json_params_matcher({"page": {"name": "first", "type": "json"}})
        ],
    )
    resp = requests.request(
        "POST",
        "http://example.com/",
        headers={"Content-Type": "application/json"},
        json={"page": {"name": "first", "type": "json"}},
    )

查询参数匹配器

查询参数作为字典

您可以使用 matchers.query_param_matcher 函数来匹配 params 请求参数。只需使用与您在 requestparams 参数中使用的相同的字典即可。

请注意,不要将查询参数作为URL的一部分。避免使用已弃用的match_querystring参数。

import responses
import requests
from responses import matchers


@responses.activate
def test_calc_api():
    url = "http://example.com/test"
    params = {"hello": "world", "I am": "a big test"}
    responses.get(
        url=url,
        body="test",
        match=[matchers.query_param_matcher(params)],
    )

    resp = requests.get(url, params=params)

    constructed_url = r"http://example.com/test?I+am=a+big+test&hello=world"
    assert resp.url == constructed_url
    assert resp.request.url == constructed_url
    assert resp.request.params == params

默认情况下,匹配器将严格验证所有参数。要验证仅指定的参数出现在原始请求中,请使用strict_match=False

查询参数作为字符串

作为替代方案,您可以使用matchers.query_string_matcher中的查询字符串值来匹配请求中的查询参数

import requests
import responses
from responses import matchers


@responses.activate
def my_func():
    responses.get(
        "https://httpbin.org/get",
        match=[matchers.query_string_matcher("didi=pro&test=1")],
    )
    resp = requests.get("https://httpbin.org/get", params={"test": 1, "didi": "pro"})


my_func()

请求关键字参数匹配器

要验证请求参数,请使用matchers.request_kwargs_matcher函数与请求kwargs进行匹配。

仅支持以下参数:timeoutverifyproxiesstreamcert

请注意,只有提供给matchers.request_kwargs_matcher的参数将进行验证。

import responses
import requests
from responses import matchers

with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
    req_kwargs = {
        "stream": True,
        "verify": False,
    }
    rsps.add(
        "GET",
        "http://111.com",
        match=[matchers.request_kwargs_matcher(req_kwargs)],
    )

    requests.get("http://111.com", stream=True)

    # >>>  Arguments don't match: {stream: True, verify: True} doesn't match {stream: True, verify: False}

请求multipart/form-data数据验证

要验证multipart/form-data数据请求正文和头部,您可以使用matchers.multipart_matcher。提供的datafiles参数将与请求进行比较

import requests
import responses
from responses.matchers import multipart_matcher


@responses.activate
def my_func():
    req_data = {"some": "other", "data": "fields"}
    req_files = {"file_name": b"Old World!"}
    responses.post(
        url="http://httpbin.org/post",
        match=[multipart_matcher(req_files, data=req_data)],
    )
    resp = requests.post("http://httpbin.org/post", files={"file_name": b"New World!"})


my_func()
# >>> raises ConnectionError: multipart/form-data doesn't match. Request body differs.

请求片段标识符验证

要验证请求URL片段标识符,您可以使用matchers.fragment_identifier_matcher。匹配器将片段字符串(#符号之后的所有内容)作为比较的输入

import requests
import responses
from responses.matchers import fragment_identifier_matcher


@responses.activate
def run():
    url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar"
    responses.get(
        url,
        match=[fragment_identifier_matcher("test=1&foo=bar")],
        body=b"test",
    )

    # two requests to check reversed order of fragment identifier
    resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar")
    resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1")


run()

请求头验证

在添加响应时,您可以指定匹配器以确保您的代码发送正确的头部,并根据请求头部提供不同的响应。

import responses
import requests
from responses import matchers


@responses.activate
def test_content_type():
    responses.get(
        url="http://example.com/",
        body="hello world",
        match=[matchers.header_matcher({"Accept": "text/plain"})],
    )

    responses.get(
        url="http://example.com/",
        json={"content": "hello world"},
        match=[matchers.header_matcher({"Accept": "application/json"})],
    )

    # request in reverse order to how they were added!
    resp = requests.get("http://example.com/", headers={"Accept": "application/json"})
    assert resp.json() == {"content": "hello world"}

    resp = requests.get("http://example.com/", headers={"Accept": "text/plain"})
    assert resp.text == "hello world"

因为requests将发送一些标准头部,而不仅仅是您的代码中指定的,所以默认情况下,匹配器之外的请求头部将被忽略。您可以通过将strict_match=True传递给匹配器来更改此行为,以确保仅发送您期望的头部,而不发送其他头部。请注意,您可能需要在使用requests时使用PreparedRequest来确保不包含任何额外的头部。

import responses
import requests
from responses import matchers


@responses.activate
def test_content_type():
    responses.get(
        url="http://example.com/",
        body="hello world",
        match=[matchers.header_matcher({"Accept": "text/plain"}, strict_match=True)],
    )

    # this will fail because requests adds its own headers
    with pytest.raises(ConnectionError):
        requests.get("http://example.com/", headers={"Accept": "text/plain"})

    # a prepared request where you overwrite the headers before sending will work
    session = requests.Session()
    prepped = session.prepare_request(
        requests.Request(
            method="GET",
            url="http://example.com/",
        )
    )
    prepped.headers = {"Accept": "text/plain"}

    resp = session.send(prepped)
    assert resp.text == "hello world"

创建自定义匹配器

如果您的应用程序需要其他编码或不同的数据验证,您可以构建自己的匹配器,该匹配器返回Tuple[matches: bool, reason: str]。其中布尔值表示请求参数是否匹配,字符串是在匹配失败时提供的原因。您的匹配器可以期望由responses提供的PreparedRequest参数。

请注意,PreparedRequest已自定义,并具有额外的属性paramsreq_kwargs

响应注册表

默认注册表

默认情况下,responses将搜索所有已注册的Response对象并返回一个匹配项。如果只有一个Response被注册,则注册表保持不变。然而,如果在同一请求中找到多个匹配项,则返回第一个匹配项并将其从注册表中删除。

有序注册表

在某些场景中,保留请求和响应的顺序很重要。您可以使用registries.OrderedRegistry来强制所有Response对象依赖于插入顺序和调用索引。在以下示例中,我们添加了多个针对相同URL的Response对象。然而,您可以看到,状态码将取决于调用顺序。

import requests

import responses
from responses.registries import OrderedRegistry


@responses.activate(registry=OrderedRegistry)
def test_invocation_index():
    responses.get(
        "http://twitter.com/api/1/foobar",
        json={"msg": "not found"},
        status=404,
    )
    responses.get(
        "http://twitter.com/api/1/foobar",
        json={"msg": "OK"},
        status=200,
    )
    responses.get(
        "http://twitter.com/api/1/foobar",
        json={"msg": "OK"},
        status=200,
    )
    responses.get(
        "http://twitter.com/api/1/foobar",
        json={"msg": "not found"},
        status=404,
    )

    resp = requests.get("http://twitter.com/api/1/foobar")
    assert resp.status_code == 404
    resp = requests.get("http://twitter.com/api/1/foobar")
    assert resp.status_code == 200
    resp = requests.get("http://twitter.com/api/1/foobar")
    assert resp.status_code == 200
    resp = requests.get("http://twitter.com/api/1/foobar")
    assert resp.status_code == 404

自定义注册表

内置的registries适用于大多数用例,但要处理特殊条件,您可以实现自定义注册表,该注册表必须遵循registries.FirstMatchRegistry接口。重新定义find方法将允许您创建自定义搜索逻辑并返回适当的Response

示例:展示如何设置自定义注册表

import responses
from responses import registries


class CustomRegistry(registries.FirstMatchRegistry):
    pass


print("Before tests:", responses.mock.get_registry())
""" Before tests: <responses.registries.FirstMatchRegistry object> """


# using function decorator
@responses.activate(registry=CustomRegistry)
def run():
    print("Within test:", responses.mock.get_registry())
    """ Within test: <__main__.CustomRegistry object> """


run()

print("After test:", responses.mock.get_registry())
""" After test: <responses.registries.FirstMatchRegistry object> """

# using context manager
with responses.RequestsMock(registry=CustomRegistry) as rsps:
    print("In context manager:", rsps.get_registry())
    """ In context manager: <__main__.CustomRegistry object> """

print("After exit from context manager:", responses.mock.get_registry())
"""
After exit from context manager: <responses.registries.FirstMatchRegistry object>
"""

动态响应

您可以使用回调函数提供动态响应。回调必须返回一个包含(状态头部主体)元组的元组。

import json

import responses
import requests


@responses.activate
def test_calc_api():
    def request_callback(request):
        payload = json.loads(request.body)
        resp_body = {"value": sum(payload["numbers"])}
        headers = {"request-id": "728d329e-0e86-11e4-a748-0c84dc037c13"}
        return (200, headers, json.dumps(resp_body))

    responses.add_callback(
        responses.POST,
        "http://calc.com/sum",
        callback=request_callback,
        content_type="application/json",
    )

    resp = requests.post(
        "http://calc.com/sum",
        json.dumps({"numbers": [1, 2, 3]}),
        headers={"content-type": "application/json"},
    )

    assert resp.json() == {"value": 6}

    assert len(responses.calls) == 1
    assert responses.calls[0].request.url == "http://calc.com/sum"
    assert responses.calls[0].response.text == '{"value": 6}'
    assert (
        responses.calls[0].response.headers["request-id"]
        == "728d329e-0e86-11e4-a748-0c84dc037c13"
    )

您还可以将编译后的正则表达式传递给 add_callback 以匹配多个 URL。

import re, json

from functools import reduce

import responses
import requests

operators = {
    "sum": lambda x, y: x + y,
    "prod": lambda x, y: x * y,
    "pow": lambda x, y: x**y,
}


@responses.activate
def test_regex_url():
    def request_callback(request):
        payload = json.loads(request.body)
        operator_name = request.path_url[1:]

        operator = operators[operator_name]

        resp_body = {"value": reduce(operator, payload["numbers"])}
        headers = {"request-id": "728d329e-0e86-11e4-a748-0c84dc037c13"}
        return (200, headers, json.dumps(resp_body))

    responses.add_callback(
        responses.POST,
        re.compile("http://calc.com/(sum|prod|pow|unsupported)"),
        callback=request_callback,
        content_type="application/json",
    )

    resp = requests.post(
        "http://calc.com/prod",
        json.dumps({"numbers": [2, 3, 4]}),
        headers={"content-type": "application/json"},
    )
    assert resp.json() == {"value": 24}


test_regex_url()

如果您想将额外的关键字参数传递给回调函数,例如在重新使用回调函数以提供略有不同结果时,可以使用 functools.partial

from functools import partial


def request_callback(request, id=None):
    payload = json.loads(request.body)
    resp_body = {"value": sum(payload["numbers"])}
    headers = {"request-id": id}
    return (200, headers, json.dumps(resp_body))


responses.add_callback(
    responses.POST,
    "http://calc.com/sum",
    callback=partial(request_callback, id="728d329e-0e86-11e4-a748-0c84dc037c13"),
    content_type="application/json",
)

与单元测试框架集成

作为pytest fixture的响应

@pytest.fixture
def mocked_responses():
    with responses.RequestsMock() as rsps:
        yield rsps


def test_api(mocked_responses):
    mocked_responses.get(
        "http://twitter.com/api/1/foobar",
        body="{}",
        status=200,
        content_type="application/json",
    )
    resp = requests.get("http://twitter.com/api/1/foobar")
    assert resp.status_code == 200

为每个测试添加默认响应

在运行 unittest 测试时,这可以用来设置一些通用的类级别响应,这些响应可能由每个测试补充。类似的接口也可以应用于 pytest 框架。

class TestMyApi(unittest.TestCase):
    def setUp(self):
        responses.get("https://example.com", body="within setup")
        # here go other self.responses.add(...)

    @responses.activate
    def test_my_func(self):
        responses.get(
            "https://httpbin.org/get",
            match=[matchers.query_param_matcher({"test": "1", "didi": "pro"})],
            body="within test",
        )
        resp = requests.get("https://example.com")
        resp2 = requests.get(
            "https://httpbin.org/get", params={"test": "1", "didi": "pro"}
        )
        print(resp.text)
        # >>> within setup
        print(resp2.text)
        # >>> within test

RequestMock方法:start、stop、reset

responsesstartstopreset 方法,与 unittest.mock.patch 非常相似。这使得在 setup 方法或需要执行多个补丁而无需嵌套装饰器或语句的地方进行请求模拟变得更容易。

class TestUnitTestPatchSetup:
    def setup(self):
        """Creates ``RequestsMock`` instance and starts it."""
        self.r_mock = responses.RequestsMock(assert_all_requests_are_fired=True)
        self.r_mock.start()

        # optionally some default responses could be registered
        self.r_mock.get("https://example.com", status=505)
        self.r_mock.put("https://example.com", status=506)

    def teardown(self):
        """Stops and resets RequestsMock instance.

        If ``assert_all_requests_are_fired`` is set to ``True``, will raise an error
        if some requests were not processed.
        """
        self.r_mock.stop()
        self.r_mock.reset()

    def test_function(self):
        resp = requests.get("https://example.com")
        assert resp.status_code == 505

        resp = requests.put("https://example.com")
        assert resp.status_code == 506

对声明的响应进行断言

当用作上下文管理器时,默认情况下,如果已注册但未访问 URL,Responses 将引发断言错误。可以通过传递 assert_all_requests_are_fired 值来禁用此功能。

import responses
import requests


def test_my_api():
    with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
        rsps.add(
            responses.GET,
            "http://twitter.com/api/1/foobar",
            body="{}",
            status=200,
            content_type="application/json",
        )

断言请求调用次数

基于响应对象进行断言

每个 Response 对象都有一个 call_count 属性,可以用来检查每个请求匹配了多少次。

@responses.activate
def test_call_count_with_matcher():
    rsp = responses.get(
        "http://www.example.com",
        match=(matchers.query_param_matcher({}),),
    )
    rsp2 = responses.get(
        "http://www.example.com",
        match=(matchers.query_param_matcher({"hello": "world"}),),
        status=777,
    )
    requests.get("http://www.example.com")
    resp1 = requests.get("http://www.example.com")
    requests.get("http://www.example.com?hello=world")
    resp2 = requests.get("http://www.example.com?hello=world")

    assert resp1.status_code == 200
    assert resp2.status_code == 777

    assert rsp.call_count == 2
    assert rsp2.call_count == 2

基于精确URL进行断言

断言请求恰好被调用 n 次。

import responses
import requests


@responses.activate
def test_assert_call_count():
    responses.get("http://example.com")

    requests.get("http://example.com")
    assert responses.assert_call_count("http://example.com", 1) is True

    requests.get("http://example.com")
    with pytest.raises(AssertionError) as excinfo:
        responses.assert_call_count("http://example.com", 1)
    assert (
        "Expected URL 'http://example.com' to be called 1 times. Called 2 times."
        in str(excinfo.value)
    )


@responses.activate
def test_assert_call_count_always_match_qs():
    responses.get("http://www.example.com")
    requests.get("http://www.example.com")
    requests.get("http://www.example.com?hello=world")

    # One call on each url, querystring is matched by default
    responses.assert_call_count("http://www.example.com", 1) is True
    responses.assert_call_count("http://www.example.com?hello=world", 1) is True

断言请求调用数据

Request 对象有一个 calls 列表,其元素对应于全局 Registry 列表中的 Call 对象。当请求的顺序没有保证但需要检查它们的正确性时,例如在多线程应用程序中,这可能很有用。

import concurrent.futures
import responses
import requests


@responses.activate
def test_assert_calls_on_resp():
    rsp1 = responses.patch("http://www.foo.bar/1/", status=200)
    rsp2 = responses.patch("http://www.foo.bar/2/", status=400)
    rsp3 = responses.patch("http://www.foo.bar/3/", status=200)

    def update_user(uid, is_active):
        url = f"http://www.foo.bar/{uid}/"
        response = requests.patch(url, json={"is_active": is_active})
        return response

    with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
        future_to_uid = {
            executor.submit(update_user, uid, is_active): uid
            for (uid, is_active) in [("3", True), ("2", True), ("1", False)]
        }
        for future in concurrent.futures.as_completed(future_to_uid):
            uid = future_to_uid[future]
            response = future.result()
            print(f"{uid} updated with {response.status_code} status code")

    assert len(responses.calls) == 3  # total calls count

    assert rsp1.call_count == 1
    assert rsp1.calls[0] in responses.calls
    assert rsp1.calls[0].response.status_code == 200
    assert json.loads(rsp1.calls[0].request.body) == {"is_active": False}

    assert rsp2.call_count == 1
    assert rsp2.calls[0] in responses.calls
    assert rsp2.calls[0].response.status_code == 400
    assert json.loads(rsp2.calls[0].request.body) == {"is_active": True}

    assert rsp3.call_count == 1
    assert rsp3.calls[0] in responses.calls
    assert rsp3.calls[0].response.status_code == 200
    assert json.loads(rsp3.calls[0].request.body) == {"is_active": True}

多重响应

您还可以为同一 URL 添加多个响应。

import responses
import requests


@responses.activate
def test_my_api():
    responses.get("http://twitter.com/api/1/foobar", status=500)
    responses.get(
        "http://twitter.com/api/1/foobar",
        body="{}",
        status=200,
        content_type="application/json",
    )

    resp = requests.get("http://twitter.com/api/1/foobar")
    assert resp.status_code == 500
    resp = requests.get("http://twitter.com/api/1/foobar")
    assert resp.status_code == 200

URL重定向

在以下示例中,您可以了解如何创建重定向链并添加将在执行链中引发并包含重定向历史的自定义异常。

A -> 301 redirect -> B
B -> 301 redirect -> C
C -> connection issue
import pytest
import requests

import responses


@responses.activate
def test_redirect():
    # create multiple Response objects where first two contain redirect headers
    rsp1 = responses.Response(
        responses.GET,
        "http://example.com/1",
        status=301,
        headers={"Location": "http://example.com/2"},
    )
    rsp2 = responses.Response(
        responses.GET,
        "http://example.com/2",
        status=301,
        headers={"Location": "http://example.com/3"},
    )
    rsp3 = responses.Response(responses.GET, "http://example.com/3", status=200)

    # register above generated Responses in ``response`` module
    responses.add(rsp1)
    responses.add(rsp2)
    responses.add(rsp3)

    # do the first request in order to generate genuine ``requests`` response
    # this object will contain genuine attributes of the response, like ``history``
    rsp = requests.get("http://example.com/1")
    responses.calls.reset()

    # customize exception with ``response`` attribute
    my_error = requests.ConnectionError("custom error")
    my_error.response = rsp

    # update body of the 3rd response with Exception, this will be raised during execution
    rsp3.body = my_error

    with pytest.raises(requests.ConnectionError) as exc_info:
        requests.get("http://example.com/1")

    assert exc_info.value.args[0] == "custom error"
    assert rsp1.url in exc_info.value.response.history[0].url
    assert rsp2.url in exc_info.value.response.history[1].url

验证重试机制

如果您使用 urllib3Retry 功能并想覆盖测试重试限制的场景,您可以使用 responses 进行这些测试。最佳方法将是使用 有序注册表

import requests

import responses
from responses import registries
from urllib3.util import Retry


@responses.activate(registry=registries.OrderedRegistry)
def test_max_retries():
    url = "https://example.com"
    rsp1 = responses.get(url, body="Error", status=500)
    rsp2 = responses.get(url, body="Error", status=500)
    rsp3 = responses.get(url, body="Error", status=500)
    rsp4 = responses.get(url, body="OK", status=200)

    session = requests.Session()

    adapter = requests.adapters.HTTPAdapter(
        max_retries=Retry(
            total=4,
            backoff_factor=0.1,
            status_forcelist=[500],
            method_whitelist=["GET", "POST", "PATCH"],
        )
    )
    session.mount("https://", adapter)

    resp = session.get(url)

    assert resp.status_code == 200
    assert rsp1.call_count == 1
    assert rsp2.call_count == 1
    assert rsp3.call_count == 1
    assert rsp4.call_count == 1

使用回调修改响应

如果您通过子类化/混合或以低级别与 requests 交互的库工具使用自定义处理,您可能需要向模拟的响应对象添加扩展处理以完全模拟测试环境。可以使用 response_callback,该回调将在返回给调用者之前由库包装。回调接受一个作为其唯一参数的 response,并期望返回一个单个 response 对象。

import responses
import requests


def response_callback(resp):
    resp.callback_processed = True
    return resp


with responses.RequestsMock(response_callback=response_callback) as m:
    m.add(responses.GET, "http://example.com", body=b"test")
    resp = requests.get("http://example.com")
    assert resp.text == "test"
    assert hasattr(resp, "callback_processed")
    assert resp.callback_processed is True

传递真实请求

在某些情况下,您可能希望允许某些请求通过响应并击中真实服务器。这可以通过 add_passthru 方法来完成。

import responses


@responses.activate
def test_my_api():
    responses.add_passthru("https://percy.io")

这将允许任何匹配该前缀的请求,否则这些请求没有注册为模拟响应,将使用标准行为通过。

如果需要允许整个域名或路径子树发送请求,可以通过正则表达式模式配置通过端点。

responses.add_passthru(re.compile("https://percy.io/\\w+"))

最后,您可以使用 Response 对象的 passthrough 参数强制响应以通过端点行为。

# Enable passthrough for a single response
response = Response(
    responses.GET,
    "http://example.com",
    body="not used",
    passthrough=True,
)
responses.add(response)

# Use PassthroughResponse
response = PassthroughResponse(responses.GET, "http://example.com")
responses.add(response)

查看/修改已注册的响应

已注册的响应作为 RequestMock 实例的公共方法可用。有时,为了调试目的查看注册响应的堆栈很有用,这可以通过 responses.registered() 访问。

替换函数允许更改先前注册的响应。该方法签名与添加方法相同。response 通过 methodurl 进行标识。只有第一个匹配的 response 被替换。

import responses
import requests


@responses.activate
def test_replace():
    responses.get("http://example.org", json={"data": 1})
    responses.replace(responses.GET, "http://example.org", json={"data": 2})

    resp = requests.get("http://example.org")

    assert resp.json() == {"data": 2}

upsert 函数允许像替换一样更改先前注册的响应。如果响应已注册,upsert 函数将像 add 一样注册它。

remove 函数接受 methodurl 参数,并将从注册列表中删除所有匹配的响应。

最后,reset 将重置所有已注册的响应。

协程和多线程

responses 支持开箱即用的协程和多线程。注意,responses 锁定 RequestMock 对象的线程,允许单个线程访问。

async def test_async_calls():
    @responses.activate
    async def run():
        responses.get(
            "http://twitter.com/api/1/foobar",
            json={"error": "not found"},
            status=404,
        )

        resp = requests.get("http://twitter.com/api/1/foobar")
        assert resp.json() == {"error": "not found"}
        assert responses.calls[0].request.url == "http://twitter.com/api/1/foobar"

    await run()

贡献

环境配置

Responses 使用多个代码检查和自动格式化工具,因此提交补丁时使用适当的工具链非常重要。

克隆仓库

git clone https://github.com/getsentry/responses.git

创建环境(例如使用 virtualenv

virtualenv .env && source .env/bin/activate

配置开发需求

make develop

测试和代码质量验证

验证代码的最简单方法是通过 tox 运行测试。当前的 tox 配置运行与 GitHub Actions CI/CD 管道中使用的相同的检查。

请从项目根目录执行以下命令行以验证您的代码

  • 支持的所有 Python 版本的单元测试

  • 使用 mypy 进行类型验证

  • 所有 pre-commit 钩子

tox

或者,您始终可以运行单个测试。请参阅以下文档。

单元测试

Responses 使用 Pytest 进行测试。您可以通过以下方式运行所有测试

tox -e py37
tox -e py310

或者手动激活所需的 Python 版本并运行

pytest

通过以下方式运行单个测试

pytest -k '<test_function_name>'

类型验证

要验证类型兼容性,运行 mypy 检查器

tox -e mypy

或者

mypy --config-file=./mypy.ini -p responses

代码质量和样式

要检查代码样式并重新格式化,运行

tox -e precom

或者

pre-commit run --all-files

项目详情


发布历史 发布通知 | RSS 源

下载文件

下载适用于您平台的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。

源代码分发

responses-0.25.3.tar.gz (77.8 kB 查看哈希值)

上传时间 源代码

构建分发

responses-0.25.3-py3-none-any.whl (55.2 kB 查看哈希值)

上传时间 Python 3

由支持