Skip to content

Commit

Permalink
Updated testing documentation
Browse files Browse the repository at this point in the history
This was vastly outdated and no longer working.
  • Loading branch information
agronholm committed Mar 30, 2022
1 parent 62d2bc8 commit e2bc17d
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 78 deletions.
1 change: 0 additions & 1 deletion docs/tutorials/webnotifier.rst
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,6 @@ class to it::
import logging

import aiohttp

from asphalt.core import Component, Event, Signal, context_teardown

logger = logging.getLogger(__name__)
Expand Down
78 changes: 32 additions & 46 deletions docs/userguide/testing.rst
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
Testing Asphalt components
==========================

.. py:currentmodule:: asphalt.core
Testing Asphalt components and component hierarchies is a relatively simple procedure:

#. Create an instance of your :class:`~asphalt.core.component.Component`
#. Create a :class:`~asphalt.core.context.Context` instance
#. Run the component's ``start()`` method with the context as the argument
#. Run the component's :meth:`~.component.Component.start` method with the context as the argument
#. Run the tests
#. Close the context to release any resources

With Asphalt projects, it is recommended to use the `py.test`_ testing framework because it is
With Asphalt projects, it is recommended to use the pytest_ testing framework because it is
already being used with Asphalt core and it provides easy testing of asynchronous code
(via the pytest-asyncio_ plugin).

Expand All @@ -24,79 +26,63 @@ them against each other.
Create a ``tests`` directory at the root of the project directory and create a module named
``test_client_server`` there (the ``test_`` prefix is important)::

import asyncio

import pytest
from asphalt.core import Context

from echo.client import ClientComponent
from echo.server import ServerComponent


@pytest.fixture
def event_loop():
# Required on pytest-asyncio v0.4.0 and newer since the event_loop fixture provided by the
# plugin no longer sets the global event loop
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
yield loop
loop.close()

def test_client_and_server(event_loop, capsys):
async def run():
async with Context() as ctx:
server = ServerComponent()
await server.start(ctx)

@pytest.fixture
def context(event_loop):
with Context() as ctx:
yield ctx
client = ClientComponent("Hello!")
await client.start(ctx)

event_loop.create_task(run())
with pytest.raises(SystemExit) as exc:
event_loop.run_forever()

@pytest.fixture
def server_component(event_loop, context):
component = ServerComponent()
event_loop.run_until_complete(component.start(context))


def test_client(event_loop, server_component, context, capsys):
client = ClientComponent('Hello!')
event_loop.run_until_complete(client.start(context))
exc = pytest.raises(SystemExit, event_loop.run_forever)
assert exc.value.code == 0

# Grab the captured output of sys.stdout and sys.stderr from the capsys fixture
out, err = capsys.readouterr()
assert out == 'Message from client: Hello!\nServer responded: Hello!\n'

The test module above contains one test function (``test_client``) and three fixtures:
assert out == "Message from client: Hello!\nServer responded: Hello!\n"

* ``event_loop``: provides an asyncio event loop and closes it after the test
* ``context`` provides the root context and runs teardown callbacks after the test
* ``server_component``: creates and starts the server component
The test module above contains one test function which uses two fixtures:

The client component is not provided as a fixture because, as always with
:class:`~asphalt.core.component.CLIApplicationComponent`, starting it would run the logic we want
to test, so we defer that to the actual test code.
* ``event_loop``: comes from pytest-asyncio_; provides an asyncio event loop
* ``capsys``: captures standard output and error, letting us find out what message the components
printed

In the test function (``test_client``), the client component is instantiated and started. Since the
component's ``start()`` function only kicks off the task that runs the client's business logic (the
``run()`` method), we have to wait until the task is complete by running the event loop (using
:meth:`~asyncio.AbstractEventLoop.run_forever`) until ``run()`` finishes and its callback code
attempts to terminate the application. For that purpose, we catch the resulting :exc:`SystemExit`
exception and verify that the application indeed completed successfully, as indicated by the return
code of 0.
In the test function (``test_client_and_server()``), the server and client components are
instantiated and started. Since the client component's
:meth:`~.component.CLIApplicationComponent.start` function only kicks off a task that runs the
client's business logic (the :meth:`~.component.CLIApplicationComponent.run` method), we have to
wait until the task is complete by running the event loop (using
:meth:`~asyncio.loop.run_forever`) until
:meth:`~.component.CLIApplicationComponent.run` finishes and its callback code attempts to
terminate the application. For that purpose, we catch the resulting :exc:`SystemExit` exception and
verify that the application indeed completed successfully, as indicated by the return code of 0.

Finally, we check that the server and the client printed the messages they were supposed to.
When the server receives a line from the client, it prints a message to standard output using
:func:`print`. Likewise, when the client gets a response from the server, it too prints out its
own message. By using pytest's built-in ``capsys`` fixture, we can capture the output and verify it
own message. By using pytest's built-in capsys_ fixture, we can capture the output and verify it
against the expected lines.

To run the test suite, make sure you're in the project directory and then do:

.. code-block:: bash
pytest tests
PYTHONPATH=. pytest tests
For more elaborate examples, please see the test suites of various `Asphalt subprojects`_.

.. _py.test: http://pytest.org/
.. _pytest: http://pytest.org/
.. _pytest-asyncio: https://pypi.python.org/pypi/pytest-asyncio
.. _capsys: https://docs.pytest.org/en/6.2.x/capture.html#accessing-captured-output-from-a-test-function
.. _Asphalt subprojects: https://github.com/asphalt-framework
40 changes: 13 additions & 27 deletions examples/tutorial1/tests/test_client_server.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,25 @@
import asyncio

# isort: off
import pytest
from echo.client import ClientComponent
from echo.server import ServerComponent

from asphalt.core import Context

from echo.client import ClientComponent
from echo.server import ServerComponent

@pytest.fixture
def event_loop():
# Required on pytest-asyncio v0.4.0 and newer since the event_loop fixture provided by the
# plugin no longer sets the global event loop
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
yield loop
loop.close()


@pytest.fixture
def context(event_loop):
ctx = Context()
yield ctx
event_loop.run_until_complete(ctx.finished.dispatch(None, return_future=True))

def test_client_and_server(event_loop, capsys):
async def run():
async with Context() as ctx:
server = ServerComponent()
await server.start(ctx)

@pytest.fixture
def server_component(event_loop, context):
component = ServerComponent()
event_loop.run_until_complete(component.start(context))
client = ClientComponent("Hello!")
await client.start(ctx)

event_loop.create_task(run())
with pytest.raises(SystemExit) as exc:
event_loop.run_forever()

def test_client(event_loop, server_component, context, capsys):
client = ClientComponent("Hello!")
event_loop.run_until_complete(client.start(context))
exc = pytest.raises(SystemExit, event_loop.run_forever)
assert exc.value.code == 0

# Grab the captured output of sys.stdout and sys.stderr from the capsys fixture
Expand Down
5 changes: 3 additions & 2 deletions examples/tutorial2/webnotifier/app.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""This is the root component for the Asphalt webnotifier tutorial."""
# isort: off
import logging
from difflib import HtmlDiff

from async_generator import aclosing
from webnotifier.detector import ChangeDetectorComponent

from asphalt.core import CLIApplicationComponent

from webnotifier.detector import ChangeDetectorComponent

logger = logging.getLogger(__name__)


Expand Down
2 changes: 1 addition & 1 deletion examples/tutorial2/webnotifier/detector.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""This is the change detector component for the Asphalt webnotifier tutorial."""
# isort: off
import asyncio
import logging

import aiohttp

from asphalt.core import Component, Event, Signal
from asphalt.core.context import context_teardown

Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ version_scheme = "post-release"
local_scheme = "dirty-tag"

[tool.isort]
src_paths = ["src"]
skip_gitignore = true
profile = "black"

Expand Down

0 comments on commit e2bc17d

Please sign in to comment.