doc/book/en/devrepo/testing.rst
changeset 9878 f3936f64bd98
parent 9864 f60a80592224
child 9880 9adf36ce805e
equal deleted inserted replaced
9877:4a604b6e3067 9878:f3936f64bd98
    51     from cubicweb import ValidationError
    51     from cubicweb import ValidationError
    52 
    52 
    53     class ClassificationHooksTC(CubicWebTC):
    53     class ClassificationHooksTC(CubicWebTC):
    54 
    54 
    55         def setup_database(self):
    55         def setup_database(self):
    56             req = self.request()
    56             with self.admin_access.repo_cnx() as cnx:
    57             group_etype = req.find_one_entity('CWEType', name='CWGroup')
    57                 group_etype = cnx.find('CWEType', name='CWGroup').one()
    58             c1 = req.create_entity('Classification', name=u'classif1',
    58                 c1 = cnx.create_entity('Classification', name=u'classif1',
    59                                    classifies=group_etype)
    59                                        classifies=group_etype)
    60             user_etype = req.find_one_entity('CWEType', name='CWUser')
    60                 user_etype = cnx.find('CWEType', name='CWUser').one()
    61             c2 = req.create_entity('Classification', name=u'classif2',
    61                 c2 = cnx.create_entity('Classification', name=u'classif2',
    62                                    classifies=user_etype)
    62                                        classifies=user_etype)
    63             self.kw1 = req.create_entity('Keyword', name=u'kwgroup', included_in=c1)
    63                 self.kw1eid = cnx.create_entity('Keyword', name=u'kwgroup', included_in=c1).eid
    64             self.kw2 = req.create_entity('Keyword', name=u'kwuser', included_in=c2)
    64                 cnx.commit()
    65 
    65 
    66         def test_cannot_create_cycles(self):
    66         def test_cannot_create_cycles(self):
    67             # direct obvious cycle
    67             with self.admin_access.repo_cnx() as cnx:
    68             self.assertRaises(ValidationError, self.kw1.cw_set,
    68                 kw1 = cnx.entity_from_eid(self.kw1eid)
    69                               subkeyword_of=self.kw1)
    69                 # direct obvious cycle
    70             # testing indirect cycles
    70                 with self.assertRaises(ValidationError):
    71             kw3 = self.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, '
    71                     kw1.cw_set(subkeyword_of=kw1)
    72                                'SK subkeyword_of K WHERE C name "classif1", K eid %s'
    72                 cnx.rollback()
    73                                % self.kw1.eid).get_entity(0,0)
    73                 # testing indirect cycles
    74             self.kw1.cw_set(subkeyword_of=kw3)
    74                 kw3 = cnx.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, '
    75             self.assertRaises(ValidationError, self.commit)
    75                                   'SK subkeyword_of K WHERE C name "classif1", K eid %(k)s'
       
    76                                   {'k': kw1}).get_entity(0,0)
       
    77                 kw3.cw_set(reverse_subkeyword_of=kw1)
       
    78                 self.assertRaises(ValidationError, cnx.commit)
    76 
    79 
    77 The test class defines a :meth:`setup_database` method which populates the
    80 The test class defines a :meth:`setup_database` method which populates the
    78 database with initial data. Each test of the class runs with this
    81 database with initial data. Each test of the class runs with this
    79 pre-populated database. A commit is done automatically after the
    82 pre-populated database.
    80 :meth:`setup_database` call. You don't have to call it explicitely.
    83 
    81 
    84 The test case itself checks that an Operation does its job of
    82 The test case itself checks that an Operation does it job of
       
    83 preventing cycles amongst Keyword entities.
    85 preventing cycles amongst Keyword entities.
    84 
    86 
    85 `create_entity` is a useful method, which easily allows to create an
    87 The `create_entity` method of connection (or request) objects allows
    86 entity. You can link this entity to others entities, by specifying as
    88 to create an entity. You can link this entity to others entities, by
    87 argument, the relation name, and the entity to link, as value. In the
    89 specifying as argument, the relation name, and the entity to link, as
    88 above example, the `Classification` entity is linked to a `CWEtype`
    90 value. In the above example, the `Classification` entity is linked to
    89 via the relation `classifies`. Conversely, if you are creating a
    91 a `CWEtype` via the relation `classifies`. Conversely, if you are
    90 `CWEtype` entity, you can link it to a `Classification` entity, by
    92 creating a `CWEtype` entity, you can link it to a `Classification`
    91 adding `reverse_classifies` as argument.
    93 entity, by adding `reverse_classifies` as argument.
    92 
    94 
    93 .. note::
    95 .. note::
    94 
    96 
    95    :meth:`commit` method is not called automatically in test_XXX
    97    the :meth:`commit` method is not called automatically. You have to
    96    methods. You have to call it explicitely if needed (notably to test
    98    call it explicitely if needed (notably to test operations). It is a
    97    operations). It is a good practice to call :meth:`clear_all_caches`
    99    good practice to regenerate entities with :meth:`entity_from_eid`
    98    on entities after a commit to avoid request cache effects.
   100    after a commit to avoid request cache effects.
    99 
   101 
   100 You can see an example of security tests in the
   102 You can see an example of security tests in the
   101 :ref:`adv_tuto_security`.
   103 :ref:`adv_tuto_security`.
   102 
   104 
   103 It is possible to have these tests run continuously using `apycot`_.
   105 It is possible to have these tests run continuously using `apycot`_.
   112 Since unit tests are done with the SQLITE backend and this does not
   114 Since unit tests are done with the SQLITE backend and this does not
   113 support multiple connections at a time, you must be careful when
   115 support multiple connections at a time, you must be careful when
   114 simulating security, changing users.
   116 simulating security, changing users.
   115 
   117 
   116 By default, tests run with a user with admin privileges. This
   118 By default, tests run with a user with admin privileges. This
   117 user/connection must never be closed.
   119 user/connection must never be closed. It is accessible through the
   118 
   120 `admin_access` object of the test classes.
   119 Before a self.login, one has to release the connection pool in use
   121 
   120 with a self.commit, self.rollback or self.close.
   122 The `repo_cnx()` method returns a connection object that can be used as a
   121 
       
   122 The `login` method returns a connection object that can be used as a
       
   123 context manager:
   123 context manager:
   124 
   124 
   125 .. sourcecode:: python
   125 .. sourcecode:: python
   126 
   126 
   127    with self.login('user1') as user:
   127    # admin_access is a pre-cooked session wrapping object
   128        req = user.req
   128    # it is built with:
       
   129    # self.admin_access = self.new_access('admin')
       
   130    with self.admin_access.repo_cnx() as cnx:
       
   131        cnx.execute(...)
       
   132        self.create_user(cnx, login='user1')
       
   133        cnx.commit()
       
   134 
       
   135    user1access = self.new_access('user1')
       
   136    with user1access.web_request() as req:
   129        req.execute(...)
   137        req.execute(...)
   130 
   138        req.cnx.commit()
   131 On exit of the context manager, either a commit or rollback is issued,
   139 
   132 which releases the connection.
   140 On exit of the context manager, a rollback is issued, which releases
   133 
   141 the connection. Don't forget to issue the `cnx.commit()` calls !
   134 When one is logged in as a normal user and wants to switch back to the
       
   135 admin user without committing, one has to use
       
   136 self.restore_connection().
       
   137 
       
   138 Usage with restore_connection:
       
   139 
       
   140 .. sourcecode:: python
       
   141 
       
   142     # execute using default admin connection
       
   143     self.execute(...)
       
   144     # I want to login with another user, ensure to free admin connection pool
       
   145     # (could have used rollback but not close here
       
   146     # we should never close defaut admin connection)
       
   147     self.commit()
       
   148     cnx = self.login('user')
       
   149     # execute using user connection
       
   150     self.execute(...)
       
   151     # I want to login with another user or with admin user
       
   152     self.commit();  cnx.close()
       
   153     # restore admin connection, never use cnx = self.login('admin'), it will return
       
   154     # the default admin connection and one may be tempted to close it
       
   155     self.restore_connection()
       
   156 
   142 
   157 .. warning::
   143 .. warning::
   158 
   144 
   159    Do not use the references kept to the entities created with a
   145    Do not use the references kept to the entities created with a
   160    connection from another !
   146    connection from another !
   180 
   166 
   181     class BlogTestsCubicWebTC(CubicWebTC):
   167     class BlogTestsCubicWebTC(CubicWebTC):
   182         """test blog specific behaviours"""
   168         """test blog specific behaviours"""
   183 
   169 
   184         def test_notifications(self):
   170         def test_notifications(self):
   185             req = self.request()
   171             with self.admin_access.web_request() as req:
   186             cubicweb_blog = req.create_entity('Blog', title=u'cubicweb',
   172                 cubicweb_blog = req.create_entity('Blog', title=u'cubicweb',
   187                                 description=u'cubicweb is beautiful')
   173                                     description=u'cubicweb is beautiful')
   188             blog_entry_1 = req.create_entity('BlogEntry', title=u'hop',
   174                 blog_entry_1 = req.create_entity('BlogEntry', title=u'hop',
   189                                              content=u'cubicweb hop')
   175                                                  content=u'cubicweb hop')
   190             blog_entry_1.cw_set(entry_of=cubicweb_blog)
   176                 blog_entry_1.cw_set(entry_of=cubicweb_blog)
   191             blog_entry_2 = req.create_entity('BlogEntry', title=u'yes',
   177                 blog_entry_2 = req.create_entity('BlogEntry', title=u'yes',
   192                                              content=u'cubicweb yes')
   178                                                  content=u'cubicweb yes')
   193             blog_entry_2.cw_set(entry_of=cubicweb_blog)
   179                 blog_entry_2.cw_set(entry_of=cubicweb_blog)
   194             self.assertEqual(len(MAILBOX), 0)
   180                 self.assertEqual(len(MAILBOX), 0)
   195             self.commit()
   181                 req.cnx.commit()
   196             self.assertEqual(len(MAILBOX), 2)
   182                 self.assertEqual(len(MAILBOX), 2)
   197             mail = MAILBOX[0]
   183                 mail = MAILBOX[0]
   198             self.assertEqual(mail.subject, '[data] hop')
   184                 self.assertEqual(mail.subject, '[data] hop')
   199             mail = MAILBOX[1]
   185                 mail = MAILBOX[1]
   200             self.assertEqual(mail.subject, '[data] yes')
   186                 self.assertEqual(mail.subject, '[data] yes')
   201 
   187 
   202 Visible actions tests
   188 Visible actions tests
   203 `````````````````````
   189 `````````````````````
   204 
   190 
   205 It is easy to write unit tests to test actions which are visible to
   191 It is easy to write unit tests to test actions which are visible to
   210 .. sourcecode:: python
   196 .. sourcecode:: python
   211 
   197 
   212     class ConferenceActionsTC(CubicWebTC):
   198     class ConferenceActionsTC(CubicWebTC):
   213 
   199 
   214         def setup_database(self):
   200         def setup_database(self):
   215             self.conf = self.create_entity('Conference',
   201             with self.admin_access.repo_cnx() as cnx:
   216                                            title=u'my conf',
   202                 self.conf = cnx.create_entity('Conference',
   217                                            url_id=u'conf',
   203                                               title=u'my conf',
   218                                            start_on=date(2010, 1, 27),
   204                                               url_id=u'conf',
   219                                            end_on = date(2010, 1, 29),
   205                                               start_on=date(2010, 1, 27),
   220                                            call_open=True,
   206                                               end_on = date(2010, 1, 29),
   221                                            reverse_is_chair_at=chair,
   207                                               call_open=True,
   222                                            reverse_is_reviewer_at=reviewer)
   208                                               reverse_is_chair_at=chair,
       
   209                                               reverse_is_reviewer_at=reviewer)
   223 
   210 
   224         def test_admin(self):
   211         def test_admin(self):
   225             req = self.request()
   212             with self.admin_access.web_request() as req:
   226             rset = req.find_entities('Conference')
   213                 rset = req.find('Conference').one()
   227             self.assertListEqual(self.pactions(req, rset),
   214                 self.assertListEqual(self.pactions(req, rset),
   228                                   [('workflow', workflow.WorkflowActions),
   215                                       [('workflow', workflow.WorkflowActions),
   229                                    ('edit', confactions.ModifyAction),
   216                                        ('edit', confactions.ModifyAction),
   230                                    ('managepermission', actions.ManagePermissionsAction),
   217                                        ('managepermission', actions.ManagePermissionsAction),
   231                                    ('addrelated', actions.AddRelatedActions),
   218                                        ('addrelated', actions.AddRelatedActions),
   232                                    ('delete', actions.DeleteAction),
   219                                        ('delete', actions.DeleteAction),
   233                                    ('generate_badge_action', badges.GenerateBadgeAction),
   220                                        ('generate_badge_action', badges.GenerateBadgeAction),
   234                                    ('addtalkinconf', confactions.AddTalkInConferenceAction)
   221                                        ('addtalkinconf', confactions.AddTalkInConferenceAction)
   235                                    ])
   222                                        ])
   236             self.assertListEqual(self.action_submenu(req, rset, 'addrelated'),
   223                 self.assertListEqual(self.action_submenu(req, rset, 'addrelated'),
   237                                   [(u'add Track in_conf Conference object',
   224                                       [(u'add Track in_conf Conference object',
   238                                     u'http://testing.fr/cubicweb/add/Track'
   225                                         u'http://testing.fr/cubicweb/add/Track'
   239                                     u'?__linkto=in_conf%%3A%(conf)s%%3Asubject&'
   226                                         u'?__linkto=in_conf%%3A%(conf)s%%3Asubject&'
   240                                     u'__redirectpath=conference%%2Fconf&'
   227                                         u'__redirectpath=conference%%2Fconf&'
   241                                     u'__redirectvid=' % {'conf': self.conf.eid}),
   228                                         u'__redirectvid=' % {'conf': self.conf.eid}),
   242                                    ])
   229                                        ])
   243 
   230 
   244 You just have to execute a rql query corresponding to the view you want to test,
   231 You just have to execute a rql query corresponding to the view you want to test,
   245 and to compare the result of
   232 and to compare the result of
   246 :meth:`~cubicweb.devtools.testlib.CubicWebTC.pactions` with the list of actions
   233 :meth:`~cubicweb.devtools.testlib.CubicWebTC.pactions` with the list of actions
   247 that must be visible in the interface. This is a list of tuples. The first
   234 that must be visible in the interface. This is a list of tuples. The first
   288   namespace, else both your subclass *and* this parent class will be run.
   275   namespace, else both your subclass *and* this parent class will be run.
   289 
   276 
   290 Cache heavy database setup
   277 Cache heavy database setup
   291 -------------------------------
   278 -------------------------------
   292 
   279 
   293 Some tests suite require a complex setup of the database that takes seconds (or
   280 Some tests suite require a complex setup of the database that takes
   294 event minutes) to complete. Doing the whole setup for all individual tests make
   281 seconds (or event minutes) to complete. Doing the whole setup for all
   295 the whole run very slow. The ``CubicWebTC`` class offer a simple way to prepare
   282 individual tests make the whole run very slow. The ``CubicWebTC``
   296 specific database once for multiple tests. The `test_db_id` class attribute of
   283 class offer a simple way to prepare specific database once for
   297 your ``CubicWebTC`` must be set a unique identifier and the
   284 multiple tests. The `test_db_id` class attribute of your
   298 :meth:`pre_setup_database` class method build the cached content. As the
   285 ``CubicWebTC`` must be set a unique identifier and the
   299 :meth:`pre_setup_database` method is not grantee to be called, you must not set
   286 :meth:`pre_setup_database` class method build the cached content. As
   300 any class attribut to be used during test there.  Databases for each `test_db_id`
   287 the :meth:`pre_setup_database` method is not garantee to be called
   301 are automatically created if not already in cache.  Clearing the cache is up to
   288 every time a test method is run, you must not set any class attribute
   302 the user. Cache files are found in the :file:`data/database` subdirectory of your
   289 to be used during test *there*. Databases for each `test_db_id` are
   303 test directory.
   290 automatically created if not already in cache. Clearing the cache is
       
   291 up to the user. Cache files are found in the :file:`data/database`
       
   292 subdirectory of your test directory.
   304 
   293 
   305 .. warning::
   294 .. warning::
   306 
   295 
   307   Take care to always have the same :meth:`pre_setup_database` function for all
   296   Take care to always have the same :meth:`pre_setup_database`
   308   call with a given `test_db_id` otherwise you test will have unpredictable
   297   function for all calls with a given `test_db_id` otherwise your test
   309   result given the first encountered one.
   298   will have unpredictable results depending on the first encountered one.
       
   299 
   310 
   300 
   311 Testing on a real-life database
   301 Testing on a real-life database
   312 -------------------------------
   302 -------------------------------
   313 
   303 
   314 The ``CubicWebTC`` class uses the `cubicweb.devtools.ApptestConfiguration`
   304 The ``CubicWebTC`` class uses the `cubicweb.devtools.ApptestConfiguration`
   330     class BlogRealDatabaseTC(CubicWebTC):
   320     class BlogRealDatabaseTC(CubicWebTC):
   331         _config = RealDatabaseConfiguration('blog',
   321         _config = RealDatabaseConfiguration('blog',
   332                                             sourcefile='/path/to/realdb_sources')
   322                                             sourcefile='/path/to/realdb_sources')
   333 
   323 
   334         def test_blog_rss(self):
   324         def test_blog_rss(self):
   335             req = self.request()
   325             with self.admin_access.web_request() as req:
   336             rset = req.execute('Any B ORDERBY D DESC WHERE B is BlogEntry, '
   326             rset = req.execute('Any B ORDERBY D DESC WHERE B is BlogEntry, '
   337                 'B created_by U, U login "logilab", B creation_date D')
   327                                'B created_by U, U login "logilab", B creation_date D')
   338             self.view('rss', rset, req=req)
   328             self.view('rss', rset, req=req)
   339 
   329 
   340 
   330 
   341 Testing with other cubes
   331 Testing with other cubes
   342 ------------------------
   332 ------------------------
   472 * repository side: RQL query execution, that may trigger hooks and operation.
   462 * repository side: RQL query execution, that may trigger hooks and operation.
   473 
   463 
   474 The client interacts with the repository through a repoapi connection.
   464 The client interacts with the repository through a repoapi connection.
   475 
   465 
   476 
   466 
       
   467 .. note::
       
   468 
       
   469    These distinction are going to disappear in cubicweb 3.21 (if not
       
   470    before).
       
   471 
   477 A repoapi connection is tied to a session in the repository. The connection and
   472 A repoapi connection is tied to a session in the repository. The connection and
   478 request objects are unaccessible from repository code / the session object is
   473 request objects are unaccessible from repository code / the session object is
   479 unaccessible from client code (theoretically at least).
   474 unaccessible from client code (theoretically at least).
   480 
   475 
   481 The web interface provides a request class.  That `request` object provides
   476 The web interface provides a request class.  That `request` object provides
   527 be thrown away once the response is sent.
   522 be thrown away once the response is sent.
   528 
   523 
   529 The web publisher handles the transaction:
   524 The web publisher handles the transaction:
   530 
   525 
   531 * commit / rollback is done automatically
   526 * commit / rollback is done automatically
   532 * you should not commit / rollback explicitly
   527 
   533 
   528 * you should not commit / rollback explicitly, except if you really
   534 Because a session lives for a long time, and database connections are a limited
   529   need it
   535 resource, we can't bind a session to its own database connection for all its
       
   536 lifetime. The repository handles a pool of connections (4 by default), and it's
       
   537 responsible to attribute them as needed.
       
   538 
   530 
   539 Let's detail the process:
   531 Let's detail the process:
   540 
   532 
   541 1. an incoming RQL query comes from a client to the repository
   533 1. an incoming RQL query comes from a client to the web stack
   542 
   534 
   543 2. the repository attributes a database connection to the session
   535 2. the web stack opens an authenticated database connection for the
   544 
   536    request, which is associated to a user session
   545 3. the repository's querier executes the query
   537 
       
   538 3. the query is executed (through the repository connection)
   546 
   539 
   547 4. this query may trigger hooks. Hooks and operation may execute some rql queries
   540 4. this query may trigger hooks. Hooks and operation may execute some rql queries
   548    through `_cw.execute`. Those queries go directly to the querier, hence don't
   541    through `cnx.execute`.
   549    touch the database connection, they use the one attributed in 2.
       
   550 
   542 
   551 5. the repository gets the result of the query in 1. If it was a RQL read query,
   543 5. the repository gets the result of the query in 1. If it was a RQL read query,
   552    the database connection is released. If it was a write query, the connection
   544    the database connection is released. If it was a write query, the connection
   553    is then tied to the session until the transaction is commited or rollbacked.
   545    is then tied to the session until the transaction is commited or rollbacked.
   554 
   546 
   555 6. results are sent back to the client
   547 6. results are sent back to the client
   556 
   548 
   557 This implies several things:
   549 This implies several things:
   558 
   550 
   559 * when using a request, or code executed in hooks, this database connection
   551 * when using a request, or code executed in hooks, this database
   560   handling is totally transparent
   552   connection handling is totally transparent
   561 
   553 
   562 * however, take care when writing tests: you are usually faking / testing both the
   554 * however, take care when writing tests: you are usually faking /
   563   server and the client side, so you have to decide when to use RepoAccess.client_cnx /
   555   testing both the server and the client side, so you have to decide
   564   RepoAccess.repo_cnx. Ask yourself "where the code I want to test will be running,
   556   when to use RepoAccess.client_cnx or RepoAccess.repo_cnx. Ask
   565   client or repository side ?". The response is usually : use a client connection :)
   557   yourself "where the code I want to test will be running, client or
   566   However, if you really need using a server-side object:
   558   repository side ?". The response is usually: use a repo (since the
   567 
   559   "client connection" concept is going away in a couple of releases).
   568   - commit / rollback will free the database connection (unless explicitly told
       
   569     not to do so).
       
   570 
       
   571   - if you issue a query after that without asking for a database connection
       
   572     (`session.get_cnxset()`), you will end up with a 'None type has no attribute
       
   573     source()' error