Repository: encode/httpx

Issue:
Backend test parametrization
Now that almost all of HTTPX internals are relying on the concurrency backend, and before tackling the Trio backend (#120), it would be good to parametrize all tests with a `backend` fixture.

For now, it would only point to `AsyncioBackend`, but we're going to need it for `TrioBackend` anyway — the main goal is to reduce clutter when the trio backend lands a PR.

Once the `backend` fixture is defined, any test that uses `AsyncClient`, `HTTPConnection` or `ConnectionPool` can basically be parametrized with it, e.g.:

```python
# conftest.py
@pytest.fixture(params=[pytest.param(AsyncioBackend, marks=pytest.mark.asyncio)])
def backend(request):
    backend_cls = request.param
    return backend_cls()

# test_async_client.py
async def test_get(backend):
    async with AsyncClient(backend=backend) as client:
```

I'll tackle this look soon, but if anyone has thoughts on this approach let me know. :-)


Discussion:

Resolving patch:
diff --git a/httpx/client.py b/httpx/client.py
--- a/httpx/client.py
+++ b/httpx/client.py
@@ -633,12 +633,17 @@ def check_concurrency_backend(self, backend: ConcurrencyBackend) -> None:
         # concurrency backends.
         # The sync client performs I/O on its own, so it doesn't need to support
         # arbitrary concurrency backends.
-        # Therefore, we kept the `backend` parameter (for testing/mocking), but enforce
-        # that the concurrency backend derives from the asyncio one.
-        if not isinstance(backend, AsyncioBackend):
-            raise ValueError(
-                "'Client' only supports asyncio-based concurrency backends"
-            )
+        # Therefore, we keep the `backend` parameter (for testing/mocking), but require
+        # that the concurrency backend relies on asyncio.
+
+        if isinstance(backend, AsyncioBackend):
+            return
+
+        if hasattr(backend, "loop"):
+            # Most likely a proxy class.
+            return
+
+        raise ValueError("'Client' only supports asyncio-based concurrency backends")
 
     def _async_request_data(
         self, data: RequestData = None


Test patch:
diff --git a/tests/client/test_async_client.py b/tests/client/test_async_client.py
--- a/tests/client/test_async_client.py
+++ b/tests/client/test_async_client.py
@@ -3,10 +3,9 @@
 import httpx
 
 
-@pytest.mark.asyncio
-async def test_get(server):
+async def test_get(server, backend):
     url = "http://127.0.0.1:8000/"
-    async with httpx.AsyncClient() as client:
+    async with httpx.AsyncClient(backend=backend) as client:
         response = await client.get(url)
     assert response.status_code == 200
     assert response.text == "Hello, world!"
@@ -16,24 +15,36 @@ async def test_get(server):
 
 
 @pytest.mark.asyncio
-async def test_post(server):
+async def test_get_no_backend(server):
+    """
+    Verify that the client is capable of making a simple request if not given a backend.
+    """
     url = "http://127.0.0.1:8000/"
     async with httpx.AsyncClient() as client:
+        response = await client.get(url)
+    assert response.status_code == 200
+    assert response.text == "Hello, world!"
+    assert response.http_version == "HTTP/1.1"
+    assert response.headers
+    assert repr(response) == "<Response [200 OK]>"
+
+
+async def test_post(server, backend):
+    url = "http://127.0.0.1:8000/"
+    async with httpx.AsyncClient(backend=backend) as client:
         response = await client.post(url, data=b"Hello, world!")
     assert response.status_code == 200
 
 
-@pytest.mark.asyncio
-async def test_post_json(server):
+async def test_post_json(server, backend):
     url = "http://127.0.0.1:8000/"
-    async with httpx.AsyncClient() as client:
+    async with httpx.AsyncClient(backend=backend) as client:
         response = await client.post(url, json={"text": "Hello, world!"})
     assert response.status_code == 200
 
 
-@pytest.mark.asyncio
-async def test_stream_response(server):
-    async with httpx.AsyncClient() as client:
+async def test_stream_response(server, backend):
+    async with httpx.AsyncClient(backend=backend) as client:
         response = await client.request("GET", "http://127.0.0.1:8000/", stream=True)
     assert response.status_code == 200
     body = await response.read()
@@ -41,31 +52,28 @@ async def test_stream_response(server):
     assert response.content == b"Hello, world!"
 
 
-@pytest.mark.asyncio
-async def test_access_content_stream_response(server):
-    async with httpx.AsyncClient() as client:
+async def test_access_content_stream_response(server, backend):
+    async with httpx.AsyncClient(backend=backend) as client:
         response = await client.request("GET", "http://127.0.0.1:8000/", stream=True)
     assert response.status_code == 200
     with pytest.raises(httpx.ResponseNotRead):
         response.content
 
 
-@pytest.mark.asyncio
-async def test_stream_request(server):
+async def test_stream_request(server, backend):
     async def hello_world():
         yield b"Hello, "
         yield b"world!"
 
-    async with httpx.AsyncClient() as client:
+    async with httpx.AsyncClient(backend=backend) as client:
         response = await client.request(
             "POST", "http://127.0.0.1:8000/", data=hello_world()
         )
     assert response.status_code == 200
 
 
-@pytest.mark.asyncio
-async def test_raise_for_status(server):
-    async with httpx.AsyncClient() as client:
+async def test_raise_for_status(server, backend):
+    async with httpx.AsyncClient(backend=backend) as client:
         for status_code in (200, 400, 404, 500, 505):
             response = await client.request(
                 "GET", f"http://127.0.0.1:8000/status/{status_code}"
@@ -79,56 +87,50 @@ async def test_raise_for_status(server):
                 assert response.raise_for_status() is None
 
 
-@pytest.mark.asyncio
-async def test_options(server):
+async def test_options(server, backend):
     url = "http://127.0.0.1:8000/"
-    async with httpx.AsyncClient() as client:
+    async with httpx.AsyncClient(backend=backend) as client:
         response = await client.options(url)
     assert response.status_code == 200
     assert response.text == "Hello, world!"
 
 
-@pytest.mark.asyncio
-async def test_head(server):
+async def test_head(server, backend):
     url = "http://127.0.0.1:8000/"
-    async with httpx.AsyncClient() as client:
+    async with httpx.AsyncClient(backend=backend) as client:
         response = await client.head(url)
     assert response.status_code == 200
     assert response.text == ""
 
 
-@pytest.mark.asyncio
-async def test_put(server):
+async def test_put(server, backend):
     url = "http://127.0.0.1:8000/"
-    async with httpx.AsyncClient() as client:
+    async with httpx.AsyncClient(backend=backend) as client:
         response = await client.put(url, data=b"Hello, world!")
     assert response.status_code == 200
 
 
-@pytest.mark.asyncio
-async def test_patch(server):
+async def test_patch(server, backend):
     url = "http://127.0.0.1:8000/"
-    async with httpx.AsyncClient() as client:
+    async with httpx.AsyncClient(backend=backend) as client:
         response = await client.patch(url, data=b"Hello, world!")
     assert response.status_code == 200
 
 
-@pytest.mark.asyncio
-async def test_delete(server):
+async def test_delete(server, backend):
     url = "http://127.0.0.1:8000/"
-    async with httpx.AsyncClient() as client:
+    async with httpx.AsyncClient(backend=backend) as client:
         response = await client.delete(url)
     assert response.status_code == 200
     assert response.text == "Hello, world!"
 
 
-@pytest.mark.asyncio
-async def test_100_continue(server):
+async def test_100_continue(server, backend):
     url = "http://127.0.0.1:8000/echo_body"
     headers = {"Expect": "100-continue"}
     data = b"Echo request body"
 
-    async with httpx.AsyncClient() as client:
+    async with httpx.AsyncClient(backend=backend) as client:
         response = await client.post(url, headers=headers, data=data)
 
     assert response.status_code == 200
diff --git a/tests/client/test_redirects.py b/tests/client/test_redirects.py
--- a/tests/client/test_redirects.py
+++ b/tests/client/test_redirects.py
@@ -100,36 +100,32 @@ async def send(
         return AsyncResponse(codes.OK, content=b"Hello, world!", request=request)
 
 
-@pytest.mark.asyncio
-async def test_redirect_301():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_redirect_301(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     response = await client.post("https://example.org/redirect_301")
     assert response.status_code == codes.OK
     assert response.url == URL("https://example.org/")
     assert len(response.history) == 1
 
 
-@pytest.mark.asyncio
-async def test_redirect_302():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_redirect_302(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     response = await client.post("https://example.org/redirect_302")
     assert response.status_code == codes.OK
     assert response.url == URL("https://example.org/")
     assert len(response.history) == 1
 
 
-@pytest.mark.asyncio
-async def test_redirect_303():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_redirect_303(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     response = await client.get("https://example.org/redirect_303")
     assert response.status_code == codes.OK
     assert response.url == URL("https://example.org/")
     assert len(response.history) == 1
 
 
-@pytest.mark.asyncio
-async def test_disallow_redirects():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_disallow_redirects(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     response = await client.post(
         "https://example.org/redirect_303", allow_redirects=False
     )
@@ -145,36 +141,32 @@ async def test_disallow_redirects():
     assert len(response.history) == 1
 
 
-@pytest.mark.asyncio
-async def test_relative_redirect():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_relative_redirect(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     response = await client.get("https://example.org/relative_redirect")
     assert response.status_code == codes.OK
     assert response.url == URL("https://example.org/")
     assert len(response.history) == 1
 
 
-@pytest.mark.asyncio
-async def test_no_scheme_redirect():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_no_scheme_redirect(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     response = await client.get("https://example.org/no_scheme_redirect")
     assert response.status_code == codes.OK
     assert response.url == URL("https://example.org/")
     assert len(response.history) == 1
 
 
-@pytest.mark.asyncio
-async def test_fragment_redirect():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_fragment_redirect(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     response = await client.get("https://example.org/relative_redirect#fragment")
     assert response.status_code == codes.OK
     assert response.url == URL("https://example.org/#fragment")
     assert len(response.history) == 1
 
 
-@pytest.mark.asyncio
-async def test_multiple_redirects():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_multiple_redirects(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     response = await client.get("https://example.org/multiple_redirects?count=20")
     assert response.status_code == codes.OK
     assert response.url == URL("https://example.org/multiple_redirects")
@@ -189,16 +181,14 @@ async def test_multiple_redirects():
     assert len(response.history[1].history) == 1
 
 
-@pytest.mark.asyncio
-async def test_too_many_redirects():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_too_many_redirects(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     with pytest.raises(TooManyRedirects):
         await client.get("https://example.org/multiple_redirects?count=21")
 
 
-@pytest.mark.asyncio
-async def test_too_many_redirects_calling_next():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_too_many_redirects_calling_next(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     url = "https://example.org/multiple_redirects?count=21"
     response = await client.get(url, allow_redirects=False)
     with pytest.raises(TooManyRedirects):
@@ -206,16 +196,14 @@ async def test_too_many_redirects_calling_next():
             response = await response.next()
 
 
-@pytest.mark.asyncio
-async def test_redirect_loop():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_redirect_loop(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     with pytest.raises(RedirectLoop):
         await client.get("https://example.org/redirect_loop")
 
 
-@pytest.mark.asyncio
-async def test_redirect_loop_calling_next():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_redirect_loop_calling_next(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     url = "https://example.org/redirect_loop"
     response = await client.get(url, allow_redirects=False)
     with pytest.raises(RedirectLoop):
@@ -223,9 +211,8 @@ async def test_redirect_loop_calling_next():
             response = await response.next()
 
 
-@pytest.mark.asyncio
-async def test_cross_domain_redirect():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_cross_domain_redirect(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     url = "https://example.com/cross_domain"
     headers = {"Authorization": "abc"}
     response = await client.get(url, headers=headers)
@@ -233,9 +220,8 @@ async def test_cross_domain_redirect():
     assert "authorization" not in response.json()["headers"]
 
 
-@pytest.mark.asyncio
-async def test_same_domain_redirect():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_same_domain_redirect(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     url = "https://example.org/cross_domain"
     headers = {"Authorization": "abc"}
     response = await client.get(url, headers=headers)
@@ -243,9 +229,8 @@ async def test_same_domain_redirect():
     assert response.json()["headers"]["authorization"] == "abc"
 
 
-@pytest.mark.asyncio
-async def test_body_redirect():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_body_redirect(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     url = "https://example.org/redirect_body"
     data = b"Example request body"
     response = await client.post(url, data=data)
@@ -253,9 +238,8 @@ async def test_body_redirect():
     assert response.json() == {"body": "Example request body"}
 
 
-@pytest.mark.asyncio
-async def test_cannot_redirect_streaming_body():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_cannot_redirect_streaming_body(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     url = "https://example.org/redirect_body"
 
     async def streaming_body():
@@ -265,9 +249,8 @@ async def streaming_body():
         await client.post(url, data=streaming_body())
 
 
-@pytest.mark.asyncio
-async def test_cross_dubdomain_redirect():
-    client = AsyncClient(dispatch=MockDispatch())
+async def test_cross_dubdomain_redirect(backend):
+    client = AsyncClient(dispatch=MockDispatch(), backend=backend)
     url = "https://example.com/cross_subdomain"
     response = await client.get(url)
     assert response.url == URL("https://www.example.org/cross_subdomain")
diff --git a/tests/concurrency.py b/tests/concurrency.py
new file mode 100644
--- /dev/null
+++ b/tests/concurrency.py
@@ -0,0 +1,19 @@
+"""
+This module contains concurrency utilities that are only used in tests, thus not
+required as part of the ConcurrencyBackend API.
+"""
+
+import asyncio
+import functools
+
+from httpx import AsyncioBackend
+
+
+@functools.singledispatch
+async def sleep(backend, seconds: int):
+    raise NotImplementedError  # pragma: no cover
+
+
+@sleep.register(AsyncioBackend)
+async def _sleep_asyncio(backend, seconds: int):
+    await asyncio.sleep(seconds)
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,6 +10,14 @@
 from uvicorn.config import Config
 from uvicorn.main import Server
 
+from httpx import AsyncioBackend
+
+
+@pytest.fixture(params=[pytest.param(AsyncioBackend, marks=pytest.mark.asyncio)])
+def backend(request):
+    backend_cls = request.param
+    return backend_cls()
+
 
 async def app(scope, receive, send):
     assert scope["type"] == "http"
@@ -150,3 +158,19 @@ async def https_server(cert_pem_file, cert_private_key_file):
     finally:
         server.should_exit = True
         await task
+
+
+@pytest.fixture
+def restart(backend):
+    async def asyncio_restart(server):
+        await server.shutdown()
+        await server.startup()
+
+    if isinstance(backend, AsyncioBackend):
+        return asyncio_restart
+
+    # The uvicorn server runs under asyncio, so we will need to figure out
+    # how to restart it under a different I/O library.
+    # This will most likely require running `asyncio_restart` in the threadpool,
+    # but that might not be sufficient.
+    raise NotImplementedError
diff --git a/tests/dispatch/test_connection_pools.py b/tests/dispatch/test_connection_pools.py
--- a/tests/dispatch/test_connection_pools.py
+++ b/tests/dispatch/test_connection_pools.py
@@ -1,14 +1,11 @@
-import pytest
-
 import httpx
 
 
-@pytest.mark.asyncio
-async def test_keepalive_connections(server):
+async def test_keepalive_connections(server, backend):
     """
     Connections should default to staying in a keep-alive state.
     """
-    async with httpx.ConnectionPool() as http:
+    async with httpx.ConnectionPool(backend=backend) as http:
         response = await http.request("GET", "http://127.0.0.1:8000/")
         await response.read()
         assert len(http.active_connections) == 0
@@ -20,12 +17,11 @@ async def test_keepalive_connections(server):
         assert len(http.keepalive_connections) == 1
 
 
-@pytest.mark.asyncio
-async def test_differing_connection_keys(server):
+async def test_differing_connection_keys(server, backend):
     """
     Connections to differing connection keys should result in multiple connections.
     """
-    async with httpx.ConnectionPool() as http:
+    async with httpx.ConnectionPool(backend=backend) as http:
         response = await http.request("GET", "http://127.0.0.1:8000/")
         await response.read()
         assert len(http.active_connections) == 0
@@ -37,14 +33,13 @@ async def test_differing_connection_keys(server):
         assert len(http.keepalive_connections) == 2
 
 
-@pytest.mark.asyncio
-async def test_soft_limit(server):
+async def test_soft_limit(server, backend):
     """
     The soft_limit config should limit the maximum number of keep-alive connections.
     """
     pool_limits = httpx.PoolLimits(soft_limit=1)
 
-    async with httpx.ConnectionPool(pool_limits=pool_limits) as http:
+    async with httpx.ConnectionPool(pool_limits=pool_limits, backend=backend) as http:
         response = await http.request("GET", "http://127.0.0.1:8000/")
         await response.read()
         assert len(http.active_connections) == 0
@@ -56,12 +51,11 @@ async def test_soft_limit(server):
         assert len(http.keepalive_connections) == 1
 
 
-@pytest.mark.asyncio
-async def test_streaming_response_holds_connection(server):
+async def test_streaming_response_holds_connection(server, backend):
     """
     A streaming request should hold the connection open until the response is read.
     """
-    async with httpx.ConnectionPool() as http:
+    async with httpx.ConnectionPool(backend=backend) as http:
         response = await http.request("GET", "http://127.0.0.1:8000/")
         assert len(http.active_connections) == 1
         assert len(http.keepalive_connections) == 0
@@ -72,12 +66,11 @@ async def test_streaming_response_holds_connection(server):
         assert len(http.keepalive_connections) == 1
 
 
-@pytest.mark.asyncio
-async def test_multiple_concurrent_connections(server):
+async def test_multiple_concurrent_connections(server, backend):
     """
     Multiple conncurrent requests should open multiple conncurrent connections.
     """
-    async with httpx.ConnectionPool() as http:
+    async with httpx.ConnectionPool(backend=backend) as http:
         response_a = await http.request("GET", "http://127.0.0.1:8000/")
         assert len(http.active_connections) == 1
         assert len(http.keepalive_connections) == 0
@@ -95,25 +88,23 @@ async def test_multiple_concurrent_connections(server):
         assert len(http.keepalive_connections) == 2
 
 
-@pytest.mark.asyncio
-async def test_close_connections(server):
+async def test_close_connections(server, backend):
     """
     Using a `Connection: close` header should close the connection.
     """
     headers = [(b"connection", b"close")]
-    async with httpx.ConnectionPool() as http:
+    async with httpx.ConnectionPool(backend=backend) as http:
         response = await http.request("GET", "http://127.0.0.1:8000/", headers=headers)
         await response.read()
         assert len(http.active_connections) == 0
         assert len(http.keepalive_connections) == 0
 
 
-@pytest.mark.asyncio
-async def test_standard_response_close(server):
+async def test_standard_response_close(server, backend):
     """
     A standard close should keep the connection open.
     """
-    async with httpx.ConnectionPool() as http:
+    async with httpx.ConnectionPool(backend=backend) as http:
         response = await http.request("GET", "http://127.0.0.1:8000/")
         await response.read()
         await response.close()
@@ -121,31 +112,30 @@ async def test_standard_response_close(server):
         assert len(http.keepalive_connections) == 1
 
 
-@pytest.mark.asyncio
-async def test_premature_response_close(server):
+async def test_premature_response_close(server, backend):
     """
     A premature close should close the connection.
     """
-    async with httpx.ConnectionPool() as http:
+    async with httpx.ConnectionPool(backend=backend) as http:
         response = await http.request("GET", "http://127.0.0.1:8000/")
         await response.close()
         assert len(http.active_connections) == 0
         assert len(http.keepalive_connections) == 0
 
 
-@pytest.mark.asyncio
-async def test_keepalive_connection_closed_by_server_is_reestablished(server):
+async def test_keepalive_connection_closed_by_server_is_reestablished(
+    server, restart, backend
+):
     """
     Upon keep-alive connection closed by remote a new connection
     should be reestablished.
     """
-    async with httpx.ConnectionPool() as http:
+    async with httpx.ConnectionPool(backend=backend) as http:
         response = await http.request("GET", "http://127.0.0.1:8000/")
         await response.read()
 
         # shutdown the server to close the keep-alive connection
-        await server.shutdown()
-        await server.startup()
+        await restart(server)
 
         response = await http.request("GET", "http://127.0.0.1:8000/")
         await response.read()
@@ -153,19 +143,19 @@ async def test_keepalive_connection_closed_by_server_is_reestablished(server):
         assert len(http.keepalive_connections) == 1
 
 
-@pytest.mark.asyncio
-async def test_keepalive_http2_connection_closed_by_server_is_reestablished(server):
+async def test_keepalive_http2_connection_closed_by_server_is_reestablished(
+    server, restart, backend
+):
     """
     Upon keep-alive connection closed by remote a new connection
     should be reestablished.
     """
-    async with httpx.ConnectionPool() as http:
+    async with httpx.ConnectionPool(backend=backend) as http:
         response = await http.request("GET", "http://127.0.0.1:8000/")
         await response.read()
 
         # shutdown the server to close the keep-alive connection
-        await server.shutdown()
-        await server.startup()
+        await restart(server)
 
         response = await http.request("GET", "http://127.0.0.1:8000/")
         await response.read()
@@ -173,8 +163,7 @@ async def test_keepalive_http2_connection_closed_by_server_is_reestablished(serv
         assert len(http.keepalive_connections) == 1
 
 
-@pytest.mark.asyncio
-async def test_connection_closed_free_semaphore_on_acquire(server):
+async def test_connection_closed_free_semaphore_on_acquire(server, restart, backend):
     """
     Verify that max_connections semaphore is released
     properly on a disconnected connection.
@@ -184,8 +173,7 @@ async def test_connection_closed_free_semaphore_on_acquire(server):
         await response.read()
 
         # Close the connection so we're forced to recycle it
-        await server.shutdown()
-        await server.startup()
+        await restart(server)
 
         response = await http.request("GET", "http://127.0.0.1:8000/")
         assert response.status_code == 200
diff --git a/tests/dispatch/test_connections.py b/tests/dispatch/test_connections.py
--- a/tests/dispatch/test_connections.py
+++ b/tests/dispatch/test_connections.py
@@ -1,44 +1,40 @@
-import pytest
-
 from httpx import HTTPConnection
 
 
-@pytest.mark.asyncio
-async def test_get(server):
-    conn = HTTPConnection(origin="http://127.0.0.1:8000/")
+async def test_get(server, backend):
+    conn = HTTPConnection(origin="http://127.0.0.1:8000/", backend=backend)
     response = await conn.request("GET", "http://127.0.0.1:8000/")
     await response.read()
     assert response.status_code == 200
     assert response.content == b"Hello, world!"
 
 
-@pytest.mark.asyncio
-async def test_post(server):
-    conn = HTTPConnection(origin="http://127.0.0.1:8000/")
+async def test_post(server, backend):
+    conn = HTTPConnection(origin="http://127.0.0.1:8000/", backend=backend)
     response = await conn.request(
         "GET", "http://127.0.0.1:8000/", data=b"Hello, world!"
     )
     assert response.status_code == 200
 
 
-@pytest.mark.asyncio
-async def test_https_get_with_ssl_defaults(https_server):
+async def test_https_get_with_ssl_defaults(https_server, backend):
     """
     An HTTPS request, with default SSL configuration set on the client.
     """
-    conn = HTTPConnection(origin="https://127.0.0.1:8001/", verify=False)
+    conn = HTTPConnection(
+        origin="https://127.0.0.1:8001/", verify=False, backend=backend
+    )
     response = await conn.request("GET", "https://127.0.0.1:8001/")
     await response.read()
     assert response.status_code == 200
     assert response.content == b"Hello, world!"
 
 
-@pytest.mark.asyncio
-async def test_https_get_with_sll_overrides(https_server):
+async def test_https_get_with_sll_overrides(https_server, backend):
     """
     An HTTPS request, with SSL configuration set on the request.
     """
-    conn = HTTPConnection(origin="https://127.0.0.1:8001/")
+    conn = HTTPConnection(origin="https://127.0.0.1:8001/", backend=backend)
     response = await conn.request("GET", "https://127.0.0.1:8001/", verify=False)
     await response.read()
     assert response.status_code == 200
diff --git a/tests/dispatch/test_http2.py b/tests/dispatch/test_http2.py
--- a/tests/dispatch/test_http2.py
+++ b/tests/dispatch/test_http2.py
@@ -1,7 +1,5 @@
 import json
 
-import pytest
-
 from httpx import AsyncClient, Client, Response
 
 from .utils import MockHTTP2Backend
@@ -29,9 +27,8 @@ def test_http2_get_request():
     assert json.loads(response.content) == {"method": "GET", "path": "/", "body": ""}
 
 
-@pytest.mark.asyncio
-async def test_async_http2_get_request():
-    backend = MockHTTP2Backend(app=app)
+async def test_async_http2_get_request(backend):
+    backend = MockHTTP2Backend(app=app, backend=backend)
 
     async with AsyncClient(backend=backend) as client:
         response = await client.get("http://example.org")
@@ -54,9 +51,8 @@ def test_http2_post_request():
     }
 
 
-@pytest.mark.asyncio
-async def test_async_http2_post_request():
-    backend = MockHTTP2Backend(app=app)
+async def test_async_http2_post_request(backend):
+    backend = MockHTTP2Backend(app=app, backend=backend)
 
     async with AsyncClient(backend=backend) as client:
         response = await client.post("http://example.org", data=b"<data>")
@@ -87,9 +83,8 @@ def test_http2_multiple_requests():
     assert json.loads(response_3.content) == {"method": "GET", "path": "/3", "body": ""}
 
 
-@pytest.mark.asyncio
-async def test_async_http2_multiple_requests():
-    backend = MockHTTP2Backend(app=app)
+async def test_async_http2_multiple_requests(backend):
+    backend = MockHTTP2Backend(app=app, backend=backend)
 
     async with AsyncClient(backend=backend) as client:
         response_1 = await client.get("http://example.org/1")
@@ -125,13 +120,12 @@ def test_http2_reconnect():
     assert json.loads(response_2.content) == {"method": "GET", "path": "/2", "body": ""}
 
 
-@pytest.mark.asyncio
-async def test_async_http2_reconnect():
+async def test_async_http2_reconnect(backend):
     """
     If a connection has been dropped between requests, then we should
     be seemlessly reconnected.
     """
-    backend = MockHTTP2Backend(app=app)
+    backend = MockHTTP2Backend(app=app, backend=backend)
 
     async with AsyncClient(backend=backend) as client:
         response_1 = await client.get("http://example.org/1")
diff --git a/tests/dispatch/test_threaded.py b/tests/dispatch/test_threaded.py
--- a/tests/dispatch/test_threaded.py
+++ b/tests/dispatch/test_threaded.py
@@ -1,7 +1,5 @@
 import json
 
-import pytest
-
 from httpx import (
     AsyncClient,
     CertTypes,
@@ -53,14 +51,13 @@ def test_threaded_dispatch():
     assert response.json() == {"hello": "world"}
 
 
-@pytest.mark.asyncio
-async def test_async_threaded_dispatch():
+async def test_async_threaded_dispatch(backend):
     """
     Use a synchronous 'Dispatcher' class with the async client.
     Calls to the dispatcher will end up running within a thread pool.
     """
     url = "https://example.org/"
-    async with AsyncClient(dispatch=MockDispatch()) as client:
+    async with AsyncClient(dispatch=MockDispatch(), backend=backend) as client:
         response = await client.get(url)
 
     assert response.status_code == 200
diff --git a/tests/dispatch/utils.py b/tests/dispatch/utils.py
--- a/tests/dispatch/utils.py
+++ b/tests/dispatch/utils.py
@@ -1,4 +1,3 @@
-import asyncio
 import ssl
 import typing
 
@@ -7,11 +6,13 @@
 import h2.events
 
 from httpx import AsyncioBackend, BaseStream, Request, TimeoutConfig
+from tests.concurrency import sleep
 
 
-class MockHTTP2Backend(AsyncioBackend):
-    def __init__(self, app):
+class MockHTTP2Backend:
+    def __init__(self, app, backend=None):
         self.app = app
+        self.backend = AsyncioBackend() if backend is None else backend
         self.server = None
 
     async def connect(
@@ -21,15 +22,20 @@ async def connect(
         ssl_context: typing.Optional[ssl.SSLContext],
         timeout: TimeoutConfig,
     ) -> BaseStream:
-        self.server = MockHTTP2Server(self.app)
+        self.server = MockHTTP2Server(self.app, backend=self.backend)
         return self.server
 
+    # Defer all other attributes and methods to the underlying backend.
+    def __getattr__(self, name: str) -> typing.Any:
+        return getattr(self.backend, name)
+
 
 class MockHTTP2Server(BaseStream):
-    def __init__(self, app):
+    def __init__(self, app, backend):
         config = h2.config.H2Configuration(client_side=False)
         self.conn = h2.connection.H2Connection(config=config)
         self.app = app
+        self.backend = backend
         self.buffer = b""
         self.requests = {}
         self.close_connection = False
@@ -40,7 +46,7 @@ def get_http_version(self) -> str:
         return "HTTP/2"
 
     async def read(self, n, timeout, flag=None) -> bytes:
-        await asyncio.sleep(0)
+        await sleep(self.backend, 0)
         send, self.buffer = self.buffer[:n], self.buffer[n:]
         return send
 
diff --git a/tests/test_asgi.py b/tests/test_asgi.py
--- a/tests/test_asgi.py
+++ b/tests/test_asgi.py
@@ -46,9 +46,8 @@ def test_asgi():
     assert response.text == "Hello, World!"
 
 
-@pytest.mark.asyncio
-async def test_asgi_async():
-    client = httpx.AsyncClient(app=hello_world)
+async def test_asgi_async(backend):
+    client = httpx.AsyncClient(app=hello_world, backend=backend)
     response = await client.get("http://www.example.org/")
     assert response.status_code == 200
     assert response.text == "Hello, World!"
@@ -61,9 +60,8 @@ def test_asgi_upload():
     assert response.text == "example"
 
 
-@pytest.mark.asyncio
-async def test_asgi_upload_async():
-    client = httpx.AsyncClient(app=echo_body)
+async def test_asgi_upload_async(backend):
+    client = httpx.AsyncClient(app=echo_body, backend=backend)
     response = await client.post("http://www.example.org/", data=b"example")
     assert response.status_code == 200
     assert response.text == "example"
@@ -75,9 +73,8 @@ def test_asgi_exc():
         client.get("http://www.example.org/")
 
 
-@pytest.mark.asyncio
-async def test_asgi_exc_async():
-    client = httpx.AsyncClient(app=raise_exc)
+async def test_asgi_exc_async(backend):
+    client = httpx.AsyncClient(app=raise_exc, backend=backend)
     with pytest.raises(ValueError):
         await client.get("http://www.example.org/")
 
@@ -88,8 +85,7 @@ def test_asgi_exc_after_response():
         client.get("http://www.example.org/")
 
 
-@pytest.mark.asyncio
-async def test_asgi_exc_after_response_async():
-    client = httpx.AsyncClient(app=raise_exc_after_response)
+async def test_asgi_exc_after_response_async(backend):
+    client = httpx.AsyncClient(app=raise_exc_after_response, backend=backend)
     with pytest.raises(ValueError):
         await client.get("http://www.example.org/")
diff --git a/tests/test_timeouts.py b/tests/test_timeouts.py
--- a/tests/test_timeouts.py
+++ b/tests/test_timeouts.py
@@ -11,40 +11,36 @@
 )
 
 
-@pytest.mark.asyncio
-async def test_read_timeout(server):
-    timeout = TimeoutConfig(read_timeout=0.000001)
+async def test_read_timeout(server, backend):
+    timeout = TimeoutConfig(read_timeout=1e-6)
 
-    async with AsyncClient(timeout=timeout) as client:
+    async with AsyncClient(timeout=timeout, backend=backend) as client:
         with pytest.raises(ReadTimeout):
             await client.get("http://127.0.0.1:8000/slow_response")
 
 
-@pytest.mark.asyncio
-async def test_write_timeout(server):
-    timeout = TimeoutConfig(write_timeout=0.000001)
+async def test_write_timeout(server, backend):
+    timeout = TimeoutConfig(write_timeout=1e-6)
 
-    async with AsyncClient(timeout=timeout) as client:
+    async with AsyncClient(timeout=timeout, backend=backend) as client:
         with pytest.raises(WriteTimeout):
             data = b"*" * 1024 * 1024 * 100
             await client.put("http://127.0.0.1:8000/slow_response", data=data)
 
 
-@pytest.mark.asyncio
-async def test_connect_timeout(server):
-    timeout = TimeoutConfig(connect_timeout=0.000001)
+async def test_connect_timeout(server, backend):
+    timeout = TimeoutConfig(connect_timeout=1e-6)
 
-    async with AsyncClient(timeout=timeout) as client:
+    async with AsyncClient(timeout=timeout, backend=backend) as client:
         with pytest.raises(ConnectTimeout):
             # See https://stackoverflow.com/questions/100841/
             await client.get("http://10.255.255.1/")
 
 
-@pytest.mark.asyncio
-async def test_pool_timeout(server):
-    pool_limits = PoolLimits(hard_limit=1, pool_timeout=0.000001)
+async def test_pool_timeout(server, backend):
+    pool_limits = PoolLimits(hard_limit=1, pool_timeout=1e-6)
 
-    async with AsyncClient(pool_limits=pool_limits) as client:
+    async with AsyncClient(pool_limits=pool_limits, backend=backend) as client:
         response = await client.get("http://127.0.0.1:8000/", stream=True)
 
         with pytest.raises(PoolTimeout):