Chapter 43
Writing Functional Tests
Difficulty
Newcomer
Skills
- It is good to know how Zope 3 generated forms work before reading this
chapter. Optional.
Problem/Task
Unit tests cover a large part of the testing requirements listed in the eXtreme
Programming literature, but are not everything. There are also integration and
functional tests. While integration tests can be handled with unit tests and doctests,
functional tests cannot. For this reason the Zope 3 community members developed an
extension to unittest that handles functional tests. This package is introduced in
this chapter.
Solution
Unit tests are very good for testing the functionality of a particular object in absence
of the environment it will eventually live in. Integration tests build on this by testing
the behavior of an object in a limited environment. Then functional tests
should test the behavior of an object in a fully running system. Therefore
functional tests often check the user interface behavior and it is not surprising
that they are found in the browser packages of Zope 3. In fact, in Zope 3’s
implementation of functional tests there exists a base test case class for
each view type, such as zope.testing.functional.BrowserTestCase or
zope.app.dav.ftests.dav.DAVTestCase.
43.1 The Browser Test Case
Each custom functional test case class provides some valuable methods that help us
write the tests in a fast and efficient manner. Here are the methods that the
BrowserTestCase class provides.
- getRootFolder()
Returns the root folder of the database. This method is available in every
functional test case class.
- makeRequest(path='',basic=None,form=None,env={},outstream=None)
This class creates a new BrowserRequest instance that can be used for
publishing a request with the Zope publisher.
- path - This is the absolute path of the URL (i.e. the URL minus
the protocol, server and port) of the object that is beeing accessed.
- basic - It provides the authentication information of the format
"<login>:<password>". When Zope 3 is brought up for functional
testing, a user with the login “mgr” and the password “mgrpw” is
automatically created having the role “zope.Manager” assigned to it.
So usually we will use "mgr:mgrpw" as our basic argument.
- form - The argument is a dictionary that contains all fields that
would be provided by an HTML form. Note that we should have
covnerted the data already to their native Python format; be sure to
only use unicode for strings.
- env - This variable is also a dictionary where we can specify further
environment variables, like HTTP headers. For example, the header
X-Header:value would be an entry of the form 'HTTP_X_HEADER':
value in the dictionary.
- outstream - Optionally we can define the the stream to which the
outputted HTML is sent. If we do not specify one, one will be created
for us.
However, one would often not use this method directly, since it does not
actually publish the request. Use the publish() method described
below.
- publish(self,path,basic=None,form=None,env={},handle_errors=False)
The method creates a request as described above, that is then published with a
fully-running Zope 3 instance and finally returns a regular browser response
object that is enhanced by a couple of methods:
- getOutput() - Returns all of the text that was pushed to the
outstream.
- getBody() - Only returns all of the HTML of the response. It
therefore excludes HTTP headers.
- getPath() - Returns the path that was passed to the request.
The path, basic, form and env have the same semantics as the
equally-named arguments to makeRequest(). If handle_errors is
False, then occuring exceptions are not caught. If True, the default
view of an exception is used and a nice formatted HTML page will be
returned. As you can imagine the first option is often more useful for
testing.
- checkForBrokenLinks(body,path,basic=None)
Given an output body and a published path, this method checks whether the
contained HTML contains any links and checks that these links are not broken.
Since the availability of pages and therefore links depends on the permissions of
the user, one might want to specify a login/password pair in the basic
argument. For example, if we have published a request as a manager, it will be
very likely that the returned HTML contains links that require the manager
role.
43.2 Testing “ZPT Page” Views
Okay, now that we know how the BrowserTestCase extends the normal
unittest.TestCase, we can use it to write some functional tests for the “add”,
“edit” and “index” view of the “ZPT Page” content type.
Anywhere, create a file called test_zptpage.py and add the following functional
testing code:
1 import time
2 import unittest
3
4 from transaction import get_transaction
5 from zope.app.tests.functional import BrowserTestCase
6 from zope.app.zptpage.zptpage import ZPTPage
7
8 class ZPTPageTests(BrowserTestCase):
9 """Funcional tests for ZPT Page."""
10
11 template = u'''\
12 <html>
13 <body>
14 <h1 tal:content="modules/time/asctime" />
15 </body>
16 </html>'''
17
18 template2 = u'''\
19 <html>
20 <body>
21 <h1 tal:content="modules/time/asctime">time</h1>
22 </body>
23 </html>'''
24
25 def createPage(self):
26 root = self.getRootFolder()
27 root['zptpage'] = ZPTPage()
28 root['zptpage'].setSource(self.template, 'text/html')
29 get_transaction().commit()
30
31 def test_add(self):
32 response = self.publish(
33 "/+/zope.app.zptpage.ZPTPage=",
34 basic='mgr:mgrpw',
35 form={'add_input_name' : u'newzptpage',
36 'field.expand.used' : u'',
37 'field.source' : self.template,
38 'field.evaluateInlineCode.used' : u'',
39 'field.evaluateInlineCode' : u'on',
40 'UPDATE_SUBMIT' : 'Add'})
41
42 self.assertEqual(response.getStatus(), 302)
43 self.assertEqual(response.getHeader('Location'),
44 'http://localhost/@@contents.html')
45
46 zpt = self.getRootFolder()['newzptpage']
47 self.assertEqual(zpt.getSource(), self.template)
48 self.assertEqual(zpt.evaluateInlineCode, True)
49
50 def test_editCode(self):
51 self.createPage()
52 response = self.publish(
53 "/zptpage/@@edit.html",
54 basic='mgr:mgrpw',
55 form={'field.expand.used' : u'',
56 'field.source' : self.template2,
57 'UPDATE_SUBMIT' : 'Change'})
58 self.assertEqual(response.getStatus(), 200)
59 self.assert_('>time<' in response.getBody())
60 zpt = self.getRootFolder()['zptpage']
61 self.assertEqual(zpt.getSource(), self.template2)
62 self.checkForBrokenLinks(response.getBody(), response.getPath(),
63 'mgr:mgrpw')
64
65 def test_index(self):
66 self.createPage()
67 t = time.asctime()
68 response = self.publish("/zptpage", basic='mgr:mgrpw')
69 self.assertEqual(response.getStatus(), 200)
70 self.assert_(response.getBody().find('<h1>'+t+'</h1>') != -1)
71
72 def test_suite():
73 return unittest.TestSuite((
74 unittest.makeSuite(ZPTPageTests),
75 ))
76
77 if __name__=='__main__':
78 unittest.main(defaultTest='test_suite')
- Line 25-29: This is the perfect example of a helper method often used
in Zope’s functional tests. It creates a “ZPT Page” content object called
zptpage. To write the new object to the ZODB, we have to commit the
transaction using get_transaction().commit().
- Line 31-48: To understand this test completely, it is surely helpful to be
familiar with the way Zope 3 adds new objects and how the widgets create
an HTML form. The “+”-sign in the URL is the adding view for a folder.
The path that follows is simply the factory id of the content type (line 33).
Instead of the factory id, we sometimes also find the name of the object’s
add form there.
The form dictionary is another piece of information that must be
carefully constructed. First of all, the field.expand.used and field.
evaluateInlineCode.used are required, whether we want to activate
expand and evaluateInlineCode or not. It is required by the
corresponding widgets. The add_input_name key contains the name
the content object will recieve and UPDATE_SUBMIT just tells the form
generator that the form was actually submitted and action should be
taken. Also note that all form entries representing a field have a “field.”
prefix, which is done by the widgets. How did I know all these variable
names? Parallel to writing the functional test, I just created a “ZPT Page”
on the browser, looking at the HTML source for the names and values.
There is no way I would have remembered all this!
On line 42, we check whether the request was successful. Code 302
signalizes a redirect and on line 43-44 we check that we are redirected to
the correct page.
Now, it is time to check in the ZODB whether the object has really been
created and that all data was set correctly. On line 46 we retrieve the
object itself and consequently we check that the source is set correctly
and the evaluateInlineCode flag was turned on (line 48) as the request
demanded in the form (line 39).
- Line 50-63: Before we can test whether the data of a “ZPT Page” can be
edited correctly, we have to create one. Here the createPage() method
comes in handy, which quickly creates a page that we can use. Having
done previous test already, the contents of the form dictionary should be
obvious.
Since the edit page returns itself, the status of the response should be 200.
We also inspect the body of the response to make sure that the temlpate
was stored correctly.
One extremly useful feature of the BrowserTestCase is the check for
broken links in the returned page. I would suggest that you do this test
whenever a HTML page is returned by the response.
- Line 65-70: Here we simply test the default view of the “ZPT Page”. No
complicated forms or environments are necessary. We just need to make
sure that the template is executed correctly.
43.3 Running Functional Tests
The testing code directly depends on the Zope 3 source tree, so make sure to have it
in your Python path. In Un*x/Linux we can do this using
1 export PYTHONPATH=$PYTHONPATH:ZOPE3/src
where ZOPE3 is the path to our Zope 3 installation. Furthermore, functional tests
depend on finding a file called ftesting.zcml, which is used to bring up the Zope 3
application server. Therefore it is best to just go to the directory ZOPE3,
since there exists such a file. You can now execute our new funtional tests
using
1 python path/to/ftest/test_zptpage.py
You will notice that these tests will take a couple seconds (5-10 seconds) to run.
This is okay, since the functional tests have to bring up the entire Zope 3 system,
which by itself will take about 4-10 seconds.
1 ...
2 ----------------------------------------------------------------------
3 Ran 3 tests in 16.623s
4
5 OK
As usual you also use the test runner to execute the tests.
Exercises
- Add another functional test that checks the “Preview” and “Inline Code”
screen.