Переглянути джерело

Developer's documentation for qubes.tests

Wojtek Porczyk 9 роки тому
батько
коміт
de9eb60f61
3 змінених файлів з 136 додано та 3 видалено
  1. 1 0
      doc/index.rst
  2. 128 0
      doc/qubes-tests.rst
  3. 7 3
      qubes/tests/__init__.py

+ 1 - 0
doc/index.rst

@@ -32,6 +32,7 @@ Developer documentation
    qubes-plugins
    qubes-plugins
    qubes-ext
    qubes-ext
    qubes-log
    qubes-log
+   qubes-tests
    qubes-dochelpers
    qubes-dochelpers
 
 
 Indices and tables
 Indices and tables

+ 128 - 0
doc/qubes-tests.rst

@@ -0,0 +1,128 @@
+: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.
+
+FIXME: where are placed integration tests?
+
+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.
+
+When writing test, you should think about order of execution. Tests should be
+written bottom-to-top, that is, tests 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. This is the reason of numbers in names of the classes and test
+methods.
+
+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
+
+
+Module contents
+---------------
+
+.. automodule:: qubes.tests
+   :members:
+   :show-inheritance:
+
+.. vim: ts=3 sw=3 et tw=80

+ 7 - 3
qubes/tests/__init__.py

@@ -25,6 +25,7 @@ def skipUnlessDom0(test_item):
     Some tests (especially integration tests) have to be run in more or less
     Some tests (especially integration tests) have to be run in more or less
     working dom0. This is checked by connecting to libvirt.
     working dom0. This is checked by connecting to libvirt.
     '''
     '''
+
     return unittest.skipUnless(in_dom0, 'outside dom0')(test_item)
     return unittest.skipUnless(in_dom0, 'outside dom0')(test_item)
 
 
 
 
@@ -44,6 +45,7 @@ class TestEmitter(qubes.events.Emitter):
     >>> emitter.fired_events
     >>> emitter.fired_events
     Counter({('event', (1, 2, 3), (('foo', 'bar'), ('spam', 'eggs'))): 1})
     Counter({('event', (1, 2, 3), (('foo', 'bar'), ('spam', 'eggs'))): 1})
     '''
     '''
+
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         super(TestEmitter, self).__init__(*args, **kwargs)
         super(TestEmitter, self).__init__(*args, **kwargs)
 
 
@@ -61,8 +63,8 @@ class TestEmitter(qubes.events.Emitter):
 
 
 class QubesTestCase(unittest.TestCase):
 class QubesTestCase(unittest.TestCase):
     '''Base class for Qubes unit tests.
     '''Base class for Qubes unit tests.
-
     '''
     '''
+
     def __str__(self):
     def __str__(self):
         return '{}/{}/{}'.format(
         return '{}/{}/{}'.format(
             '.'.join(self.__class__.__module__.split('.')[2:]),
             '.'.join(self.__class__.__module__.split('.')[2:]),
@@ -74,7 +76,8 @@ class QubesTestCase(unittest.TestCase):
         '''Check whether event was fired on given emitter and fail if it did
         '''Check whether event was fired on given emitter and fail if it did
         not.
         not.
 
 
-        :param TestEmitter emitter: emitter which is being checked
+        :param emitter: emitter which is being checked
+        :type emitter: :py:class:`TestEmitter`
         :param str event: event identifier
         :param str event: event identifier
         :param list args: when given, all items must appear in args passed to event
         :param list args: when given, all items must appear in args passed to event
         :param list kwargs: when given, all items must appear in kwargs passed to event
         :param list kwargs: when given, all items must appear in kwargs passed to event
@@ -96,7 +99,8 @@ class QubesTestCase(unittest.TestCase):
     def assertEventNotFired(self, emitter, event, args=[], kwargs=[]):
     def assertEventNotFired(self, emitter, event, args=[], kwargs=[]):
         '''Check whether event was fired on given emitter. Fail if it did.
         '''Check whether event was fired on given emitter. Fail if it did.
 
 
-        :param TestEmitter emitter: emitter which is being checked
+        :param emitter: emitter which is being checked
+        :type emitter: :py:class:`TestEmitter`
         :param str event: event identifier
         :param str event: event identifier
         :param list args: when given, all items must appear in args passed to event
         :param list args: when given, all items must appear in args passed to event
         :param list kwargs: when given, all items must appear in kwargs passed to event
         :param list kwargs: when given, all items must appear in kwargs passed to event