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 Automatic views testing for details. Integration of CubicWeb tests with UI testing tools such as selenium are currently under invesitgation.
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.
We start with an example extracted from the keyword cube (available from http://www.cubicweb.org/project/cubicweb-keyword).
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.
You can see an example of security tests in the Step 1: configuring security into the schema.
It is possible to have these tests run continuously using apycot.
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.
The login method returns a connection object that can be used as a context manager:
with self.login('user1') as user:
req = user.req
req.execute(...)
On exit of the context manager, either a commit or rollback is issued, which releases the connection.
When one is logged in as a normal user and wants to switch back to the admin user without committing, one has to use self.restore_connection().
Usage with restore_connection:
# 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 !
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:
Let us look at simple example from the blog cube.
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')
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.
The auto_populate method uses a smart algorithm to create pseudo-random data in the database, thus enabling the views to be invoked and tested.
Depending on the schema, hooks and operations constraints, it is not always possible for the automatic auto_populate to proceed.
It is possible of course to completely redefine auto_populate. A lighter solution is to give hints (fill some class attributes) about what entities and relations have to be skipped by the auto_populate mechanism. These are:
The CubicWebTC class uses the cubicweb.devtools.ApptestConfiguration configuration class to setup its testing environment (database driver, user password, application home, and so on). The cubicweb.devtools module also provides a RealDatabaseConfiguration class that will read a regular cubicweb sources file to fetch all this information but will also prevent the database to be initalized and reset between tests.
For a test class to use a specific configuration, you have to set the _config class attribute on the class as in:
from cubicweb.devtools import RealDatabaseConfiguration
from cubicweb.devtools.testlib import CubicWebTC
class BlogRealDatabaseTC(CubicWebTC):
_config = RealDatabaseConfiguration('blog',
sourcefile='/path/to/realdb_sources')
def test_blog_rss(self):
req = self.request()
rset = req.execute('Any B ORDERBY D DESC WHERE B is BlogEntry, '
'B created_by U, U login "logilab", B creation_date D')
self.view('rss', rset)
Sometimes a small component cannot be tested all by itself, so one needs to specify other cubes to be used as part of the the unit test suite. This is handled by the bootstrap_cubes file located under mycube\test\data. One example from the preview cube:
card, file, preview
The format is:
The pytest utility (shipping with logilab-common, which is a mandatory dependency of CubicWeb) extends the Python unittest functionality and is the preferred way to run the CubicWeb test suites. Bare unittests also work the usual way.
To use it, you may:
Additionally, the -x option tells pytest to exit at the first error or failure. The -i option tells pytest to drop into pdb whenever an exception occurs in a test.
When the -x option has been used and the run stopped on a test, it is possible, after having fixed the test, to relaunch pytest with the -R option to tell it to start testing again from where it previously failed.
The base class of CubicWebTC is logilab.common.testlib.TestCase, which provides a lot of convenient assertion methods.
A unittest.TestCase extension with some additional methods.
compares two dicts
If the two dict differ, the first difference is shown in the error message :param dict1: a Python Dictionary :param dict2: a Python Dictionary :param msg: custom message (String) in case of failure
compares two dicts
If the two dict differ, the first difference is shown in the error message :param dict1: a Python Dictionary :param dict2: a Python Dictionary :param msg: custom message (String) in case of failure
compares if two floats have a distance smaller than expected precision.
| Parameters: |
|
|---|
assert <object> is in <set>
| Parameters: |
|
|---|
compares identity of two reference
| Parameters: |
|
|---|
check if an object is an instance of a class
| Parameters: |
|
|---|
compare two strings and assert that the text lines of the strings are equal.
| Parameters: |
|
|---|
compare two strings and assert that the text lines of the strings are equal.
| Parameters: |
|
|---|
compares two lists
If the two list differ, the first difference is shown in the error message
| Parameters: |
|
|---|
compares two lists
If the two list differ, the first difference is shown in the error message
| Parameters: |
|
|---|
assert obj is None
| Parameter: | obj – Python Object to be tested |
|---|
assert <object> is not in <set>
| Parameters: |
|
|---|
override default failUnlessRaise method to return the raised exception instance.
Fail unless an exception of class excClass is thrown by callableObj when invoked with arguments args and keyword arguments kwargs. If a different type of exception is thrown, it will not be caught, and the test case will be deemed to have suffered an error, exactly as for an unexpected exception.
| Parameters: |
|
|---|
compares two sets and shows difference between both
Don’t use it for iterables other than sets.
| Parameters: |
|
|---|
compares two sets and shows difference between both
Don’t use it for iterables other than sets.
| Parameters: |
|
|---|
compare two multiline strings (using difflib and splitlines())
| Parameters: |
|
|---|
compare two multiline strings (using difflib and splitlines())
| Parameters: |
|
|---|
compares two iterable and shows difference between both
| Parameters: |
|
|---|
compares two iterable and shows difference between both
| Parameters: |
|
|---|
compares two iterable and shows difference between both
| Parameters: |
|
|---|
compares two iterable and shows difference between both
| Parameters: |
|
|---|
asserts the XML string is well-formed (no DTD conformance check)
| Parameter: | context – number of context lines in standard message (show all data if negative). Only available with element tree |
|---|
asserts the XML stream is well-formed (no DTD conformance check)
| Parameter: | context – number of context lines in standard message (show all data if negative). Only available with element tree |
|---|
override default failUnlessRaise method to return the raised exception instance.
Fail unless an exception of class excClass is thrown by callableObj when invoked with arguments args and keyword arguments kwargs. If a different type of exception is thrown, it will not be caught, and the test case will be deemed to have suffered an error, exactly as for an unexpected exception.
| Parameters: |
|
|---|
abstract class for test using an apptest environment
attributes:
This method tests the view vid on rset using template
If no error occurred while rendering the view, the HTML is analyzed and parsed.
| Returns: | an instance of cubicweb.devtools.htmlparser.PageInfo encapsulation the generated HTML |
|---|