doc/book/en/development/testing.rst
branchstable
changeset 5394 105011657405
parent 5393 875bdc0fe8ce
child 5395 e0ab7433e640
equal deleted inserted replaced
5393:875bdc0fe8ce 5394:105011657405
     1 .. -*- coding: utf-8 -*-
       
     2 
       
     3 Tests
       
     4 =====
       
     5 
       
     6 Unit tests
       
     7 ----------
       
     8 
       
     9 The *CubicWeb* framework provides the `CubicWebTC` test base class in
       
    10 the module `cubicweb.devtools.testlib`.
       
    11 
       
    12 Tests shall be put into the mycube/test directory. Additional test
       
    13 data shall go into mycube/test/data.
       
    14 
       
    15 It is much advised to write tests concerning entities methods, hooks
       
    16 and operations, security. The CubicWebTC base class has convenience
       
    17 methods to help test all of this.
       
    18 
       
    19 .. note::
       
    20 
       
    21   In the realm of views, there is not much to do but check that the
       
    22   views are valid XHTML.  See :ref:`automatic_views_tests` for
       
    23   details. Integration of CubicWeb tests with UI testing tools such as
       
    24   `selenium`_ are currently under invesitgation.
       
    25 
       
    26 .. _selenium: http://seleniumhq.org/projects/ide/
       
    27 
       
    28 Most unit tests need a live database to work against. This is achieved
       
    29 by CubicWeb using automatically sqlite (bundled with Python, see
       
    30 http://docs.python.org/library/sqlite3.html) as a backend.
       
    31 
       
    32 The database is stored in the mycube/test/tmpdb,
       
    33 mycube/test/tmpdb-template files. If it does not (yet) exists, it will
       
    34 be built automatically when the test suit starts.
       
    35 
       
    36 .. warning::
       
    37 
       
    38   Whenever the schema changes (new entities, attributes, relations)
       
    39   one must delete these two files. Changes concerned only with entity
       
    40   or relation type properties (constraints, cardinalities,
       
    41   permissions) and generally dealt with using the
       
    42   `sync_schema_props_perms()` fonction of the migration environment
       
    43   need not a database regeneration step.
       
    44 
       
    45 Unit test by example
       
    46 ````````````````````
       
    47 
       
    48 We start with an example extracted from the keyword cube (available
       
    49 from http://www.cubicweb.org/project/cubicweb-keyword).
       
    50 
       
    51 .. sourcecode:: python
       
    52 
       
    53     from cubicweb.devtools.testlib import CubicWebTC
       
    54     from cubicweb import ValidationError
       
    55 
       
    56     class ClassificationHooksTC(CubicWebTC):
       
    57 
       
    58         def setup_database(self):
       
    59             req = self.request()
       
    60             group_etype = req.execute('Any X WHERE X name "CWGroup"').get_entity(0,0)
       
    61             c1 = req.create_entity('Classification', name=u'classif1',
       
    62                                    classifies=group_etype)
       
    63             user_etype = req.execute('Any X WHERE X name "CWUser"').get_entity(0,0)
       
    64             c2 = req.create_entity('Classification', name=u'classif2',
       
    65                                    classifies=user_etype)
       
    66             self.kw1 = req.create_entity('Keyword', name=u'kwgroup', included_in=c1)
       
    67             self.kw2 = req.create_entity('Keyword', name=u'kwuser', included_in=c2)
       
    68 
       
    69         def test_cannot_create_cycles(self):
       
    70             # direct obvious cycle
       
    71             self.assertRaises(ValidationError, self.kw1.set_relations,
       
    72                               subkeyword_of=self.kw1)
       
    73             # testing indirect cycles
       
    74             kw3 = self.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, '
       
    75                                'SK subkeyword_of K WHERE C name "classif1", K eid %s'
       
    76                                % self.kw1.eid).get_entity(0,0)
       
    77             self.kw1.set_relations(subkeyword_of=kw3)
       
    78             self.assertRaises(ValidationError, self.commit)
       
    79 
       
    80 The test class defines a `setup_database` method which populates the
       
    81 database with initial data. Each test of the class runs with this
       
    82 pre-populated database.
       
    83 
       
    84 The test case itself checks that an Operation does it job of
       
    85 preventing cycles amongst Keyword entities.
       
    86 
       
    87 You can see an example of security tests in the
       
    88 :ref:`adv_tuto_security`.
       
    89 
       
    90 It is possible to have these tests run continuously using `apycot`_.
       
    91 
       
    92 .. _apycot: http://www.logilab.org/project/apycot
       
    93 
       
    94 Managing connections or users
       
    95 +++++++++++++++++++++++++++++
       
    96 
       
    97 Since unit tests are done with the SQLITE backend and this does not
       
    98 support multiple connections at a time, you must be careful when
       
    99 simulating security, changing users.
       
   100 
       
   101 By default, tests run with a user with admin privileges. This
       
   102 user/connection must never be closed.
       
   103 
       
   104 Before a self.login, one has to release the connection pool in use
       
   105 with a self.commit, self.rollback or self.close.
       
   106 
       
   107 The `login` method returns a connection object that can be used as a
       
   108 context manager:
       
   109 
       
   110 .. sourcecode:: python
       
   111 
       
   112    with self.login('user1') as user:
       
   113        req = user.req
       
   114        req.execute(...)
       
   115 
       
   116 On exit of the context manager, either a commit or rollback is issued,
       
   117 which releases the connection.
       
   118 
       
   119 When one is logged in as a normal user and wants to switch back to the
       
   120 admin user without committing, one has to use
       
   121 self.restore_connection().
       
   122 
       
   123 Usage with restore_connection:
       
   124 
       
   125 .. sourcecode:: python
       
   126 
       
   127     # execute using default admin connection
       
   128     self.execute(...)
       
   129     # I want to login with another user, ensure to free admin connection pool
       
   130     # (could have used rollback but not close here
       
   131     # we should never close defaut admin connection)
       
   132     self.commit()
       
   133     cnx = self.login('user')
       
   134     # execute using user connection
       
   135     self.execute(...)
       
   136     # I want to login with another user or with admin user
       
   137     self.commit();  cnx.close()
       
   138     # restore admin connection, never use cnx = self.login('admin'), it will return
       
   139     # the default admin connection and one may be tempted to close it
       
   140     self.restore_connection()
       
   141 
       
   142 .. warning::
       
   143 
       
   144    Do not use the references kept to the entities created with a
       
   145    connection from another !
       
   146 
       
   147 Email notifications tests
       
   148 -------------------------
       
   149 
       
   150 When running tests potentially generated e-mails are not really sent
       
   151 but is found in the list `MAILBOX` of module
       
   152 `cubicweb.devtools.testlib`.
       
   153 
       
   154 You can test your notifications by analyzing the contents of this list, which
       
   155 contains objects with two attributes:
       
   156 
       
   157 * `recipients`, the list of recipients
       
   158 * `msg`, object email.Message
       
   159 
       
   160 Let us look at simple example from the ``blog`` cube.
       
   161 
       
   162 .. sourcecode:: python
       
   163 
       
   164     from cubicweb.devtools.testlib import CubicWebTC, MAILBOX
       
   165 
       
   166     class BlogTestsCubicWebTC(CubicWebTC):
       
   167         """test blog specific behaviours"""
       
   168 
       
   169         def test_notifications(self):
       
   170             req = self.request()
       
   171             cubicweb_blog = req.create_entity('Blog', title=u'cubicweb',
       
   172                                 description=u'cubicweb is beautiful')
       
   173             blog_entry_1 = req.create_entity('BlogEntry', title=u'hop',
       
   174                                              content=u'cubicweb hop')
       
   175             blog_entry_1.set_relations(entry_of=cubicweb_blog)
       
   176             blog_entry_2 = req.create_entity('BlogEntry', title=u'yes',
       
   177                                              content=u'cubicweb yes')
       
   178             blog_entry_2.set_relations(entry_of=cubicweb_blog)
       
   179             self.assertEquals(len(MAILBOX), 0)
       
   180             self.commit()
       
   181             self.assertEquals(len(MAILBOX), 2)
       
   182             mail = MAILBOX[0]
       
   183             self.assertEquals(mail.subject, '[data] hop')
       
   184             mail = MAILBOX[1]
       
   185             self.assertEquals(mail.subject, '[data] yes')
       
   186 
       
   187 .. _automatic_views_tests:
       
   188 
       
   189 Automatic views testing
       
   190 -----------------------
       
   191 
       
   192 This is done automatically with the AutomaticWebTest class. At cube
       
   193 creation time, the mycube/test/test_mycube.py file contains such a
       
   194 test. The code here has to be uncommented to be usable, without
       
   195 further modification.
       
   196 
       
   197 The ``auto_populate`` method uses a smart algorithm to create
       
   198 pseudo-random data in the database, thus enabling the views to be
       
   199 invoked and tested.
       
   200 
       
   201 Depending on the schema, hooks and operations constraints, it is not
       
   202 always possible for the automatic auto_populate to proceed.
       
   203 
       
   204 It is possible of course to completely redefine auto_populate. A
       
   205 lighter solution is to give hints (fill some class attributes) about
       
   206 what entities and relations have to be skipped by the auto_populate
       
   207 mechanism. These are:
       
   208 
       
   209 * `no_auto_populate`, may contain a list of entity types to skip
       
   210 * `ignored_relations`, may contain a list of relation types to skip
       
   211 * `application_rql`, may contain a list of rql expressions that
       
   212   auto_populate cannot guess by itself; these must yield resultsets
       
   213   against which views may be selected.
       
   214 
       
   215 
       
   216 Test APIS
       
   217 ---------
       
   218 
       
   219 Using Pytest
       
   220 ````````````
       
   221 
       
   222 The `pytest` utility (shipping with `logilab-common`_, which is a
       
   223 mandatory dependency of CubicWeb) extends the Python unittest
       
   224 functionality and is the preferred way to run the CubicWeb test
       
   225 suites. Bare unittests also work the usual way.
       
   226 
       
   227 .. _logilab-common: http://www.logilab.org/project/logilab-common
       
   228 
       
   229 To use it, you may:
       
   230 
       
   231 * just launch `pytest` in your cube to execute all tests (it will
       
   232   discover them automatically)
       
   233 * launch `pytest unittest_foo.py` to execute one test file
       
   234 * launch `pytest unittest_foo.py bar` to execute all test methods and
       
   235   all test cases whose name contain `bar`
       
   236 
       
   237 Additionally, the `-x` option tells pytest to exit at the first error
       
   238 or failure. The `-i` option tells pytest to drop into pdb whenever an
       
   239 exception occurs in a test.
       
   240 
       
   241 When the `-x` option has been used and the run stopped on a test, it
       
   242 is possible, after having fixed the test, to relaunch pytest with the
       
   243 `-R` option to tell it to start testing again from where it previously
       
   244 failed.
       
   245 
       
   246 Using the `TestCase` base class
       
   247 ```````````````````````````````
       
   248 
       
   249 The base class of CubicWebTC is logilab.common.testlib.TestCase, which
       
   250 provides a lot of convenient assertion methods.
       
   251 
       
   252 .. autoclass:: logilab.common.testlib.TestCase
       
   253    :members:
       
   254 
       
   255 CubicWebTC API
       
   256 ``````````````
       
   257 .. autoclass:: cubicweb.devtools.testlib.CubicWebTC
       
   258    :members: