Does the SanicTestClient support response streaming?


#1

I am writing test cases around an endpoint which streams JSON as its response and I haven’t been able to figure out how to set up the test cases to get the streamed response successfully. Does the SanicTestClient support response streaming? From my cursory look through its implementation, it doesn’t appear to, but I am not well-versed with aiohttp, so I could easily be overlooking something. I’m using sanic==19.3.1.

Some context
The endpoint being tested:

@core.route('/cache')
async def read_cache(request):
    """Stream cached data for device readings.

    Query Parameters:
        start: An RFC3339 formatted timestamp which specifies a starting bound on the
            cache data to return. If left unspecified, there will be no starting bound.
        end: An RFC3339 formatted timestamp which specifies an ending bound on the
            cache data to return. If left unspecified, there will be no ending bound.

    HTTP Codes:
        * 200: OK
        * 400: Invalid query parameter(s)
        * 500: Catchall processing error
    """
    log_request(request, params=request.args)

    # Parse the start param. Explicitly check that multiple starts are not defined.
    start = ''
    param_start = request.args.getlist('start')
    if param_start:
        if len(param_start) > 1:
            raise errors.InvalidUsage(
                'invalid parameter: only one cache start may be specified',
            )
        start = param_start[0]

    # Parse the end param. Explicitly check that multiple ends are not defined.
    end = ''
    param_end = request.args.getlist('end')
    if param_end:
        if len(param_end) > 1:
            raise errors.InvalidUsage(
                'invalid parameter: only one cache end may be specified',
            )
        end = param_end[0]

    # Define the function that will be used to stream the responses back.
    async def response_streamer(response):
        async for reading in cmd.read_cache(start, end):
            await response.write(reading)

    return stream(response_streamer, content_type='application/json')

My test case:

def test_ok(self, test_app):
    with asynctest.patch('server.cmd.read_cache') as mock_cmd:
        mock_cmd.side_effect = [
            {
                'value': 1,
                'type': 'temperature',
            },
            {
                'value': 2,
                'type': 'temperature',
            },
            {
                'value': 3,
                'type': 'temperature',
            },
        ]

        # also tried this with the chunked=True request param
        resp = test_app.test_client.get('/v3/readcache', gather_request=False)
        assert resp.status == 200
        assert resp.headers['Transfer-Encoding'] == 'chunked'
        assert resp.headers['Content-Type'] == 'application/json'

        body = ujson.loads(resp.body)
        assert body == mock_cmd.return_value

    mock_cmd.assert_called_once()
    mock_cmd.assert_called_with('', '')

The resulting test error:

        self.app.run(debug=debug, **server_kwargs)
        self.app.listeners["after_server_start"].pop()
    
        if exceptions:
>           raise ValueError("Exception during request: {}".format(exceptions))
E           ValueError: Exception during request: [ClientResponseError("400, message='invalid character in chunk size header'",)]

.tox/py36/lib/python3.6/site-packages/sanic/testing.py:109: ValueError

#2

@edaniszewski Yes, the SanicTestClient will allow you to stream content.

Take a look at tests/test_request_stream.py for some examples.


#3

@ahopkins Thanks! Looks like I was using it correctly for the most part. This issue turned out to be related to asynctest and how it’s side_effect implicit behavior for iterators doesn’t really work for an async generator being called in an async for loop.

Thanks for reading through my question and for the example reference!


#4

:+1: happy to help with this or any other questions