Skip to content

Commit

Permalink
adding a fuller testclient, and documentation. (#910)
Browse files Browse the repository at this point in the history
  • Loading branch information
toumorokoshi authored and asvetlov committed Jun 3, 2016
1 parent 032c38a commit 2291aa3
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 50 deletions.
120 changes: 96 additions & 24 deletions aiohttp/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@

import asyncio
import aiohttp
from aiohttp import server
from aiohttp import helpers
from aiohttp import ClientSession
from aiohttp.client import _RequestContextManager
from . import server
from . import helpers
from . import ClientSession
from . import hdrs


def run_briefly(loop):
Expand Down Expand Up @@ -322,15 +322,16 @@ class TestClient:
"""
A test client implementation, for a aiohttp.web.Application.
:param app: the aiohttp.web application passed
to create_test_server
:param app: the aiohttp.web application passed to create_test_server
:type app: aiohttp.web.Application
:param protocol: the aiohttp.web application passed
to create_test_server
:param protocol: the aiohttp.web application passed to create_test_server
:type app: aiohttp.web.Application
TestClient can also be used as a contextmanager, returning
the instance of itself instantiated.
"""

def __init__(self, app, protocol="http"):
Expand All @@ -347,23 +348,72 @@ def __init__(self, app, protocol="http"):
)
self._closed = False

def request(self, method, url, *args, **kwargs):
return _RequestContextManager(self._request(
method, url, *args, **kwargs
))
@property
def session(self):
"""a raw handler to the aiohttp.ClientSession. unlike the methods on
the TestClient, client session requests do not automatically
include the host in the url queried, and will require an
absolute path to the resource.
"""
return self._session

@asyncio.coroutine
def _request(self, method, url, *args, **kwargs):
def request(self, method, path, *args, **kwargs):
""" routes a request to the http server.
the interface is identical to asyncio.request,
except the loop kwarg is overriden
by the instance used by the application.
"""
return (yield from self._session.request(
method, self._root + url, *args, **kwargs
))
return self._session.request(
method, self._root + path, *args, **kwargs
)

def get(self, path, *args, **kwargs):
"""Perform an HTTP GET request. """
return self.request(hdrs.METH_GET, path, *args, **kwargs)

def post(self, path, *args, **kwargs):
"""Perform an HTTP POST request. """
return self.request(hdrs.METH_POST, path, *args, **kwargs)

def options(self, path, *args, **kwargs):
"""Perform an HTTP OPTIONS request. """
return self.request(hdrs.METH_OPTIONS, path, *args, **kwargs)

def head(self, path, *args, **kwargs):
"""Perform an HTTP HEAD request. """
return self.request(hdrs.METH_HEAD, path, *args, **kwargs)

def put(self, path, *args, **kwargs):
"""Perform an HTTP PUT request."""
return self.request(hdrs.METH_PUT, path, *args, **kwargs)

def patch(self, path, *args, **kwargs):
"""Perform an HTTP PATCH request."""
return self.request(hdrs.METH_PATCH, path, *args, **kwargs)

def delete(self, path, *args, **kwargs):
"""Perform an HTTP PATCH request."""
return self.request(hdrs.METH_DELETE, path, *args, **kwargs)

def ws_connect(self, path, *args, **kwargs):
"""Initiate websocket connection. the api is identical to
aiohttp.ClientSession.ws_connect.
"""
return self._session.ws_connect(
self._root + path, *args, **kwargs
)

def close(self):
""" close all fixtures created by the test client.
After that point, the TestClient is no longer
usable.
This is an idempotent function: running close
multiple times will not have any additional effects.
close is also run when the object is garbage collected,
and on exit when used as a context manager.
"""
if not self._closed:
loop = self._loop
loop.run_until_complete(self._session.close())
Expand All @@ -384,6 +434,20 @@ def __exit__(self, exc_type, exc_value, traceback):


class AioHTTPTestCase(unittest.TestCase):
"""A base class to allow for unittest web applications using
aiohttp.
provides the following:
* self.client (aiohttp.test_utils.TestClient): an aiohttp test client.
* self.loop (asyncio.BaseEventLoop): the event loop in which the
application and server are running.
* self.app (aiohttp.web.Application): the application returned by
self.get_app()
note that the TestClient's methods are asynchronous: you will have to
execute function on the test client using asynchronous methods.
"""

def get_app(self, loop):
"""
Expand All @@ -406,11 +470,10 @@ def tearDown(self):
teardown_test_loop(self.loop)


def run_loop(func):
"""
to be used with AioHTTPTestCase. Handles
executing an asynchronous function, using
the event loop of AioHTTPTestCase.
def unittest_run_loop(func):
"""a decorator that should be used with asynchronous methods of an
AioHTTPTestCase. Handles executing an asynchronous function, using
the self.loop of the AioHTTPTestCase.
"""

@functools.wraps(func)
Expand All @@ -422,8 +485,7 @@ def new_func(self):

@contextlib.contextmanager
def loop_context():
"""
create an event_loop, for test purposes.
"""a contextmanager that creates an event_loop, for test purposes.
handles the creation and cleanup of a test loop.
"""
loop = setup_test_loop()
Expand All @@ -432,12 +494,22 @@ def loop_context():


def setup_test_loop():
"""create and return an asyncio.BaseEventLoop
instance. The caller should also call teardown_test_loop,
once they are done with the loop.
"""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(None)
return loop


def teardown_test_loop(loop):
"""teardown and cleanup an event_loop created
by setup_test_loop.
:param loop: the loop to teardown
:type loop: asyncio.BaseEventLoop
"""
is_closed = getattr(loop, 'is_closed')
if is_closed is not None:
closed = is_closed()
Expand Down
8 changes: 0 additions & 8 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,6 @@ aiohttp.streams module
:show-inheritance:


aiohttp.test_utils module
-------------------------

.. automodule:: aiohttp.test_utils
:members:
:undoc-members:
:show-inheritance:

aiohttp.websocket module
------------------------

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ Contents
multipart
api
logging
testing
gunicorn
faq
new_router
Expand Down
48 changes: 37 additions & 11 deletions docs/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
Testing
=======

Testing aiohttp servers
-----------------------
.. currentmodule:: aiohttp.test_utils

Testing aiohttp web servers
---------------------------

aiohttp provides test framework agnostic utilities for web
servers. An example would be::
Expand All @@ -20,7 +22,7 @@ servers. An example would be::

async def test_get_route():
nonlocal client
resp = await client.request("GET", "/")
resp = await client.get("/")
assert resp.status == 200
text = await resp.text()
assert "Hello, world" in text
Expand All @@ -39,7 +41,7 @@ basis, the TestClient object can be used directly::
root = "http://127.0.0.1:{}".format(port)

async def test_get_route():
resp = await test_client.request("GET", url)
resp = await client.get("/")
assert resp.status == 200
text = await resp.text()
assert "Hello, world" in text
Expand All @@ -49,8 +51,24 @@ basis, the TestClient object can be used directly::
# the deletion of the TestServer.
del client

pytest example
==============

A full list of the utilities provided can be found at the
:data:`api reference <aiohttp.test_utils>`

The Test Client
~~~~~~~~~~~~~~~

The :data:`aiohttp.test_utils.TestClient` creates an asyncio server
for the web.Application object, as well as a ClientSession to perform
requests. In addition, TestClient provides 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.test_utils.TestClient>`



Pytest example
~~~~~~~~~~~~~~

A pytest example could look like::

Expand Down Expand Up @@ -85,13 +103,13 @@ A pytest example could look like::
loop.run_until_complete(test_get_route())


unittest example
================
Unittest example
~~~~~~~~~~~~~~~~

To test applications with the standard library's unittest or unittest-based
functionality, the AioHTTPTestCase is provided::

from aiohttp.test_utils import AioHTTPTestCase, run_loop
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop
from aiohttp import web

class MyAppTestCase(AioHTTPTestCase):
Expand All @@ -104,10 +122,10 @@ functionality, the AioHTTPTestCase is provided::
# it's important to use the loop passed here.
return web.Application(loop=loop)

# the run_loop decorator can be used in tandem with
# the unittest_run_loop decorator can be used in tandem with
# the AioHTTPTestCase to simplify running
# tests that are asynchronous
@run_loop
@unittest_run_loop
async def test_example(self):
request = await self.client.request("GET", "/")
assert request.status == 200
Expand All @@ -124,3 +142,11 @@ functionality, the AioHTTPTestCase is provided::
assert "Hello, world" in text

self.loop.run_until_complete(test_get_route())

aiohttp.test_utils
------------------

.. automodule:: aiohttp.test_utils
:members: TestClient, AioHTTPTestCase, run_loop, loop_context, setup_test_loop, teardown_test_loop
:undoc-members:
:show-inheritance:
Loading

0 comments on commit 2291aa3

Please sign in to comment.