requests and httpx, Now That You Know · practice
Test a Client Without a Server
Every check of who_am_i and trail_httpx so far needed a server: pl_http started yours, and the libraries talked to it. Tests should not. In Networks with Python you tested connect
A transport is the seam
An httpx client does its httpx.MockTransport is a transport that calls a function of yours instead:
import httpx
def answer(request):
if request.url.path == "/v0/books/2":
return httpx.Response(308, headers={"location": "/books/2"})
return httpx.Response(200, json={"id": 2, "title": "Café Society", "author": "Ana Ruiz"})
client = httpx.Client(transport=httpx.MockTransport(answer))
The function, often called a handler, receives an httpx.Request and returns an httpx.Response. The request has everything the client would have sent: request.method, request.url.path, request.headers (with host already in it), and request.content, the body as bytes. httpx.Response(200, json=...) encodes the content-type: application/json for you.
Everything above the transport runs as usual. When answer returns the 308 and the call passed follow_redirects=True, the client resolves the location, builds the next request, and calls answer again with path /books/2. No socket is opened at any point; the handler is the whole server.
Passing a function to another function is something you have done since Networks with Python’s injected connect. Here the handler is the MockTransport calls it.
What does a MockTransport handler receive and return?
The handler can also see the timeout. httpx attaches it to every request as request.extensions["timeout"], a HTTPX_TIMEOUT, httpx.Timeout(2.0, connect=1.0), it is {'connect': 1.0, 'read': 2.0, 'write': 2.0, 'pool': 2.0}; with no timeout given, every phase is httpx’s 5.0. A handler that appends each request, or the parts it cares about, to a
How can a test see the timeout an httpx call used?
The session stand-in you know
requests has transports too, but who_am_i takes a whole session, so the stand-in can be a whole session. Open fakes.py. fake_session(replies, calls) returns an request, get, post, put, and delete, the who_am_i calls. Each call appends a dict to your calls list, with every keyword requests would have received: method, url, params, headers, json, auth, timeout, allow_redirects. Then it answers with the next item of replies, or raises it if that item is an
fake_response(status, data) builds the replies. It has status_code, json(), case-insensitive headers, and a raise_for_status() that raises requests.HTTPError for 4xx and 5xx, so who_am_i runs against it unchanged:
calls = []
replies = [fakes.fake_response(200, {"user": "ana"}), fakes.fake_response(200, {"user": "ana"})]
session = fakes.fake_session(replies, calls)
After who_am_i runs, calls[0] is the login and calls[1] is /me. A test can now assert what no server would show you directly: the credentials passed in auth, and the timeout of each call.
Test through the module
Your tests are checked too. Check my work runs them against your own side_by_side.py, then again with one function replaced by a version that has a mistake, to see whether a test notices. It puts each version in place by setting it on the side_by_side.trail_httpx = .... A test that calls side_by_side.trail_httpx(...) looks the name up at the moment it runs, so it meets whichever version is there. A from side_by_side import trail_httpx line copies the original into your test file’s own name, and the swap never reaches it.
The versions with mistakes are the ones this lesson’s two tests are named for: a trail_httpx that does not follow the redirect, and a who_am_i whose GET /me passes no timeout. A test that checks only the login call’s timeout passes for the second one, so check both calls.
Why call side_by_side.trail_httpx(...) instead of importing the name with from side_by_side import trail_httpx?
Your task
Both functions take their stand-in after the * in their definitions, so a test passes it by name, as client=client or session=session:
def trail_httpx(url, *, client=None, timeout=HTTPX_TIMEOUT):
...
def who_am_i(base_url, user, password, *, session, timeout=TIMEOUT):
...
In tests/test_side_by_side.py, replace the two placeholders that say assert False, "not written yet":
test_trail_httpx_follows_a_redirect: write a handler that answers 308 with alocationfor one path and 200 for the next, make a client withhttpx.MockTransport(handler), callside_by_side.trail_httpx(url, client=client), and assert that the trail is both lines, the 308 first.test_who_am_i_sends_credentials_and_timeouts: giveside_by_side.who_am_iafakes.fake_sessionwith two replies and a timeout of your choosing, then assert the, the authof the login call, and thetimeoutof both calls.
Names and extra tests are up to you. Every test must pass its own stand-in to the function; a test that leaves client or session out would need a network, and fails here. Run only this file, since tests/test_client.py waits for Lesson 6:
python -m pytest -q tests/test_side_by_side.py
It should report 2 passed, or more if you wrote more. Then press Check my work. Lesson 5 looks at the lines both libraries send that nobody asked for.
Task
Test trail_httpx and who_am_i without a server.
In tests/test_side_by_side.py, replace the two placeholders:
test_trail_httpx_follows_a_redirect: a handleranswers 308 with a locationfor one path and 200 for the path it points to. Make a client withhttpx.Client(transport=httpx.MockTransport(handler)), callside_by_side.trail_httpx(url, client=client), and assert that the trail holds both lines, the 308 first.test_who_am_i_sends_credentials_and_timeouts: callside_by_side.who_am_i(...)withfakes.fake_session(replies, calls)and a timeout of your choosing. Assert what it returns, theauthof the login call, and thetimeoutof both recorded calls, theGET /meas well as thePOST /login.
Keep import side_by_side and call the functions through the from side_by_side import line and store neither function in a name at the top of the file. Pass a client or a session to every call. Do not import socket, http, or urllib modules. More tests are welcome.
In the python -m pytest -q tests/test_side_by_side.py runs these tests; with both written it reports 2 passed or more.
Then press Check my work. It reads the file, then runs it three times with pytest: against your own side_by_side.py, against a trail_httpx that does not follow redirects, and against a who_am_i whose GET /me has no timeout. Your tests must all pass against the first, and at least one must fail against each of the others. While they run, side_by_side’s requests and httpx refuse to send anything, so a test that forgets its stand-in fails. A run that takes longer than 3 seconds is stopped and reported. Nothing from your terminal is graded.