core-admin/doc/qubes-tests.rst
Marek Marczykowski-Górecki 13f35da24a
doc/tests: extend qubes-specific quirks in tests
- per-VM (template) tests
- extra tests loader
2019-11-30 04:38:10 +01:00

248 lines
10 KiB
ReStructuredText

:py:mod:`qubes.tests` -- Writing tests for qubes
================================================
Writing tests is very important for ensuring quality of code that is delivered.
Given test case may check for variety of conditions, but they generally fall
inside those two categories of conformance tests:
* Unit tests: these test smallest units of code, probably methods of functions,
or even combination of arguments for one specific method.
* Integration tests: these test interworking of units.
We are interested in both categories.
There is also distinguished category of regression tests (both unit- and
integration-level), which are included because they check for specific bugs that
were fixed in the past and should not happen in the future. Those should be
accompanied with reference to closed ticked that describes the bug.
Qubes' tests are written using :py:mod:`unittest` module from Python Standard
Library for both unit test and integration tests.
Test case organisation
----------------------
Every module (like :py:mod:`qubes.vm.qubesvm`) should have its companion (like
``qubes.tests.vm.qubesvm``). Packages ``__init__.py`` files should be
accompanied by ``init.py`` inside respective directory under :file:`tests/`.
Inside tests module there should be one :py:class:`qubes.tests.QubesTestCase`
class for each class in main module plus one class for functions and global
variables. :py:class:`qubes.tests.QubesTestCase` classes should be named
``TC_xx_ClassName``, where ``xx`` is two-digit number. Test functions should be
named ``test_xxx_test_name``, where ``xxx`` is three-digit number. You may
introduce some structure of your choice in this number.
Integration tests for Qubes core features are stored in :file:`tests/integ/`
directory. Additional tests may be loaded from other packages (see extra test
loader below). Those tests are run only on real Qubes system and are not suitable
for running in VM or in Travis. Test classes of this category inherit from
:py:class:`qubes.tests.SystemTestCase`.
Writing tests
-------------
First of all, testing is art, not science. Testing is not panaceum and won't
solve all of your problems. Rules given in this guide and elsewhere should be
followed, but shouldn't be worshipped.
Test can be divided into three phases. The first part is setup phase. In this
part you should arrange for a test condition to occur. You intentionally put
system under test in some specific state. Phase two is executing test condition
-- for example you check some variable for equality or expect that some
exception is raised. Phase three is responsible for returning a verdict. This is
largely done by the framework.
When writing test, you should think about order of execution. This is the reason
of numbers in names of the classes and test methods. Tests should be written
bottom-to-top, that is, test setups that are ran later may depend on features
that are tested after but not the other way around. This is important, because
when encountering failure we expect the reason happen *before*, and not after
failure occured. Therefore, when encountering multiple errors, we may instantly
focus on fixing the first one and not wondering if any later problems may be
relevant or not. Some people also like to enable
:py:attr:`unittest.TestResult.failfast` feature, which stops on the first failed
test -- with wrong order this messes up their workflow.
Test should fail for one reason only and test one specific issue. This does not
mean that you can use one ``.assert*`` method per ``test_`` function: for
example when testing one regular expression you are welcome to test many valid
and/or invalid inputs, especcialy when test setup is complicated. However, if
you encounter problems during setup phase, you should *skip* the test, and not
fail it. This also aids interpretation of results.
You may, when it makes sense, manipulate private members of classes under tests.
This violates one of the founding principles of object-oriented programming, but
may be required to write tests in correct order if your class provides public
methods with circular dependencies. For example containers may check if added
item is already in container, but you can't test ``__contains__`` method without
something already inside. Don't forget to test the other method later.
Special Qubes-specific considerations
-------------------------------------
Events
^^^^^^
:py:class:`qubes.tests.QubesTestCase` provides convenient methods for checking
if event fired or not: :py:meth:`qubes.tests.QubesTestCase.assertEventFired` and
:py:meth:`qubes.tests.QubesTestCase.assertEventNotFired`. These require that
emitter is subclass of :py:class:`qubes.tests.TestEmitter`. You may instantiate
it directly::
import qubes.tests
class TC_10_SomeClass(qubes.tests.QubesTestCase):
def test_000_event(self):
emitter = qubes.tests.TestEmitter()
emitter.fire_event('did-fire')
self.assertEventFired(emitter, 'did-fire')
If you need to snoop specific class (which already is a child of
:py:class:`qubes.events.Emitter`, possibly indirect), you can define derivative
class which uses :py:class:`qubes.tests.TestEmitter` as mix-in::
import qubes
import qubes.tests
class TestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder):
pass
class TC_20_PropertyHolder(qubes.tests.QubesTestCase):
def test_000_event(self):
emitter = TestHolder()
self.assertEventNotFired(emitter, 'did-not-fire')
Dom0
^^^^
Qubes is a complex piece of software and depends on number other complex pieces,
notably VM hypervisor or some other isolation provider. Not everything may be
testable under all conditions. Some tests (mainly unit tests) are expected to
run during compilation, but many tests (probably all of the integration tests
and more) can run only inside already deployed Qubes installation. There is
special decorator, :py:func:`qubes.tests.skipUnlessDom0` which causes test (or
even entire class) to be skipped outside dom0. Use it freely::
import qubes.tests
class TC_30_SomeClass(qubes.tests.QubesTestCase):
@qubes.tests.skipUnlessDom0
def test_000_inside_dom0(self):
# this is skipped outside dom0
pass
@qubes.tests.skipUnlessDom0
class TC_31_SomeOtherClass(qubes.tests.QubesTestCase):
# all tests in this class are skipped
pass
VM tests
^^^^^^^^
Some integration tests verifies not only dom0 part of the system, but also VM
part. In those cases, it makes sense to iterate them for different templates.
Additionally, list of the templates can be dynamic (different templates
installed, only some considered for testing etc).
This can be achieved by creating a mixin class with the actual tests (a class
inheriting just from :py:class:`object`, instead of
:py:class:`qubes.tests.SystemTestCase` or :py:class:`unittest.TestCase`) and
then create actual test classes dynamically using
:py:func:`qubes.tests.create_testcases_for_templates`.
Test classes created this way will have :py:attr:`template` set to the template
name under test and also this template will be set as the default template
during the test execution.
The function takes a test class name prefix (template name will be appended to
it after '_' separator), a classes to inherit from (in most cases the just
created mixin and :py:class:`qubes.tests.SystemTestCase`) and a current module
object (use `sys.modules[__name__]`). The function will return created test
classes but also add them to the appropriate module (pointed by the *module*
parameter). This should be done in two cases:
* :py:func:`load_tests` function - when test loader request list of tests
* on module import time, using a wrapper
:py:func:`qubes.tests.maybe_create_testcases_on_import` (will call the
function only if explicit list of templates is given, to avoid loading
:file:`qubes.xml` when just importing the module)
An example boilerplate looks like this::
def create_testcases_for_templates():
return qubes.tests.create_testcases_for_templates('TC_00_AppVM',
TC_00_AppVMMixin, qubes.tests.SystemTestCase,
module=sys.modules[__name__])
def load_tests(loader, tests, pattern):
tests.addTests(loader.loadTestsFromNames(
create_testcases_for_templates()))
return tests
qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)
This will by default create tests for all the templates installed in the system.
Additionally, it is possible to control this process using environment
variables:
* `QUBES_TEST_TEMPLATES` - space separated list of templates to test
* `QUBES_TEST_LOAD_ALL` - create tests for all the templates (by inspecting
the :file:`qubes.xml` file), even at module import time
This is dynamic test creation is intentionally made compatible with Nose2 test
runner and its load_tests protocol implementation.
Extra tests
^^^^^^^^^^^
Most tests live in this package, but it is also possible to store tests in other
packages while still using infrastructure provided here and include them in the
common test run. Loading extra tests is implemented in
:py:mod:`qubes.tests.extra`. To write test to be loaded this way, you need to
create test class(es) as usual. You can also use helper class
:py:class:`qubes.tests.extra.ExtraTestCase` (instead of
:py:class:`qubes.tests.SystemTestCase`) which provide few convenient functions
and hide usage of asyncio for simple cases (like `vm.start()`, `vm.run()`).
The next step is to register the test class(es). You need to do this by defining
entry point for your package. There are two groups:
* `qubes.tests.extra` - for general tests (called once)
* `qubes.tests.extra.for_template` - for per-VM tests (called for each template
under test)
As a name in the group, choose something unique, preferably package name. An
object reference should point at the function that returns a list of test
classes.
Example :file:`setup.py`::
from setuptools import setup
setup(
name='splitgpg',
version='1.0',
packages=['splitgpg'],
entry_points={
'qubes.tests.extra.for_template':
'splitgpg = splitgpg.tests:list_tests',
}
)
The test loading process can be additionally controlled with environment
variables:
* `QUBES_TEST_EXTRA_INCLUDE` - space separated list of tests to include (named
by a name in an entry point, `splitgpg` in the above example); if defined, only
those extra tests will be loaded
* `QUBES_TEST_EXTRA_EXCLUDE` - space separated list of tests to exclude
Module contents
---------------
.. automodule:: qubes.tests
:members:
:show-inheritance:
.. vim: ts=3 sw=3 et tw=80