From ea89a85b5e93e6f02abd98878a53264fc5d3a484 Mon Sep 17 00:00:00 2001 From: jcass Date: Wed, 20 Jan 2016 00:07:15 +0200 Subject: [PATCH 1/5] docs:add section with some background and pointers on how to test extensions. --- docs/extensiondev.rst | 225 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index f797368f..4b96d2e7 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -542,3 +542,228 @@ your HTTP requests:: For further details, see Requests' docs on `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 `_. +When it comes to running tests, Mopidy typically makes use of testing tools +like `tox `_ and +`pytest `_. + +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 `_ 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 `_ +directory on the Mopidy development branch contains hundreds of sample test +cases that cover virtually every aspect of using the server. \ No newline at end of file From 7f03b2125840ecc495fa88d1e4709cbc8921767e Mon Sep 17 00:00:00 2001 From: jcass Date: Wed, 20 Jan 2016 00:19:59 +0200 Subject: [PATCH 2/5] docs:align case of headings with rest of section. Remove fragmented sentences. --- docs/extensiondev.rst | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 4b96d2e7..8e6cc106 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -543,7 +543,7 @@ your HTTP requests:: For further details, see Requests' docs on `session objects `__. -Testing Extensions +Testing extensions ================== Creating test cases for your extensions makes them much simpler to maintain @@ -557,7 +557,7 @@ When it comes to running tests, Mopidy typically makes use of testing tools like `tox `_ and `pytest `_. -Testing Approach +Testing approach ---------------- To a large extent the testing approach to follow depends on how your extension @@ -574,7 +574,7 @@ 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 +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 @@ -613,7 +613,7 @@ An example of what these tests could look like is provided below:: registry.add.assert_has_calls(calls, any_order=True) -Testing Backend Actors +Testing backend actors ---------------------- Backends can usually be constructed with a small mockup of the configuration file, and mocking the audio actor:: @@ -671,19 +671,19 @@ 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 +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 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 +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:: @@ -730,11 +730,11 @@ 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 +Keep in mind 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 +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 @@ -751,8 +751,9 @@ actor, may look something like this:: self.send_mock.side_effect = send -...and then just call ``replay_events()`` at the relevant points in your code -to have the events fire:: +Once all of the events have been captured, a method like +``replay_events()`` can be called at the relevant points in the code to have +the events fire:: def replay_events(self, my_actor, until=None): while self.events: @@ -762,8 +763,6 @@ to have the events fire:: frontend.on_event(event, **kwargs).get() -Further Reading ---------------- -The `/tests `_ -directory on the Mopidy development branch contains hundreds of sample test -cases that cover virtually every aspect of using the server. \ No newline at end of file +For further details and examples, refer to the +`/tests `_ +directory on the Mopidy development branch. \ No newline at end of file From f62057a9ad57bbbc060028f83774efe0639a4271 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 17 Jan 2016 07:55:34 +0100 Subject: [PATCH 3/5] flake8: Fix compat with pep8 1.7.0 (cherry picked from commit 18b609fa6ed3c06c0dc3156cbb7409c9494c0bc2) --- mopidy/audio/scan.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index ca2c308c..fd5d2d49 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -141,8 +141,9 @@ def _process(pipeline, timeout_ms): have_audio = False missing_message = None - types = (gst.MESSAGE_ELEMENT | gst.MESSAGE_APPLICATION | gst.MESSAGE_ERROR - | gst.MESSAGE_EOS | gst.MESSAGE_ASYNC_DONE | gst.MESSAGE_TAG) + types = ( + gst.MESSAGE_ELEMENT | gst.MESSAGE_APPLICATION | gst.MESSAGE_ERROR | + gst.MESSAGE_EOS | gst.MESSAGE_ASYNC_DONE | gst.MESSAGE_TAG) previous = clock.get_time() while timeout > 0: From 05729d3dc068e92da0e0f080b780b8a298417bf7 Mon Sep 17 00:00:00 2001 From: jcass Date: Wed, 20 Jan 2016 09:36:07 +0200 Subject: [PATCH 4/5] docs:fix bullet list formatting. --- docs/extensiondev.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 8e6cc106..c208a092 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -577,6 +577,7 @@ 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 @@ -651,6 +652,7 @@ testing:: 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.):: From edc3929dafe73dfb4f7d2b59711c7ab57b2df618 Mon Sep 17 00:00:00 2001 From: jcass Date: Wed, 20 Jan 2016 11:49:29 +0200 Subject: [PATCH 5/5] docs:address PR review comments. --- docs/extensiondev.rst | 48 ++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index c208a092..348082fd 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -576,6 +576,7 @@ 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 @@ -589,20 +590,20 @@ An example of what these tests could look like is provided below:: 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) + assert '[my_extension]' in config + assert 'enabled = true' in config + assert 'param_1 = value_1' in config + assert 'param_2 = value_2' in config + assert 'param_n = value_n' in 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) + assert 'enabled' in schema + assert 'param_1' in schema + assert 'param_2' in schema + assert 'param_n' in schema def test_setup(self): registry = mock.Mock() @@ -616,10 +617,11 @@ An example of what these tests could look like is provided below:: Testing backend actors ---------------------- + Backends can usually be constructed with a small mockup of the configuration file, and mocking the audio actor:: - @pytest.fixture() + @pytest.fixture def config(): return { 'http': { @@ -641,10 +643,17 @@ file, and mocking the audio actor:: def get_backend(config): return backend.MyBackend(config=config, audio=mock.Mock()) +The following libraries might be useful for mocking any HTTP requests that +your extension makes: -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:: +- `responses `_ - A utility library for + mocking out the requests Python library. +- `vcrpy `_ - Automatically mock your HTTP + interactions to simplify and speed up testing. + +At the very least, 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', @@ -654,7 +663,9 @@ testing:: 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.):: +- that it sets up the various providers (e.g. library, playback, etc.) + +:: def test_uri_schemes(config): backend = get_backend(config) @@ -675,18 +686,21 @@ 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:: @@ -700,8 +714,9 @@ 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:: +You'll also need a list of :class:`~mopidy.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 = [ @@ -738,6 +753,7 @@ 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