docs:add section with some background and pointers on how to test extensions.

This commit is contained in:
jcass 2016-01-20 00:07:15 +02:00
parent aa9e806230
commit ea89a85b5e

View File

@ -542,3 +542,228 @@ your HTTP requests::
For further details, see Requests' docs on `session objects
<http://www.python-requests.org/en/latest/user/advanced/#session-objects>`__.
Testing Extensions
==================
Creating test cases for your extensions makes them much simpler to maintain
over the long term. It can also make it easier for you to review and accept
pull requests from other contributors knowing that they will not break the
extension in some unanticipated way.
Before getting started, it is important to familiarize yourself with the
Python `mock library <https://docs.python.org/dev/library/unittest.mock.html>`_.
When it comes to running tests, Mopidy typically makes use of testing tools
like `tox <https://tox.readthedocs.org/en/latest/>`_ and
`pytest <http://pytest.org/latest/>`_.
Testing Approach
----------------
To a large extent the testing approach to follow depends on how your extension
is structured, which parts of Mopidy it interacts with, and if it uses any 3rd
party APIs or makes any HTTP requests to the outside world.
The sections that follow contain code extracts that highlight some of the
key areas that should be tested. For more exhaustive examples, you may want to
take a look at the test cases that ship with Mopidy itself which covers
everything from instantiating various controllers, reading configuration files,
and simulating events that your extension can listen to.
In general your tests should cover the extension definition, the relevant
Mopidy controllers, and the Pykka backend and / or frontend actors that form
part of the extension.
Testing the Extension Definition
--------------------------------
Test cases for checking the definition of the extension should ensure that:
- the extension provides a ``ext.conf`` configuration file containing the
relevant parameters with their default values,
- that the config schema is fully defined, and
- that the extension's actor(s) are added to the Mopidy registry on setup.
An example of what these tests could look like is provided below::
def test_get_default_config(self):
ext = Extension()
config = ext.get_default_config()
self.assertIn('[my_extension]', config)
self.assertIn('enabled = true', config)
self.assertIn('param_1 = value_1', config)
self.assertIn('param_2 = value_2', config)
self.assertIn('param_n = value_n', config)
def test_get_config_schema(self):
ext = Extension()
schema = ext.get_config_schema()
self.assertIn('enabled', schema)
self.assertIn('param_1', schema)
self.assertIn('param_2', schema)
self.assertIn('param_n', schema)
def test_setup(self):
registry = mock.Mock()
ext = Extension()
ext.setup(registry)
calls = [mock.call('frontend', frontend_lib.MyFrontend),
mock.call('backend', backend_lib.MyBackend)]
registry.add.assert_has_calls(calls, any_order=True)
Testing Backend Actors
----------------------
Backends can usually be constructed with a small mockup of the configuration
file, and mocking the audio actor::
@pytest.fixture()
def config():
return {
'http': {
'hostname': '127.0.0.1',
'port': '6680'
},
'proxy': {
'hostname': 'host_mock',
'port': 'port_mock'
},
'my_extension': {
'enabled': True,
'param_1': 'value_1',
'param_2': 'value_2',
'param_n': 'value_n',
}
}
def get_backend(config):
return backend.MyBackend(config=config, audio=mock.Mock())
You'll probably want to patch ``requests`` or any other web API's that you use
to avoid any unintended HTTP requests from being made by your backend during
testing::
from mock import patch
@mock.patch('requests.get',
mock.Mock(side_effect=Exception('Intercepted unintended HTTP call')))
Backend tests should also ensure that:
- the backend provides a unique URI scheme,
- that it sets up the various providers (e.g. library, playback, etc.)::
def test_uri_schemes(config):
backend = get_backend(config)
assert 'my_scheme' in backend.uri_schemes
def test_init_sets_up_the_providers(config):
backend = get_backend(config)
assert isinstance(backend.library, library.MyLibraryProvider)
assert isinstance(backend.playback, playback.MyPlaybackProvider)
Once you have a backend instance to work with, testing the various playback,
library, and other providers is straight forward and should not require any
special setup or processing.
Testing Libraries
-----------------
Library test cases should cover the implementations of the standard Mopidy
API (e.g. ``browse``, ``lookup``, ``refresh``, ``get_images``, ``search``,
etc.)
Testing Playback Controllers
----------------------------
Testing ``change_track`` and ``translate_uri`` is probably the highest
priority, since these methods are used to prepare the track and provide its
audio URL to Mopidy's core for playback.
Testing Frontends
-----------------
Because most frontends will interact with the Mopidy core, it will most likely
be necessary to have a full core running for testing purposes::
self.core = core.Core.start(
config, backends=[get_backend(config]).proxy()
It may be advisable to take a quick look at the
`Pykka API <https://www.pykka.org/en/latest/>`_ at this point to make sure that
you are familiar with ``ThreadingActor``, ``ThreadingFuture``, and the
``proxies`` that allow you to access the attributes and methods of the actor
directly.
You'll also need a list of ``models.Track`` and a list of URIs in order to
populate the core with some simple tracks that can be used for testing::
class BaseTest(unittest.TestCase):
tracks = [
models.Track(uri='my_scheme:track:id1', length=40000), # Regular track
models.Track(uri='my_scheme:track:id2', length=None), # No duration
]
uris = [ 'my_scheme:track:id1', 'my_scheme:track:id2']
In the ``setup()`` method of your test class, you will then probably need to
monkey patch looking up tracks in the library (so that it will always use the
lists that you defined), and then populate the core's tracklist::
def lookup(uris):
result = {uri: [] for uri in uris}
for track in self.tracks:
if track.uri in result:
result[track.uri].append(track)
return result
self.core.library.lookup = lookup
self.tl_tracks = self.core.tracklist.add(uris=self.uris).get()
With all of that done you should finally be ready to instantiate your frontend::
self.frontend = frontend.MyFrontend.start(config(), self.core).proxy()
...and then just remember that the normal core and frontend methods will usually
return ``pykka.ThreadingFuture`` objects, so you will need to add ``.get()`` at
the end of most method calls in order to get to the actual return values.
Triggering Events
-----------------
There may be test case scenarios that require simulating certain event triggers
that your extension's actors can listen for and respond on. An example for
patching the listener to store these events, and then play them back for your
actor, may look something like this::
self.events = []
self.patcher = mock.patch('mopidy.listener.send')
self.send_mock = self.patcher.start()
def send(cls, event, **kwargs):
self.events.append((event, kwargs))
self.send_mock.side_effect = send
...and then just call ``replay_events()`` at the relevant points in your code
to have the events fire::
def replay_events(self, my_actor, until=None):
while self.events:
if self.events[0][0] == until:
break
event, kwargs = self.events.pop(0)
frontend.on_event(event, **kwargs).get()
Further Reading
---------------
The `/tests <https://github.com/mopidy/mopidy/tree/develop/tests>`_
directory on the Mopidy development branch contains hundreds of sample test
cases that cover virtually every aspect of using the server.