Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(testing): add msgpack support #2394

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion docs/changes/4.1.0.rst
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Newsfragment file missing. You don't need to manually include it in 4.1.0.rst, but rather create a separate newsfragment file. Please check our docs how to contribute these files.

Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,21 @@ on GitHub.

.. NOTE(vytas): No changes to the supported platforms (yet).


arthurprioli marked this conversation as resolved.
Show resolved Hide resolved
.. towncrier release notes start


Misc
arthurprioli marked this conversation as resolved.
Show resolved Hide resolved
----

- Running mypy on code that uses parts of ``falcon.testing`` naively
would lead to errors like::

Name "falcon.testing.TestClient" is not defined

This has been fixed by explicitly exporting the names that are
imported in the ``falcon.testing`` namespace. (`#2387 <https://github.com/falconry/falcon/issues/2387>`__)


Contributors to this Release
----------------------------

Expand Down
40 changes: 40 additions & 0 deletions falcon/testing/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@
from falcon.asgi_spec import ScopeType
from falcon.constants import COMBINED_METHODS
from falcon.constants import MEDIA_JSON
from falcon.constants import MEDIA_MSGPACK
from falcon.errors import CompatibilityError
from falcon.media import MessagePackHandler
from falcon.testing import helpers
from falcon.testing.srmock import StartResponseMock
from falcon.typing import Headers
Expand Down Expand Up @@ -455,6 +457,7 @@ def simulate_request(
content_type: Optional[str] = None,
body: Optional[Union[str, bytes]] = None,
json: Optional[Any] = None,
msgpack: Optional[Any] = None,
file_wrapper: Optional[Callable[..., Any]] = None,
wsgierrors: Optional[TextIO] = None,
params: Optional[Mapping[str, Any]] = None,
Expand Down Expand Up @@ -592,6 +595,7 @@ def simulate_request(
content_type=content_type,
body=body,
json=json,
msgpack=msgpack,
params=params,
params_csv=params_csv,
protocol=protocol,
Expand All @@ -615,6 +619,7 @@ def simulate_request(
headers,
body,
json,
msgpack,
extras,
)

Expand Down Expand Up @@ -724,6 +729,7 @@ async def _simulate_request_asgi(
content_type: Optional[str] = None,
body: Optional[Union[str, bytes]] = None,
json: Optional[Any] = None,
msgpack: Optional[Any] = None,
params: Optional[Mapping[str, Any]] = None,
params_csv: bool = False,
protocol: str = 'http',
Expand Down Expand Up @@ -808,6 +814,11 @@ async def _simulate_request_asgi(
overrides `body` and sets the Content-Type header to
``'application/json'``, overriding any value specified by either
the `content_type` or `headers` arguments.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
vytas7 marked this conversation as resolved.
Show resolved Hide resolved
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
``'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments.
host(str): A string to use for the hostname part of the fully
qualified request URL (default: 'falconframework.org')
remote_addr (str): A string to use as the remote IP address for the
Expand Down Expand Up @@ -846,6 +857,7 @@ async def _simulate_request_asgi(
headers,
body,
json,
msgpack,
extras,
)

Expand Down Expand Up @@ -1551,6 +1563,11 @@ def simulate_post(app: Callable[..., Any], path: str, **kwargs: Any) -> Result:
overrides `body` and sets the Content-Type header to
``'application/json'``, overriding any value specified by either
the `content_type` or `headers` arguments.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
``'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments.
file_wrapper (callable): Callable that returns an iterable,
to be used as the value for *wsgi.file_wrapper* in the
WSGI environ (default: ``None``). This can be used to test
Expand Down Expand Up @@ -1662,6 +1679,11 @@ def simulate_put(app: Callable[..., Any], path: str, **kwargs: Any) -> Result:
overrides `body` and sets the Content-Type header to
``'application/json'``, overriding any value specified by either
the `content_type` or `headers` arguments.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
``'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments.
file_wrapper (callable): Callable that returns an iterable,
to be used as the value for *wsgi.file_wrapper* in the
WSGI environ (default: ``None``). This can be used to test
Expand Down Expand Up @@ -1862,6 +1884,11 @@ def simulate_patch(app: Callable[..., Any], path: str, **kwargs: Any) -> Result:
overrides `body` and sets the Content-Type header to
``'application/json'``, overriding any value specified by either
the `content_type` or `headers` arguments.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
`'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments.
host(str): A string to use for the hostname part of the fully
qualified request URL (default: 'falconframework.org')
remote_addr (str): A string to use as the remote IP address for the
Expand Down Expand Up @@ -1968,6 +1995,11 @@ def simulate_delete(app: Callable[..., Any], path: str, **kwargs: Any) -> Result
overrides `body` and sets the Content-Type header to
``'application/json'``, overriding any value specified by either
the `content_type` or `headers` arguments.
msgpack(Msgpack serializable): A Msgpack document to serialize as the
body of the request (default: ``None``). If specified,
overrides `body` and sets the Content-Type header to
``'application/msgpack'``, overriding any value specified by
either the `content_type` or `headers` arguments.
host(str): A string to use for the hostname part of the fully
qualified request URL (default: 'falconframework.org')
remote_addr (str): A string to use as the remote IP address for the
Expand Down Expand Up @@ -2248,6 +2280,7 @@ def _prepare_sim_args(
headers: Optional[HeaderArg],
body: Optional[Union[str, bytes]],
json: Optional[Any],
msgpack: Optional[Any],
extras: Optional[Mapping[str, Any]],
) -> Tuple[
str, str, Optional[HeaderArg], Optional[Union[str, bytes]], Mapping[str, Any]
Expand Down Expand Up @@ -2284,6 +2317,13 @@ def _prepare_sim_args(
headers = dict(headers or {})
headers['Content-Type'] = MEDIA_JSON

if msgpack is not None:
body = MessagePackHandler.serialize(
MessagePackHandler(), content_type=None, media=msgpack
arthurprioli marked this conversation as resolved.
Show resolved Hide resolved
)
headers = headers or {}
headers['Content-Type'] = MEDIA_MSGPACK

return path, query_string, headers, body, extras


Expand Down
14 changes: 14 additions & 0 deletions tests/test_testing.py
Copy link
Member

@vytas7 vytas7 Nov 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need more extensive tests for this new feature.
You have a couple of test cases there, but we should also check whether the simulated body is correct, not just the content type.

I would suggest to use pytest.mark.parametrize(...) to create multiple test cases from different test data, but the same test code.

We also want to test the combination of msgpack= together with other parameters such as json=, etc, in order to verify that the documented precedence order is correct.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a doubt about that, with json we pass parameters (json), (json, headers), (json,content-type), (json, headers, content-type), I did similar tests with both msgpack and json with msgpack, but mintests still breaks. What's the best way to parametrize it? And how do I check if simulated body is correct?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can parametrize a parameter by specifying different values that it is called with.

As to checking whether a simulated body is correct, you can capture it in your test application that you are simulating requests against, and return it in the response. Or, you can use SimpleTestResource in your code.

Copy link
Author

@arthurprioli arthurprioli Jan 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @vytas7, I have two doubts:

  • Should I parametrize all of the parameters like content type and headers or just msgpack and json?
@pytest.mark.parametrize(
    'json,msgpack,result',
    [({}, None, falcon.MEDIA_JSON),
     (None, {}, falcon.MEDIA_MSGPACK),
     ({}, {}, falcon.MEDIA_MSGPACK),
     ]
)
.
.
.
.
    result = testing.simulate_post(app, '/', json=json, msgpack=msgpack)
    assert result.text == result

    result = testing.simulate_post(app, '/', json=json, msgpack=msgpack, content_type=falcon.MEDIA_HTML)
    assert result.text == result

    result = testing.simulate_post(app, '/', json=json, msgpack=msgpack, headers=headers)
    assert result.text == result

    result = testing.simulate_post(app, '/', json=json, msgpack=msgpack, headers=headers, content_type=falcon.MEDIA_HTML)
    assert result.text == result
  • Should I make another test function for msgpack parameters? Or can I just skipif the whole function and keep it all in the same as the json?

Copy link
Member

@vytas7 vytas7 Nov 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to add/adapt tests checking what happens when msgpack is not installed.
Judging from the tox -e mintest output, this case is not always handled as expected in the proposed changeset, at least not in the tests.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would I do that? Would it be with a try, except statement while importing msgpack? Or is there a better way for testing it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check other existing tests, e.g., tests/test_media_multipart.py, or many others.

You can reuse the following pattern:

# Somewhere in the beginning of the file, towards the end of the import blocks:
try:
    import msgpack
except ImportError:
    msgpack = None

Then, later, shield your tests in question with the pytest.mark.skipif(...) decorator, e.g.:

@pytest.mark.skipif(msgpack is None, reason='msgpack is required for this test')
def test_my_new_msgpack_parameter(some_param, ...):
    ...

Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,20 @@ def on_post(self, req, resp):
)
assert result.text == falcon.MEDIA_JSON

result = testing.simulate_post(app, '/', msgpack={})
assert result.text == falcon.MEDIA_MSGPACK

result = testing.simulate_post(app, '/', msgpack={}, content_type=falcon.MEDIA_HTML)
assert result.text == falcon.MEDIA_MSGPACK

result = testing.simulate_post(app, '/', msgpack={}, headers=headers)
assert result.text == falcon.MEDIA_MSGPACK

result = testing.simulate_post(
app, '/', msgpack={}, headers=headers, content_type=falcon.MEDIA_HTML
)
assert result.text == falcon.MEDIA_MSGPACK
arthurprioli marked this conversation as resolved.
Show resolved Hide resolved


@pytest.mark.parametrize('mode', ['wsgi', 'asgi', 'asgi-stream'])
def test_content_type(util, mode):
Expand Down
Loading