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:

Patch:
diff --git a/httpx/client.py b/httpx/client.py
index f7a3c5d..37e824f 100644
--- a/httpx/client.py
+++ b/httpx/client.py
@@ -633,12 +633,17 @@ class Client(BaseClient):
         # 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
diff --git a/httpx/concurrency/asyncio.py b/httpx/concurrency/asyncio.py
index 9b97783..8fc625f 100644
--- a/httpx/concurrency/asyncio.py
+++ b/httpx/concurrency/asyncio.py
@@ -18,8 +18,8 @@ from ..config import PoolLimits, TimeoutConfig
 from ..exceptions import ConnectTimeout, PoolTimeout, ReadTimeout, WriteTimeout
 from .base import (
     BaseBackgroundManager,
-    BasePoolSemaphore,
     BaseEvent,
+    BasePoolSemaphore,
     BaseQueue,
     BaseStream,
     ConcurrencyBackend,
@@ -194,6 +194,44 @@ class AsyncioBackend(ConcurrencyBackend):
             stream_reader=stream_reader, stream_writer=stream_writer, timeout=timeout
         )
 
+    async def start_tls(
+        self,
+        stream: BaseStream,
+        hostname: str,
+        ssl_context: ssl.SSLContext,
+        timeout: TimeoutConfig,
+    ) -> BaseStream:
+
+        loop = self.loop
+        if not hasattr(loop, "start_tls"):  # pragma: no cover
+            raise NotImplementedError(
+                "asyncio.AbstractEventLoop.start_tls() is only available in Python 3.7+"
+            )
+
+        assert isinstance(stream, Stream)
+
+        stream_reader = asyncio.StreamReader()
+        protocol = asyncio.StreamReaderProtocol(stream_reader)
+        transport = stream.stream_writer.transport
+
+        loop_start_tls = loop.start_tls  # type: ignore
+        transport = await asyncio.wait_for(
+            loop_start_tls(
+                transport=transport,
+                protocol=protocol,
+                sslcontext=ssl_context,
+                server_hostname=hostname,
+            ),
+            timeout=timeout.connect_timeout,
+        )
+
+        stream_reader.set_transport(transport)
+        stream.stream_reader = stream_reader
+        stream.stream_writer = asyncio.StreamWriter(
+            transport=transport, protocol=protocol, reader=stream_reader, loop=loop
+        )
+        return stream
+
     async def run_in_threadpool(
         self, func: typing.Callable, *args: typing.Any, **kwargs: typing.Any
     ) -> typing.Any:
diff --git a/httpx/concurrency/base.py b/httpx/concurrency/base.py
index 9bfd54d..bf2aed4 100644
--- a/httpx/concurrency/base.py
+++ b/httpx/concurrency/base.py
@@ -116,6 +116,15 @@ class ConcurrencyBackend:
     ) -> BaseStream:
         raise NotImplementedError()  # pragma: no cover
 
+    async def start_tls(
+        self,
+        stream: BaseStream,
+        hostname: str,
+        ssl_context: ssl.SSLContext,
+        timeout: TimeoutConfig,
+    ) -> BaseStream:
+        raise NotImplementedError()  # pragma: no cover
+
     def get_semaphore(self, limits: PoolLimits) -> BasePoolSemaphore:
         raise NotImplementedError()  # pragma: no cover
 
diff --git a/httpx/dispatch/asgi.py b/httpx/dispatch/asgi.py
index 6c1fc26..c56d757 100644
--- a/httpx/dispatch/asgi.py
+++ b/httpx/dispatch/asgi.py
@@ -1,10 +1,10 @@
 import typing
 
-from .base import AsyncDispatcher
-from ..concurrency.base import ConcurrencyBackend
 from ..concurrency.asyncio import AsyncioBackend
+from ..concurrency.base import ConcurrencyBackend
 from ..config import CertTypes, TimeoutTypes, VerifyTypes
 from ..models import AsyncRequest, AsyncResponse
+from .base import AsyncDispatcher
 
 
 class ASGIDispatch(AsyncDispatcher):
diff --git a/httpx/dispatch/connection.py b/httpx/dispatch/connection.py
index 0e9819c..87c2a64 100644
--- a/httpx/dispatch/connection.py
+++ b/httpx/dispatch/connection.py
@@ -2,7 +2,6 @@ import functools
 import ssl
 import typing
 
-from .base import AsyncDispatcher
 from ..concurrency.asyncio import AsyncioBackend
 from ..concurrency.base import ConcurrencyBackend
 from ..config import (
@@ -16,6 +15,7 @@ from ..config import (
     VerifyTypes,
 )
 from ..models import AsyncRequest, AsyncResponse, Origin
+from .base import AsyncDispatcher
 from .http2 import HTTP2Connection
 from .http11 import HTTP11Connection
 
diff --git a/httpx/dispatch/connection_pool.py b/httpx/dispatch/connection_pool.py
index 0e14bf8..eb990a9 100644
--- a/httpx/dispatch/connection_pool.py
+++ b/httpx/dispatch/connection_pool.py
@@ -1,6 +1,5 @@
 import typing
 
-from .base import AsyncDispatcher
 from ..concurrency.asyncio import AsyncioBackend
 from ..concurrency.base import ConcurrencyBackend
 from ..config import (
@@ -13,6 +12,7 @@ from ..config import (
     VerifyTypes,
 )
 from ..models import AsyncRequest, AsyncResponse, Origin
+from .base import AsyncDispatcher
 from .connection import HTTPConnection
 
 CONNECTIONS_DICT = typing.Dict[Origin, typing.List[HTTPConnection]]
diff --git a/httpx/dispatch/threaded.py b/httpx/dispatch/threaded.py
index 8176608..7454a9e 100644
--- a/httpx/dispatch/threaded.py
+++ b/httpx/dispatch/threaded.py
@@ -1,4 +1,3 @@
-from .base import AsyncDispatcher, Dispatcher
 from ..concurrency.base import ConcurrencyBackend
 from ..config import CertTypes, TimeoutTypes, VerifyTypes
 from ..models import (
@@ -11,6 +10,7 @@ from ..models import (
     Response,
     ResponseContent,
 )
+from .base import AsyncDispatcher, Dispatcher
 
 
 class ThreadedDispatcher(AsyncDispatcher):
diff --git a/httpx/dispatch/wsgi.py b/httpx/dispatch/wsgi.py
index 0cbe109..73a6fc1 100644
--- a/httpx/dispatch/wsgi.py
+++ b/httpx/dispatch/wsgi.py
@@ -1,9 +1,9 @@
 import io
 import typing
 
-from .base import Dispatcher
 from ..config import CertTypes, TimeoutTypes, VerifyTypes
 from ..models import Request, Response
+from .base import Dispatcher
 
 
 class WSGIDispatch(Dispatcher):


Test patch:
diff --git a/tests/client/test_async_client.py b/tests/client/test_async_client.py
index e4e6274..bd29743 100644
--- a/tests/client/test_async_client.py
+++ b/tests/client/test_async_client.py
@@ -3,10 +3,9 @@ import pytest
 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
index 3062733..3916b6f 100644
--- a/tests/client/test_redirects.py
+++ b/tests/client/test_redirects.py
@@ -100,36 +100,32 @@ class MockDispatch(AsyncDispatcher):
         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 test_cannot_redirect_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
index 0000000..1240034
--- /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
index f8a6d0d..2974da4 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,6 +10,14 @@ from cryptography.hazmat.primitives.serialization import (
 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
index 1bd564b..4dd8424 100644
--- 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
index 5273536..59e9bb1 100644
--- 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
index 30332dd..64bbb6a 100644
--- 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
index 8d70eae..a2bb954 100644
--- 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
index 3315135..b86552d 100644
--- 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.connection
 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 @@ class MockHTTP2Backend(AsyncioBackend):
         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 @@ class MockHTTP2Server(BaseStream):
         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
index 3ee7631..f31e871 100644
--- 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_concurrency.py b/tests/test_concurrency.py
new file mode 100644
index 0000000..870a592
--- /dev/null
+++ b/tests/test_concurrency.py
@@ -0,0 +1,31 @@
+import sys
+
+import pytest
+
+from httpx import AsyncioBackend, HTTPVersionConfig, SSLConfig, TimeoutConfig
+
+
+@pytest.mark.xfail(
+    sys.version_info < (3, 7),
+    reason="Requires Python 3.7+ for AbstractEventLoop.start_tls()",
+)
+@pytest.mark.asyncio
+async def test_start_tls_on_socket_stream(https_server):
+    """
+    See that the backend can make a connection without TLS then
+    start TLS on an existing connection.
+    """
+    backend = AsyncioBackend()
+    ctx = SSLConfig().load_ssl_context_no_verify(HTTPVersionConfig())
+    timeout = TimeoutConfig(5)
+
+    stream = await backend.connect("127.0.0.1", 8001, None, timeout)
+    assert stream.is_connection_dropped() is False
+    assert stream.stream_writer.get_extra_info("cipher", default=None) is None
+
+    stream = await backend.start_tls(stream, "127.0.0.1", ctx, timeout)
+    assert stream.is_connection_dropped() is False
+    assert stream.stream_writer.get_extra_info("cipher", default=None) is not None
+
+    await stream.write(b"GET / HTTP/1.1\r\n\r\n")
+    assert (await stream.read(8192, timeout)).startswith(b"HTTP/1.1 200 OK\r\n")
diff --git a/tests/test_timeouts.py b/tests/test_timeouts.py
index ab69e69..540fa91 100644
--- a/tests/test_timeouts.py
+++ b/tests/test_timeouts.py
@@ -11,40 +11,36 @@ from httpx import (
 )
 
 
-@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):