-
Notifications
You must be signed in to change notification settings - Fork 0
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
nh2.mock #1
Comments
For ntelebot, I created an I remember the concept of "entry points" (and the string "pytest11" is also very familiar), and I'm not sure why I didn't use that. (I don't appear to have documented anything 🙁.) Assuming there was no good reason (that I'm just forgetting), I'm currently thinking I should create another
Then any project that installs nh2 would get the plugin installed into itself as well (no need to import one So the general pattern might look like: def test_basic(nh2_mock):
with nh2_mock("""
GET https://example.com/test -> 200 {"a": "b"}
"""):
conn = nh2.connection.Connection('example.com', 443)
assert conn.request('GET', '/test').wait().json() == {'a': 'b'}
def test_live_request(nh2_mock):
with nh2_mock.live:
conn = nh2.connection.Connection('httpbin.org', 443)
assert conn.request('GET', … # Make an actual request to httpbin.org.
def test_low_level(nh2_mock):
with nh2_mock.lowlevel("""
example.com 443 1 <<< <RequestReceived stream_id:1, headers:[(b':method', b'GET'), (b':path', b'/test'), (b':authority', b'example.com'), (b':scheme', b'https')]>
example.com 443 1 <<< <StreamEnded stream_id:1>
example.com 443 1 >>> ??? send_headers(???)
"""):
conn = nh2.connection.Connection('example.com', 443)
assert conn.request('GET', '/test').wait().json() == {'a': 'b'} To do:
|
Stage 1Monkeypatched
async def test_blah():
conn = await nh2.connection.Connection('example.com', 443)
assert conn.mock_server.get_events() == [/"""
⋮
]/""" Maybe even: assert conn.get_events() = … ? Then just: conn.mock_server.send_headers(…) duh! (Right?)
Stage 2Make async def test_blah():
with ng2.mock.expect('example.com', 443) as mock_server:
conn = await nh2.connection.Connection('example.com', 443)
assert conn.mock_server is mock_server
assert mock_server.get_events() == [/"""
⋮
]/"""
mock_server.send_headers(…)
assert conn.get_events() = …
Stage 3For things like async def test_bot():
bot = ntelebot.bot.Bot(token)
with ng2.mock.expect('api.telegram.org', 443) as tba:
assert await bot.get_me() == …
assert tba.client.get_events() == … This suggests I won't even be able to define def get_me(self):
live_request = await self.conn.request(…)
response = await live_request.wait()
return response.json() because — for testing — I need to take control (to assert the request and provide the response) between the two Off the top of my head, I'm considering something like async def test_bot():
bot = ntelebot.bot.Bot(token)
with ng2.mock.expect('api.telegram.org', 443) as tba:
paused_coro = nh2.mock.run_until_send(bot.get_me())
assert tba.get_events() == …
tba.send_headers(…)
response = nh2.mock.run_until_send(paused_coro)
assert response == … I think this is the only way to handle testing "workflow" functions (which send multiple HTTP requests — and await their responses — before returning). |
Given a function like: async def blah(sock):
await sock.send('cat')
data = await sock.receive()
assert data == 'mew'
await sock.send('dog')
data = await sock.receive()
assert data == 'arf'
return 'zebra' I want to be able to write a test like roughly: async def test_blah():
left, right = create_pipe()
coro = blah(left)
run_until_next_send(coro)
data = await right.receive()
assert data == 'cat'
await right.send('mew')
run_until_next_send(coro)
data = await right.receive()
assert == 'dog'
await right.send('arf')
assert run_until_next_send(coro) == 'zebra' With I had been thinking class MyError(Exception):
pass
async def blah():
print('running')
raise MyError('hi')
print('finishing')
coro = blah()
try:
coro.send(None)
except MyError as e:
print('got', e)
coro.send(None)
This is basically setting a breakpoint, so I started looking into things like bdb and I could go back to what I was doing with nsync and finish writing my own coroutine runner… but the code would still be doing heavy anyio socket I/O, etc., so I think I'd have to end up actually subclassing or monkeypatching anyio's runner to actually implement this (rather than calling For the simple case of directly interacting with async def test_blah():
mock_server = await nh2.mock.expect_connect(…)
conn = await nh2.connection.Connection(…)
live_request = await conn.request(…)
assert await mock_server.read() == …
await mock_server.[respond]
response = await live_request.wait()
assert response.data == … because nothing causes However, for complicated workflows (like Honestly, the main stumbling block is just how awkward working with return values from coroutines is (at least with anyio). This seems to have been intentional, and I still don't understand why — something to do with what happens when you cancel a task, I guess. Anyway, if I recreate async def blah(sock):
await sock.send('cat')
data = await sock.receive()
assert data == 'mew'
await sock.send('dog')
data = await sock.receive()
assert data == 'arf'
return 'zebra'
async def test_blah():
with TaskGroupWrapper() as tg:
left, right = create_pipe()
future = tg.start_soon(blah, left) #coro = blah(left)
#run_until_next_send(coro) — blah automatically starts.
data = await right.receive()
assert data == 'cat'
await right.send('mew')
#run_until_next_send(coro) — blah automatically resumes.
data = await right.receive()
assert == 'dog'
await right.send('arf')
#run_until_next_send(coro) — blah automatically resumes.
assert await future == 'zebra' |
Introduce `await nh2.mock.expect_connect(host, port)` which prepares nh2 for an upcoming `await nh2.connection.Connection(host, port)`. See #1. Also introduce a new nh2.anyio_util module, which creates async-friendly pipes and adds a `create_task_group` that essentially undoes python-trio/trio#136 (comment) 🙁. A followup will probably modify nh2.mock.MockServer.read to accept something like `lowlevel=False` or `format=…` to replace the YAML-ish raw h2 event format with something like just 'GET https://example.com/dummy'. It might also still make sense for a followup to intercept the call to self.c.receive_data in nh2.connection.Connection.read, just to make the raw h2 events it receives assertable too. (I almost, almost, almost renamed nh2.mock to nh2.testing, but I couldn't stomach how that interfered with Tab completion for test_* files. I also almost renamed nh2.mock to nh2._testing and nh2._pytest_plugin._connection_mock to nh2_mock and just had it return the nh2._testing module, so tests would use nh2_mock.expect_connect (etc.), but I think that would tie things to pytest a little too tightly.)
…ake recently received client events assertable. Document the complete connect/request/await/close cycle tested in test_mock. See #1.
Assert that the expected connection has been established before the context manager ends, similar to: mock_server = await nh2.mock.expect_connect(…) conn = await nh2.connection.Connection(…) assert conn.mock_server is mock_server Note that sometimes tests only imply a connection, they don't explicitly set one themselves, as in: async def opaque_workflow(): conn = await nh2.connection.Connection('example.com', 443) async with nh2.anyio_util.create_task_group() as tg: async with nh2.mock.expect_connect('example.com', 443) as mock_server: future = tg.start_soon(opaque_workflow) so expect_connect needs to allow the async system to actually schedule and run backgrounded tasks (which for now is done with just an `anyio.sleep(.01)`). Explicitly reset nh2.mock.expect_connect's cache between runs. See #1.
Before building anything else, I want to formalize the mocking process.
I want to be able to reuse this in consumers (like ntelebot), which won't necessarily expose direct access to a
Connection
, etc.I'm thinking of pulling:
nh2/nh2/connection.py
Lines 21 to 24 in 350d703
into a separate
Connection.connect
, then having the mocking system monkeypatchConnection.connect
so it instantiates a mock server that's actually listening on a random port, then actually connecting theConnection
to it (through a real socket), and having the server pull expected (and to-be-replied) events from a global transcript.The general pattern, using
pytest.raises
as an example, might look something like:Tests would feed events into the mocking system by targeting the individual
Connection
instance:host
,port
, andConnection
instance number (to be able to test reconnection/request migration in an upcomingConnectionManager
, etc.). This could almost look like:but
h2.events.RequestReceived
's__init__
doesn't accept arguments like that (and there's no equivalent toh2.events.RemoteSettingsChanged.from_settings
). (And frankly that syntax is really clunky.)Adapting the transcript format from foyerbot/ntelebot/metabot might look something like:
but that
RemoteSettingsChanged
line is abominable, and I'm not sure how to represent the response. (It doesn't look like responses are ever actually instantiated as events inh2
, methods likeH2Connection.send_headers
instantiate, manipulate, then serializehyperframe.frame.HeadersFrame
, etc., so the only readily available syntax describing a response is the literalsend_headers
method call.)Eventually I'll want to expose just simple fully-formed requests and fully-formed responses, but testing things like:
nh2/nh2/connection.py
Lines 115 to 124 in 350d703
requires much lower-level control:
nh2/nh2/test_connection.py
Lines 37 to 87 in 350d703
The text was updated successfully, but these errors were encountered: