doc/book/en/development/testing.rst
author Sylvain Thénault <sylvain.thenault@logilab.fr>
Thu, 15 Apr 2010 12:46:57 +0200
changeset 5271 94e16453a640
parent 5229 67dbd07a05f3
child 5255 15ea2f3464a4
permissions -rw-r--r--
[dbapi] fix user handling on dbapi request. Avoid getting None as _user and remove the need for a property.

.. -*- coding: utf-8 -*-

Tests
=====

.. toctree::
   :maxdepth: 1

Unit tests
----------

The *CubicWeb* framework provides the `CubicWebTC` test base class in
the module `cubicweb.devtools.testlib`.

Tests shall be put into the mycube/test directory. Additional test
data shall go into mycube/test/data.

It is much advised to write tests concerning entities methods, hooks
and operations, security. The CubicWebTC base class has convenience
methods to help test all of this.

.. note::

  In the realm of views, there is not much to do but check that the
  views are valid XHTML.  See :ref:`automatic_views_tests` for
  details. Integration of CubicWeb tests with UI testing tools such as
  `selenium`_ are currently under invesitgation.

.. _selenium: http://seleniumhq.org/projects/ide/

Most unit tests need a live database to work against. This is achieved
by CubicWeb using automatically sqlite (bundled with Python, see
http://docs.python.org/library/sqlite3.html) as a backend.

The database is stored in the mycube/test/tmpdb,
mycube/test/tmpdb-template files. If it does not (yet) exists, it will
be built automatically when the test suit starts.

.. warning::

  Whenever the schema changes (new entities, attributes, relations)
  one must delete these two files. Changes concerned only with entity
  or relation type properties (constraints, cardinalities,
  permissions) and generally dealt with using the
  `sync_schema_props_perms()` fonction of the migration environment
  need not a database regeneration step.

Unit test by example
````````````````````

We start with an example extracted from the keyword cube (available
from http://www.cubicweb.org/project/cubicweb-keyword).

.. sourcecode:: python

    from cubicweb.devtools.testlib import CubicWebTC
    from cubicweb import ValidationError

    class ClassificationHooksTC(CubicWebTC):

        def setup_database(self):
            req = self.request()
            group_etype = req.execute('Any X WHERE X name "CWGroup"').get_entity(0,0)
            c1 = req.create_entity('Classification', name=u'classif1',
                                   classifies=group_etype)
            user_etype = req.execute('Any X WHERE X name "CWUser"').get_entity(0,0)
            c2 = req.create_entity('Classification', name=u'classif2',
                                   classifies=user_etype)
            self.kw1 = req.create_entity('Keyword', name=u'kwgroup', included_in=c1)
            self.kw2 = req.create_entity('Keyword', name=u'kwuser', included_in=c2)

        def test_cannot_create_cycles(self):
            # direct obvious cycle
            self.assertRaises(ValidationError, self.kw1.set_relations,
                              subkeyword_of=self.kw1)
            # testing indirect cycles
            kw3 = self.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, '
                               'SK subkeyword_of K WHERE C name "classif1", K eid %s'
                               % self.kw1.eid).get_entity(0,0)
            self.kw1.set_relations(subkeyword_of=kw3)
            self.assertRaises(ValidationError, self.commit)

The test class defines a `setup_database` method which populates the
database with initial data. Each test of the class runs with this
pre-populated database.

The test case itself checks that an Operation does it job of
preventing cycles amongst Keyword entities.


XXX ref to advanced use case
XXX apycot plug

Managing connections or users
+++++++++++++++++++++++++++++

Since unit tests are done with the SQLITE backend and this does not
support multiple connections at a time, you must be careful when
simulating security, changing users.

By default, tests run with a user with admin privileges. This
user/connection must never be closed.

Before a self.login, one has to release the connection pool in use
with a self.commit, self.rollback or self.close.

When one is logged in as a normal user and wants to switch back to the
admin user, one has to use self.restore_connection().

Usually it looks like this:

.. sourcecode:: python

    # execute using default admin connection
    self.execute(...)
    # I want to login with another user, ensure to free admin connection pool
    # (could have used rollback but not close here, we should never close defaut admin connection)
    self.commit()
    cnx = self.login('user')
    # execute using user connection
    self.execute(...)
    # I want to login with another user or with admin user
    self.commit();  cnx.close()
    # restore admin connection, never use cnx = self.login('admin'), it will return
    # the default admin connection and one may be tempted to close it
    self.restore_connection()

.. warning::

   Do not use the references kept to the entities created with a
   connection from another !

XXX the new context manager ?

Email notifications tests
-------------------------

When running tests potentially generated e-mails are not really sent
but is found in the list `MAILBOX` of module
`cubicweb.devtools.testlib`.

You can test your notifications by analyzing the contents of this list, which
contains objects with two attributes:

* `recipients`, the list of recipients
* `msg`, object email.Message

Let us look at simple example from the ``blog`` cube.

.. sourcecode:: python

    from cubicweb.devtools.testlib import CubicWebTC, MAILBOX

    class BlogTestsCubicWebTC(CubicWebTC):
        """test blog specific behaviours"""

        def test_notifications(self):
            req = self.request()
            cubicweb_blog = req.create_entity('Blog', title=u'cubicweb',
                                description=u'cubicweb is beautiful')
            blog_entry_1 = req.create_entity('BlogEntry', title=u'hop',
                                             content=u'cubicweb hop')
            blog_entry_1.set_relations(entry_of=cubicweb_blog)
            blog_entry_2 = req.create_entity('BlogEntry', title=u'yes',
                                             content=u'cubicweb yes')
            blog_entry_2.set_relations(entry_of=cubicweb_blog)
            self.assertEquals(len(MAILBOX), 0)
            self.commit()
            self.assertEquals(len(MAILBOX), 2)
            mail = MAILBOX[0]
            self.assertEquals(mail.subject, '[data] hop')
            mail = MAILBOX[1]
            self.assertEquals(mail.subject, '[data] yes')

.. _automatic_views_tests:

Automatic views testing
-----------------------

This is done automatically with the AutomaticWebTest class. At cube
creation time, the mycube/test/test_mycube.py file contains such a
test. The code here has to be uncommented to be usable, without
further modification.

XXX more to come


Using Pytest
````````````

.. automodule:: logilab.common.testlib
.. autoclass:: logilab.common.testlib.TestCase
   :members:

XXX pytestconf.py & options (e.g --source to use a different db
backend than sqlite)

CubicWebTC API
``````````````
.. autoclass:: cubicweb.devtools.testlib.CubicWebTC
   :members: