From 5cd7736aed3dd6d4bd07493ee8af6dc87fa6b9e0 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 24 Nov 2016 14:58:42 +0200 Subject: [PATCH 1/6] Work on docs --- aiohttp/test_utils.py | 5 +- aiohttp/web_server.py | 26 ++++++++- docs/spelling_wordlist.txt | 1 + docs/testing.rst | 109 +++++++++++++++++++++++++++++-------- tests/test_web_server.py | 45 +++++++++++++++ 5 files changed, 160 insertions(+), 26 deletions(-) diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py index 0fbe24cbd13..61c3df55d8c 100644 --- a/aiohttp/test_utils.py +++ b/aiohttp/test_utils.py @@ -74,7 +74,7 @@ def start_server(self, **kwargs): @abstractmethod # pragma: no cover @asyncio.coroutine - def _make_factory(self): + def _make_factory(self, **kwargs): pass def make_url(self, path): @@ -164,7 +164,8 @@ def __init__(self, handler, @asyncio.coroutine def _make_factory(self, **kwargs): - return WebServer(self._handler, loop=self._loop) + self.handler = WebServer(self._handler, loop=self._loop, **kwargs) + return self.handler @asyncio.coroutine def _close_hook(self): diff --git a/aiohttp/web_server.py b/aiohttp/web_server.py index 67fe1dad51e..9ae8caac684 100644 --- a/aiohttp/web_server.py +++ b/aiohttp/web_server.py @@ -1,10 +1,14 @@ """Low level HTTP server.""" import asyncio +import traceback + +from html import escape as html_escape + from .helpers import TimeService from .server import ServerHttpProtocol -from .web_exceptions import HTTPException +from .web_exceptions import HTTPException, HTTPInternalServerError from .web_reqrep import BaseRequest __all__ = ('RequestHandler', 'WebServer') @@ -59,6 +63,26 @@ def handle_request(self, message, payload): resp = yield from self._handler(request) except HTTPException as exc: resp = exc + except Exception as exc: + msg = "

500 Internal Server Error

" + if self.debug: + try: + tb = traceback.format_exc() + tb = html_escape(tb) + msg += '

Traceback:

\n
'
+                        msg += tb
+                        msg += '
' + except: + pass + else: + msg += "Server got itself in trouble" + msg = ("500 Internal Server Error" + "" + msg + "") + resp = HTTPInternalServerError(text=msg, + content_type='text/html') + self.logger.exception( + "Error handling request", + exc_info=exc) resp_msg = yield from resp.prepare(request) yield from resp.write_eof() diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 91f43b14762..2fcc04f160f 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -77,6 +77,7 @@ hostnames HTTPException HttpProcessingError https +incapsulates impl Indices infos diff --git a/docs/testing.rst b/docs/testing.rst index 92a5d75d759..1718f8f16c2 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -153,17 +153,26 @@ basis, the TestClient object can be used directly:: A full list of the utilities provided can be found at the :data:`api reference ` -The Test Client -~~~~~~~~~~~~~~~ +The Test Client and Servers +~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The :class:`aiohttp.test_utils.TestClient` creates an asyncio server -for the web.Application object, as well as a -:class:`aiohttp.ClientSession` to perform requests. In addition, -*TestClient* provides proxy methods to the client for common -operations such as *ws_connect*, *get*, *post*, etc. +*aiohttp* test utils provides a scaffolding for testing aiohttp-based +web servers. + +They are consist of two parts: running test server and making HTTP +requests to this server. + +:class:`~aiohttp.test_utils.TestServer` runs :class:`aiohttp.web.Application` +based server, :class:`~aiohttp.test_utils.RawTestServer` starts +:class:`aiohttp.web.WebServer` low level server. + +For performing HTTP requests to these servers you have to create a +test client: :class:`aiohttp.test_utils.TestClient` instance. + +The client incapsulates :class:`aiohttp.ClientSession` by providing +proxy methods to the client for common operations such as +*ws_connect*, *get*, *post*, etc. -Please see the full api at the -:class:`TestClass api reference ` .. _aiohttp-testing-unittest-example: @@ -327,11 +336,9 @@ Test server usually works in conjunction with :class:`aiohttp.test_utils.TestClient` which provides handy client methods for accessing to the server. -.. class:: TestServer(app, *, scheme="http", host='127.0.0.1') +.. class:: BaseTestServer(*, scheme='http', host='127.0.0.1') - Test server (not started yet after constructor call). - - :param app: :class:`aiohttp.web.Application` instance to run. + Base class for test servers. :param str scheme: HTTP scheme, non-protected ``"http"`` by default. @@ -339,10 +346,6 @@ for accessing to the server. (``'127.0.0.1'``) by default. - .. attribute:: app - - :class:`aiohttp.web.Application` instance to run. - .. attribute:: scheme A *scheme* for tested application, ``'http'`` for non-protected @@ -358,12 +361,11 @@ for accessing to the server. .. attribute:: handler - :class:`aiohttp.web.RequestHandlerFactory` returned by - ``self.app.make_handler()``. + :class:`aiohttp.web.WebServer` used for HTTP requests serving. .. attribute:: server - :class:`asyncio.AbstractServer` used for running :attr:`app`. + :class:`asyncio.AbstractServer` used for managing accepted connections. .. comethod:: start_server(**kwargs) @@ -375,14 +377,75 @@ for accessing to the server. .. method:: make_url(path) - Return :class:`~yarl.URL` for given *path*. + Return an *absolute* :class:`~yarl.URL` for given *path*. + + +.. class:: RawTestServer(handler, *, \ + loop=None, scheme="http", host='127.0.0.1') + + Low-level test server (derived from :class:`BaseTestServer`). + + :param handler: a coroutine for handling web requests. The + handler should accept + :class:`aiohttp.web.BaseRequest` and return a + response instance, + e.g. :class:`~aiohttp.web.StreamResponse` or + :class:`~aiohttp.web.Response`. + + The handler could raise + :class:`~aiohttp.web.HTTPException` as a signal for + non-200 HTTP response. + + :param str scheme: HTTP scheme, non-protected ``"http"`` by default. + + :param str host: a host for TCP socket, IPv4 *local host* + (``'127.0.0.1'``) by default. + + +.. class:: TestServer(app, *, scheme="http", host='127.0.0.1') + + Test server (derived from :class:`BaseTestServer`) for starting + :class:`~aiohttp.web.Application`. + + :param app: :class:`aiohttp.web.Application` instance to run. + + :param str scheme: HTTP scheme, non-protected ``"http"`` by default. + + :param str host: a host for TCP socket, IPv4 *local host* + (``'127.0.0.1'``) by default. + + + .. attribute:: app + + :class:`aiohttp.web.Application` instance to run. Test Client ~~~~~~~~~~~ -.. class:: TestClient +.. class:: TestClient(app_or_server, *, \ + scheme='http', host='127.0.0.1', \ + cookie_jar=None, **kwargs) + + A test client used for making calls to tested server. + + :param app_or_server: :class:`BaseTestServer` instance for making + client requests to it. + If the parameter is + :class:`aiohttp.web.Application` the tool + creates :class:`TestServer` implicitly for + serving the application. + + :param cookie_jar: an optional :class:`aiohttp.CookieJar` instance, + may be useful with ``CookieJar(unsafe=True)`` + option. + +:class:`aiohttp.web.Application` instance to run. + + +Mocked coroutine +~~~~~~~~~~~~~~~~ .. function:: make_mocked_coro(return_value) @@ -410,7 +473,7 @@ Test Client .. automodule:: aiohttp.test_utils - :members: TestClient, AioHTTPTestCase, unittest_run_loop, + :members: AioHTTPTestCase, unittest_run_loop, loop_context, setup_test_loop, teardown_test_loop, make_mocked_request :undoc-members: diff --git a/tests/test_web_server.py b/tests/test_web_server.py index 818d84402f2..d41218ae2b9 100644 --- a/tests/test_web_server.py +++ b/tests/test_web_server.py @@ -1,4 +1,5 @@ import asyncio +from unittest import mock from aiohttp import web @@ -15,3 +16,47 @@ def handler(request): assert resp.status == 200 txt = yield from resp.text() assert txt == '/path/to' + + +@asyncio.coroutine +def test_raw_server_not_http_exception(raw_test_server, test_client): + exc = RuntimeError("custom runtime error") + + @asyncio.coroutine + def handler(request): + raise exc + + logger = mock.Mock() + server = yield from raw_test_server(handler, logger=logger) + client = yield from test_client(server) + resp = yield from client.get('/path/to') + assert resp.status == 500 + + txt = yield from resp.text() + assert "

500 Internal Server Error

" in txt + + logger.exception.assert_called_with( + "Error handling request", + exc_info=exc) + + +@asyncio.coroutine +def test_raw_server_not_http_exception_debug(raw_test_server, test_client): + exc = RuntimeError("custom runtime error") + + @asyncio.coroutine + def handler(request): + raise exc + + logger = mock.Mock() + server = yield from raw_test_server(handler, logger=logger, debug=True) + client = yield from test_client(server) + resp = yield from client.get('/path/to') + assert resp.status == 500 + + txt = yield from resp.text() + assert "

Traceback:

" in txt + + logger.exception.assert_called_with( + "Error handling request", + exc_info=exc) From d0fed022d8c9bf39b0eaa4475a4519fecd3fd912 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 24 Nov 2016 15:07:28 +0200 Subject: [PATCH 2/6] Fix test --- tests/test_web_functional.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index 96729acab2a..c62695b5358 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -52,7 +52,7 @@ def handler(request): resp = yield from client.get('/') assert 500 == resp.status - logger.exception.assert_called_with("Error handling request") + assert logger.exception.called @asyncio.coroutine From 00b594d20336c53d0e16562de81531f16a94de29 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 24 Nov 2016 15:43:03 +0200 Subject: [PATCH 3/6] Drop extra test client props --- CHANGES.rst | 8 +++++- aiohttp/pytest_plugin.py | 6 ++-- aiohttp/test_utils.py | 10 +------ aiohttp/web_server.py | 4 +-- docs/testing.rst | 40 ++++++++++++++++++++++++++- tests/test_pytest_plugin.py | 4 +-- tests/test_test_utils.py | 2 -- tests/test_web_functional.py | 8 +++--- tests/test_web_sendfile_functional.py | 1 + 9 files changed, 57 insertions(+), 26 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 605f556234d..31367eeb50f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,8 +14,14 @@ CHANGES - Support task attribute for StreamResponse #1410 -- +- Drop `TestClient.app` property, use `TestClient.server.app` instead + (BACKWARD INCOMPATIBLE) + +- Drop `TestClient.handler` property, use `TestClient.server.handler` instead + (BACKWARD INCOMPATIBLE) +- `TestClient.server` property returns a test server instance, was + `asyncio.AbstractServer` (BACKWARD INCOMPATIBLE) - - diff --git a/aiohttp/pytest_plugin.py b/aiohttp/pytest_plugin.py index 6a2d81f51fb..d0dbbdfe8d2 100644 --- a/aiohttp/pytest_plugin.py +++ b/aiohttp/pytest_plugin.py @@ -6,10 +6,8 @@ from aiohttp.web import Application from .test_utils import unused_port as _unused_port -from .test_utils import (RawTestServer, TestClient, - TestServer, - loop_context, setup_test_loop, - teardown_test_loop) +from .test_utils import (RawTestServer, TestClient, TestServer, loop_context, + setup_test_loop, teardown_test_loop) @contextlib.contextmanager diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py index 61c3df55d8c..60768359f3a 100644 --- a/aiohttp/test_utils.py +++ b/aiohttp/test_utils.py @@ -218,10 +218,6 @@ def __init__(self, app_or_server, *, scheme=sentinel, host=sentinel, def start_server(self): yield from self._server.start_server() - @property - def app(self): - return self._server.app - @property def host(self): return self._server.host @@ -230,13 +226,9 @@ def host(self): def port(self): return self._server.port - @property - def handler(self): - return self._server.handler - @property def server(self): - return self._server.server + return self._server @property def session(self): diff --git a/aiohttp/web_server.py b/aiohttp/web_server.py index 9ae8caac684..76d7e3677ee 100644 --- a/aiohttp/web_server.py +++ b/aiohttp/web_server.py @@ -2,10 +2,8 @@ import asyncio import traceback - from html import escape as html_escape - from .helpers import TimeService from .server import ServerHttpProtocol from .web_exceptions import HTTPException, HTTPInternalServerError @@ -72,7 +70,7 @@ def handle_request(self, message, payload): msg += '

Traceback:

\n
'
                         msg += tb
                         msg += '
' - except: + except: # pragma: no cover pass else: msg += "Server got itself in trouble" diff --git a/docs/testing.rst b/docs/testing.rst index 1718f8f16c2..c02da818987 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -441,7 +441,45 @@ Test Client may be useful with ``CookieJar(unsafe=True)`` option. -:class:`aiohttp.web.Application` instance to run. + :param str scheme: HTTP scheme, non-protected ``"http"`` by default. + + :param str host: a host for TCP socket, IPv4 *local host* + (``'127.0.0.1'``) by default. + + .. attribute:: scheme + + A *scheme* for tested application, ``'http'`` for non-protected + run and ``'htttps'`` for TLS encrypted server. + + .. attribute:: host + + *host* used to start a test server. + + .. attribute:: port + + A random *port* used to start a server. + + .. attribute:: handler + + :class:`aiohttp.web.WebServer` used for HTTP requests serving. + + .. attribute:: server + + :class:`asyncio.AbstractServer` used for managing accepted connections. + + .. comethod:: start_server(**kwargs) + + Start a test server. + + .. comethod:: close() + + Stop and finish executed test server. + + .. method:: make_url(path) + + Return an *absolute* :class:`~yarl.URL` for given *path*. + + Mocked coroutine diff --git a/tests/test_pytest_plugin.py b/tests/test_pytest_plugin.py index 860c916bffa..c2738d1a21c 100644 --- a/tests/test_pytest_plugin.py +++ b/tests/test_pytest_plugin.py @@ -114,7 +114,7 @@ def test_set_value(cli): assert resp.status == 200 text = yield from resp.text() assert text == 'thanks for the data' - assert cli.app['value'] == 'foo' + assert cli.server.app['value'] == 'foo' @asyncio.coroutine @@ -123,7 +123,7 @@ def test_get_value(cli): assert resp.status == 200 text = yield from resp.text() assert text == 'value: unknown' - cli.app['value'] = 'bar' + cli.server.app['value'] = 'bar' resp = yield from cli.get('/') assert resp.status == 200 text = yield from resp.text() diff --git a/tests/test_test_utils.py b/tests/test_test_utils.py index 087930900e1..c9eca16fa3f 100644 --- a/tests/test_test_utils.py +++ b/tests/test_test_utils.py @@ -226,12 +226,10 @@ def test_make_mocked_request_transport(): def test_test_client_props(loop): app = _create_example_app(loop) client = _TestClient(app, host='localhost') - assert client.app == app assert client.host == 'localhost' assert client.port is None with client: assert isinstance(client.port, int) - assert client.handler is not None assert client.server is not None assert client.port is None diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index c62695b5358..7101e2139f8 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -825,19 +825,19 @@ def handler(request): app = web.Application(loop=loop) app.router.add_get('/', handler) client = yield from test_client(app) - assert client.handler.requests_count == 0 + assert client.server.handler.requests_count == 0 resp = yield from client.get('/') assert 200 == resp.status - assert client.handler.requests_count == 1 + assert client.server.handler.requests_count == 1 resp = yield from client.get('/') assert 200 == resp.status - assert client.handler.requests_count == 2 + assert client.server.handler.requests_count == 2 resp = yield from client.get('/') assert 200 == resp.status - assert client.handler.requests_count == 3 + assert client.server.handler.requests_count == 3 @asyncio.coroutine diff --git a/tests/test_web_sendfile_functional.py b/tests/test_web_sendfile_functional.py index 50f1e5ab7ea..09917e1cc44 100644 --- a/tests/test_web_sendfile_functional.py +++ b/tests/test_web_sendfile_functional.py @@ -1,6 +1,7 @@ import asyncio import os import pathlib + import pytest import aiohttp From 3065c7b9ea2aea931c5b63088e986adaebf4f0c5 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 24 Nov 2016 15:52:21 +0200 Subject: [PATCH 4/6] Document test client --- aiohttp/test_utils.py | 8 +++---- docs/testing.rst | 52 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py index 60768359f3a..8597b38de62 100644 --- a/aiohttp/test_utils.py +++ b/aiohttp/test_utils.py @@ -232,7 +232,7 @@ def server(self): @property def session(self): - """A raw handler to the aiohttp.ClientSession. + """An internal aiohttp.ClientSession. Unlike the methods on the TestClient, client session requests do not automatically include the host in the url queried, and @@ -246,11 +246,11 @@ def make_url(self, path): @asyncio.coroutine def request(self, method, path, *args, **kwargs): - """Routes a request to the http server. + """Routes a request to tested http server. The interface is identical to asyncio.ClientSession.request, except the loop kwarg is overridden by the instance used by the - application. + test server. """ resp = yield from self._session.request( @@ -306,7 +306,7 @@ def delete(self, path, *args, **kwargs): def ws_connect(self, path, *args, **kwargs): """Initiate websocket connection. - The api is identical to aiohttp.ClientSession.ws_connect. + The api corresponds to aiohttp.ClientSession.ws_connect. """ ws = yield from self._session.ws_connect( diff --git a/docs/testing.rst b/docs/testing.rst index c02da818987..02823bae4c1 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -459,13 +459,18 @@ Test Client A random *port* used to start a server. - .. attribute:: handler + .. attribute:: server - :class:`aiohttp.web.WebServer` used for HTTP requests serving. + :class:`BaseTestServer` test server instance used in conjunction + with client. - .. attribute:: server + .. attribute:: session - :class:`asyncio.AbstractServer` used for managing accepted connections. + An internal :class:`aiohttp.ClientSession`. + + Unlike the methods on the :class:`TestClient`, client session + requests do not automatically include the host in the url + queried, and will require an absolute path to the resource. .. comethod:: start_server(**kwargs) @@ -479,8 +484,47 @@ Test Client Return an *absolute* :class:`~yarl.URL` for given *path*. + .. comethod:: request(method, path, *args, **kwargs) + + Routes a request to tested http server. + + The interface is identical to + :meth:`asyncio.ClientSession.request`, except the loop kwarg is + overridden by the instance used by the test server. + + .. comethod:: get(path, *args, **kwargs) + + Perform an HTTP GET request. + + .. comethod:: post(path, *args, **kwargs) + + Perform an HTTP POST request. + + .. comethod:: options(path, *args, **kwargs) + + Perform an HTTP OPTIONS request. + + .. comethod:: head(path, *args, **kwargs) + + Perform an HTTP HEAD request. + + .. comethod:: put(path, *args, **kwargs) + + Perform an HTTP PUT request. + + .. comethod:: patch(path, *args, **kwargs) + + Perform an HTTP PATCH request. + + .. comethod:: delete(path, *args, **kwargs) + + Perform an HTTP DELETE request. + + .. comethod:: delete(path, *args, **kwargs) + Initiate websocket connection. + The api corresponds to :meth:`aiohttp.ClientSession.ws_connect`. Mocked coroutine ~~~~~~~~~~~~~~~~ From 8688ec4e62e3d84db792f1267abb537ded50f934 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 24 Nov 2016 20:05:23 +0200 Subject: [PATCH 5/6] Add missing test functions --- aiohttp/test_utils.py | 39 ----------- docs/testing.rst | 154 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 146 insertions(+), 47 deletions(-) diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py index 8597b38de62..11c27f40950 100644 --- a/aiohttp/test_utils.py +++ b/aiohttp/test_utils.py @@ -480,45 +480,6 @@ def make_mocked_request(method, path, headers=None, *, Useful in unit tests, when spinning full web server is overkill or specific conditions and errors are hard to trigger. - :param method: str, that represents HTTP method, like; GET, POST. - :type method: str - - :param path: str, The URL including *PATH INFO* without the host or scheme - :type path: str - - :param headers: mapping containing the headers. Can be anything accepted - by the multidict.CIMultiDict constructor. - :type headers: dict, multidict.CIMultiDict, list of pairs - - :param version: namedtuple with encoded HTTP version - :type version: aiohttp.protocol.HttpVersion - - :param closing: flag indicates that connection should be closed after - response. - :type closing: bool - - :param app: the aiohttp.web application attached for fake request - :type app: aiohttp.web.Application - - :param reader: object for storing and managing incoming data - :type reader: aiohttp.parsers.StreamParser - - :param writer: object for managing outcoming data - :type wirter: aiohttp.parsers.StreamWriter - - :param transport: asyncio transport instance - :type transport: asyncio.transports.Transport - - :param payload: raw payload reader object - :type payload: aiohttp.streams.FlowControlStreamReader - - :param sslcontext: ssl.SSLContext object, for HTTPS connection - :type sslcontext: ssl.SSLContext - - :param secure_proxy_ssl_header: A tuple representing a HTTP header/value - combination that signifies a request is secure. - :type secure_proxy_ssl_header: tuple - """ if version < HttpVersion(1, 1): diff --git a/docs/testing.rst b/docs/testing.rst index 02823bae4c1..fd08fd96692 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -526,8 +526,76 @@ Test Client The api corresponds to :meth:`aiohttp.ClientSession.ws_connect`. -Mocked coroutine -~~~~~~~~~~~~~~~~ +Unittest's TestCase +~~~~~~~~~~~~~~~~~~~~ + +.. class:: AioHTTPTestCase + + A base class to allow for unittest web applications using aiohttp. + + Derived from :class:`unittest.TestCase` + + Provides the following: + + .. attribute:: client + + an aiohttp test client, :class:`TestClient` instance. + + .. attribute:: loop + + The event loop in which the application and server are running. + + .. attribute:: app + + The application returned by :meth:`get_app` + (:class:`aiohttp.web.Application` instance). + + .. method:: get_app(loop) + + This method should be overridden + to return the :class:`aiohttp.web.Application` + object to test. + + :param loop: the event_loop to use + :type loop: asyncio.AbstractEventLoop + + :return: :class:`aiohttp.web.Application` instance. + + .. method:: setUp() + + Standard test initialization method. + + .. method:: tearDown() + + Standard test finalization method. + + + .. note:: + + The ``TestClient``'s methods are asynchronous: you have to + execute function on the test client using asynchronous methods. + + A basic test class wraps every test method by + :func:`unittest_run_loop` decorator:: + + class TestA(AioHTTPTestCase): + + @unittest_run_loop + async def test_f(self): + resp = await self.client.get('/') + + +.. decorator:: unittest_run_loop: + + A decorator dedicated to use with asynchronous methods of an + :class:`AioHTTPTestCase`. + + Handles executing an asynchronous function, using + the :attr:`AioHTTPTestCase.loop` of the :class:`AioHTTPTestCase`. + + +Utilities +~~~~~~~~~ .. function:: make_mocked_coro(return_value) @@ -548,19 +616,89 @@ Mocked coroutine *return_value* when called. +.. function:: make_mocked_request(method, path, headers=None, *, \ + version=HttpVersion(1, 1), \ + closing=False, \ + app=None, \ + reader=sentinel, \ + writer=sentinel, \ + transport=sentinel, \ + payload=sentinel, \ + sslcontext=None, \ + secure_proxy_ssl_header=None) + + Creates mocked web.Request testing purposes. + + Useful in unit tests, when spinning full web server is overkill or + specific conditions and errors are hard to trigger. + + :param method: str, that represents HTTP method, like; GET, POST. + :type method: str + + :param path: str, The URL including *PATH INFO* without the host or scheme + :type path: str + + :param headers: mapping containing the headers. Can be anything accepted + by the multidict.CIMultiDict constructor. + :type headers: dict, multidict.CIMultiDict, list of pairs + + :param version: namedtuple with encoded HTTP version + :type version: aiohttp.protocol.HttpVersion + + :param closing: flag indicates that connection should be closed after + response. + :type closing: bool + + :param app: the aiohttp.web application attached for fake request + :type app: aiohttp.web.Application + + :param reader: object for storing and managing incoming data + :type reader: aiohttp.parsers.StreamParser + + :param writer: object for managing outcoming data + :type wirter: aiohttp.parsers.StreamWriter + + :param transport: asyncio transport instance + :type transport: asyncio.transports.Transport + + :param payload: raw payload reader object + :type payload: aiohttp.streams.FlowControlStreamReader + + :param sslcontext: ssl.SSLContext object, for HTTPS connection + :type sslcontext: ssl.SSLContext + + :param secure_proxy_ssl_header: A tuple representing a HTTP header/value + combination that signifies a request is secure. + :type secure_proxy_ssl_header: tuple + + :return: :class:`aiohttp.web.Request` object. + + .. function:: unused_port() Return an unused port number for IPv4 TCP protocol. + :return int: ephemeral port number which could be reused by test server. + +.. function:: loop_context(loop_factory=) + + A contextmanager that creates an event_loop, for test purposes. + + Handles the creation and cleanup of a test loop. + +.. function:: setup_test_loop(loop_factory=) + + Create and return an :class:`asyncio.AbstractEventLoop` instance. + + The caller should also call teardown_test_loop, once they are done + with the loop. +.. function:: teardown_test_loop(loop) -.. automodule:: aiohttp.test_utils - :members: AioHTTPTestCase, unittest_run_loop, - loop_context, setup_test_loop, teardown_test_loop, - make_mocked_request - :undoc-members: - :show-inheritance: + Teardown and cleanup an event_loop created by setup_test_loop. + :param loop: the loop to teardown + :type loop: asyncio.AbstractEventLoop From a3e2c555364a20ca0661fbb8783b867b4b983a60 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 24 Nov 2016 20:13:13 +0200 Subject: [PATCH 6/6] Reorder chapters --- docs/testing.rst | 354 +++++++++++++++++++++++------------------------ 1 file changed, 177 insertions(+), 177 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index fd08fd96692..db1e9aa3e76 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -103,56 +103,6 @@ app test client:: assert await resp.text() == 'value: bar' -.. _aiohttp-testing-framework-agnostic-utilities: - -Framework Agnostic Utilities -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -High level test creation:: - - from aiohttp.test_utils import TestClient, loop_context - from aiohttp import request - - # loop_context is provided as a utility. You can use any - # asyncio.BaseEventLoop class in it's place. - with loop_context() as loop: - app = _create_example_app(loop) - with TestClient(app) as client: - - async def test_get_route(): - nonlocal client - resp = await client.get("/") - assert resp.status == 200 - text = await resp.text() - assert "Hello, world" in text - - loop.run_until_complete(test_get_route()) - - -If it's preferred to handle the creation / teardown on a more granular -basis, the TestClient object can be used directly:: - - from aiohttp.test_utils import TestClient - - with loop_context() as loop: - app = _create_example_app(loop) - client = TestClient(app) - loop.run_until_complete(client.start_server()) - root = "http://127.0.0.1:{}".format(port) - - async def test_get_route(): - resp = await client.get("/") - assert resp.status == 200 - text = await resp.text() - assert "Hello, world" in text - - loop.run_until_complete(test_get_route()) - loop.run_until_complete(client.close()) - - -A full list of the utilities provided can be found at the -:data:`api reference ` - The Test Client and Servers ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -177,8 +127,10 @@ proxy methods to the client for common operations such as .. _aiohttp-testing-unittest-example: -Unittest Example -~~~~~~~~~~~~~~~~ +.. _aiohttp-testing-unittest-style: + +Unittest style +~~~~~~~~~~~~~~ To test applications with the standard library's unittest or unittest-based functionality, the AioHTTPTestCase is provided:: @@ -215,6 +167,71 @@ functionality, the AioHTTPTestCase is provided:: self.loop.run_until_complete(test_get_route()) +.. class:: AioHTTPTestCase + + A base class to allow for unittest web applications using aiohttp. + + Derived from :class:`unittest.TestCase` + + Provides the following: + + .. attribute:: client + + an aiohttp test client, :class:`TestClient` instance. + + .. attribute:: loop + + The event loop in which the application and server are running. + + .. attribute:: app + + The application returned by :meth:`get_app` + (:class:`aiohttp.web.Application` instance). + + .. method:: get_app(loop) + + This method should be overridden + to return the :class:`aiohttp.web.Application` + object to test. + + :param loop: the event_loop to use + :type loop: asyncio.AbstractEventLoop + + :return: :class:`aiohttp.web.Application` instance. + + .. method:: setUp() + + Standard test initialization method. + + .. method:: tearDown() + + Standard test finalization method. + + + .. note:: + + The ``TestClient``'s methods are asynchronous: you have to + execute function on the test client using asynchronous methods. + + A basic test class wraps every test method by + :func:`unittest_run_loop` decorator:: + + class TestA(AioHTTPTestCase): + + @unittest_run_loop + async def test_f(self): + resp = await self.client.get('/') + + +.. decorator:: unittest_run_loop: + + A decorator dedicated to use with asynchronous methods of an + :class:`AioHTTPTestCase`. + + Handles executing an asynchronous function, using + the :attr:`AioHTTPTestCase.loop` of the :class:`AioHTTPTestCase`. + + Faking request object --------------------- @@ -249,8 +266,116 @@ conditions that hard to reproduce on real server:: way. +.. function:: make_mocked_request(method, path, headers=None, *, \ + version=HttpVersion(1, 1), \ + closing=False, \ + app=None, \ + reader=sentinel, \ + writer=sentinel, \ + transport=sentinel, \ + payload=sentinel, \ + sslcontext=None, \ + secure_proxy_ssl_header=None) + + Creates mocked web.Request testing purposes. + + Useful in unit tests, when spinning full web server is overkill or + specific conditions and errors are hard to trigger. + + :param method: str, that represents HTTP method, like; GET, POST. + :type method: str + + :param path: str, The URL including *PATH INFO* without the host or scheme + :type path: str + + :param headers: mapping containing the headers. Can be anything accepted + by the multidict.CIMultiDict constructor. + :type headers: dict, multidict.CIMultiDict, list of pairs + + :param version: namedtuple with encoded HTTP version + :type version: aiohttp.protocol.HttpVersion + + :param closing: flag indicates that connection should be closed after + response. + :type closing: bool + + :param app: the aiohttp.web application attached for fake request + :type app: aiohttp.web.Application + + :param reader: object for storing and managing incoming data + :type reader: aiohttp.parsers.StreamParser + + :param writer: object for managing outcoming data + :type wirter: aiohttp.parsers.StreamWriter + + :param transport: asyncio transport instance + :type transport: asyncio.transports.Transport + + :param payload: raw payload reader object + :type payload: aiohttp.streams.FlowControlStreamReader + + :param sslcontext: ssl.SSLContext object, for HTTPS connection + :type sslcontext: ssl.SSLContext + + :param secure_proxy_ssl_header: A tuple representing a HTTP header/value + combination that signifies a request is secure. + :type secure_proxy_ssl_header: tuple + + :return: :class:`aiohttp.web.Request` object. + + .. _aiohttp-testing-writing-testable-services: +.. _aiohttp-testing-framework-agnostic-utilities: + +Framework Agnostic Utilities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +High level test creation:: + + from aiohttp.test_utils import TestClient, loop_context + from aiohttp import request + + # loop_context is provided as a utility. You can use any + # asyncio.BaseEventLoop class in it's place. + with loop_context() as loop: + app = _create_example_app(loop) + with TestClient(app) as client: + + async def test_get_route(): + nonlocal client + resp = await client.get("/") + assert resp.status == 200 + text = await resp.text() + assert "Hello, world" in text + + loop.run_until_complete(test_get_route()) + + +If it's preferred to handle the creation / teardown on a more granular +basis, the TestClient object can be used directly:: + + from aiohttp.test_utils import TestClient + + with loop_context() as loop: + app = _create_example_app(loop) + client = TestClient(app) + loop.run_until_complete(client.start_server()) + root = "http://127.0.0.1:{}".format(port) + + async def test_get_route(): + resp = await client.get("/") + assert resp.status == 200 + text = await resp.text() + assert "Hello, world" in text + + loop.run_until_complete(test_get_route()) + loop.run_until_complete(client.close()) + + +A full list of the utilities provided can be found at the +:data:`api reference ` + Writing testable services ------------------------- @@ -526,73 +651,6 @@ Test Client The api corresponds to :meth:`aiohttp.ClientSession.ws_connect`. -Unittest's TestCase -~~~~~~~~~~~~~~~~~~~~ - -.. class:: AioHTTPTestCase - - A base class to allow for unittest web applications using aiohttp. - - Derived from :class:`unittest.TestCase` - - Provides the following: - - .. attribute:: client - - an aiohttp test client, :class:`TestClient` instance. - - .. attribute:: loop - - The event loop in which the application and server are running. - - .. attribute:: app - - The application returned by :meth:`get_app` - (:class:`aiohttp.web.Application` instance). - - .. method:: get_app(loop) - - This method should be overridden - to return the :class:`aiohttp.web.Application` - object to test. - - :param loop: the event_loop to use - :type loop: asyncio.AbstractEventLoop - - :return: :class:`aiohttp.web.Application` instance. - - .. method:: setUp() - - Standard test initialization method. - - .. method:: tearDown() - - Standard test finalization method. - - - .. note:: - - The ``TestClient``'s methods are asynchronous: you have to - execute function on the test client using asynchronous methods. - - A basic test class wraps every test method by - :func:`unittest_run_loop` decorator:: - - class TestA(AioHTTPTestCase): - - @unittest_run_loop - async def test_f(self): - resp = await self.client.get('/') - - -.. decorator:: unittest_run_loop: - - A decorator dedicated to use with asynchronous methods of an - :class:`AioHTTPTestCase`. - - Handles executing an asynchronous function, using - the :attr:`AioHTTPTestCase.loop` of the :class:`AioHTTPTestCase`. - Utilities ~~~~~~~~~ @@ -616,64 +674,6 @@ Utilities *return_value* when called. -.. function:: make_mocked_request(method, path, headers=None, *, \ - version=HttpVersion(1, 1), \ - closing=False, \ - app=None, \ - reader=sentinel, \ - writer=sentinel, \ - transport=sentinel, \ - payload=sentinel, \ - sslcontext=None, \ - secure_proxy_ssl_header=None) - - Creates mocked web.Request testing purposes. - - Useful in unit tests, when spinning full web server is overkill or - specific conditions and errors are hard to trigger. - - :param method: str, that represents HTTP method, like; GET, POST. - :type method: str - - :param path: str, The URL including *PATH INFO* without the host or scheme - :type path: str - - :param headers: mapping containing the headers. Can be anything accepted - by the multidict.CIMultiDict constructor. - :type headers: dict, multidict.CIMultiDict, list of pairs - - :param version: namedtuple with encoded HTTP version - :type version: aiohttp.protocol.HttpVersion - - :param closing: flag indicates that connection should be closed after - response. - :type closing: bool - - :param app: the aiohttp.web application attached for fake request - :type app: aiohttp.web.Application - - :param reader: object for storing and managing incoming data - :type reader: aiohttp.parsers.StreamParser - - :param writer: object for managing outcoming data - :type wirter: aiohttp.parsers.StreamWriter - - :param transport: asyncio transport instance - :type transport: asyncio.transports.Transport - - :param payload: raw payload reader object - :type payload: aiohttp.streams.FlowControlStreamReader - - :param sslcontext: ssl.SSLContext object, for HTTPS connection - :type sslcontext: ssl.SSLContext - - :param secure_proxy_ssl_header: A tuple representing a HTTP header/value - combination that signifies a request is secure. - :type secure_proxy_ssl_header: tuple - - :return: :class:`aiohttp.web.Request` object. - - .. function:: unused_port() Return an unused port number for IPv4 TCP protocol.