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

TestClient accepts backend and backend_options as arguments #1210

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 15 additions & 8 deletions docs/testclient.md
Expand Up @@ -33,18 +33,25 @@ case you should use `client = TestClient(app, raise_server_exceptions=False)`.

### Selecting the Async backend

`TestClient.async_backend` is a dictionary which allows you to set the options
for the backend used to run tests. These options are passed to
`anyio.start_blocking_portal()`. See the [anyio documentation](https://anyio.readthedocs.io/en/stable/basics.html#backend-options)
for more information about backend options. By default, `asyncio` is used.
`TestClient` takes arguments `backend` (a string) and `backend_options` (a dictionary).
These options are passed to `anyio.start_blocking_portal()`. See the [anyio documentation](https://anyio.readthedocs.io/en/stable/basics.html#backend-options)
for more information about the accepted backend options.
By default, `asyncio` is used with default options.

To run `Trio`, set `async_backend["backend"] = "trio"`, for example:
To run `Trio`, pass `backend="trio"`. For example:

```python
def test_app()
client = TestClient(app)
client.async_backend["backend"] = "trio"
...
with TestClient(app, backend="trio") as client:
...
```

To run `asyncio` with `uvloop`, pass `backend_options={"use_uvloop": True}`. For example:

```python
def test_app()
with TestClient(app, backend_options={"use_uvloop": True}) as client:
...
```

### Testing WebSocket sessions
Expand Down
10 changes: 7 additions & 3 deletions starlette/testclient.py
Expand Up @@ -381,13 +381,11 @@ def receive_json(self, mode: str = "text") -> typing.Any:

class TestClient(requests.Session):
__test__ = False # For pytest to not discover this up.

#: These options are passed to `anyio.start_blocking_portal()`
#: These are the default options for the constructor arguments
graingert marked this conversation as resolved.
Show resolved Hide resolved
async_backend: typing.Dict[str, typing.Any] = {
Copy link
Member

Choose a reason for hiding this comment

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

Do we need this class-level declaration still? Class vars always confuse me, but this means somebody can do:

TestClient.async_backend["backend"] = "trio"

to default all future TestClient instances to use the Trio backend, right?

I know removing this would be slightly changing the API, however in the code examples we gave we only modified instance attributes and that usage should stay the same. What's your thinking around this?

Copy link
Member

@graingert graingert Jun 28, 2021

Choose a reason for hiding this comment

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

I've removed it in my followup PR: #1211

"backend": "asyncio",
"backend_options": {},
}

task: "Future[None]"

def __init__(
Expand All @@ -396,8 +394,14 @@ def __init__(
base_url: str = "http://testserver",
raise_server_exceptions: bool = True,
root_path: str = "",
backend: typing.Optional[str] = None,
backend_options: typing.Optional[typing.Dict[str, typing.Any]] = None,
) -> None:
super().__init__()
self.async_backend = {
"backend": backend or self.async_backend["backend"],
"backend_options": backend_options or self.async_backend["backend_options"],
}
if _is_asgi3(app):
app = typing.cast(ASGI3App, app)
asgi_app = app
Expand Down
19 changes: 19 additions & 0 deletions tests/test_testclient.py
Expand Up @@ -132,3 +132,22 @@ async def asgi(receive, send):
with client.websocket_connect("/") as websocket:
data = websocket.receive_json()
assert data == {"message": "test"}


def test_backend_name(request):
"""
Test that the tests are defaulting to the correct backend and that a new
instance of TestClient can be created using different backend options.
"""
# client created using monkeypatched async_backend
client1 = TestClient(mock_service)
if "trio" in request.keywords:
client2 = TestClient(mock_service, backend="asyncio")
assert client1.async_backend["backend"] == "trio"
assert client2.async_backend["backend"] == "asyncio"
elif "asyncio" in request.keywords:
client2 = TestClient(mock_service, backend="trio")
assert client1.async_backend["backend"] == "asyncio"
assert client2.async_backend["backend"] == "trio"
else:
pytest.fail("Unknown backend") # pragma: nocover