Commit b255c894 authored by Julien Muchembled's avatar Julien Muchembled

PortalTransforms: merge upstream 2.0

This fixes test_20_reStructuredText partially.

Conflicts:
	Products/PortalTransforms/TransformEngine.py
	Products/PortalTransforms/libtransforms/commandtransform.py
	Products/PortalTransforms/transforms/safe_html.py
	Products/PortalTransforms/utils.py

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@41726 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 4adafd42
# -*- coding: utf-8 -*-
from logging import DEBUG
from persistent.list import PersistentList
from zope.interface import implements
from AccessControl import ClassSecurityInfo
from Acquisition import aq_base
from App.class_init import default__class_init__ as InitializeClass
from Persistence import PersistentMapping
try:
from ZODB.PersistentList import PersistentList
except ImportError:
from persistent.list import PersistentList
from OFS.Folder import Folder
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from Persistence import PersistentMapping
from Products.CMFCore.ActionProviderBase import ActionProviderBase
from Products.CMFCore.permissions import ManagePortal, View
try:
......@@ -22,18 +17,19 @@ except ImportError: # BACK: Zope 2.8
registerToolInterface = lambda tool_id, tool_interface: None
from Products.CMFCore.utils import UniqueObject
from Products.CMFCore.utils import getToolByName
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from Products.PortalTransforms.libtransforms.utils import MissingBinary
from Products.PortalTransforms import transforms
from Products.PortalTransforms.interfaces import IDataStream
from Products.PortalTransforms.interfaces import ITransform
from Products.PortalTransforms.interfaces import IEngine
from Products.PortalTransforms.interfaces import IPortalTransformsTool
from Products.PortalTransforms.data import datastream
from Products.PortalTransforms.chain import TransformsChain
from Products.PortalTransforms.chain import chain
from Products.PortalTransforms.cache import Cache
from Products.PortalTransforms.interfaces import IDataStream
from Products.PortalTransforms.interfaces import ITransform
from Products.PortalTransforms.interfaces import IEngine
from Products.PortalTransforms.interfaces import IPortalTransformsTool
from Products.PortalTransforms.libtransforms.utils import MissingBinary
from Products.PortalTransforms.Transform import Transform
from Products.PortalTransforms.transforms import initialize
from Products.PortalTransforms.utils import log
from Products.PortalTransforms.utils import TransformException
from Products.PortalTransforms.utils import _www
......@@ -50,28 +46,25 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
implements(IPortalTransformsTool, IEngine)
meta_types = all_meta_types = (
{ 'name' : 'Transform',
'action' : 'manage_addTransformForm'},
{ 'name' : 'TransformsChain',
'action' : 'manage_addTransformsChainForm'},
{'name': 'Transform', 'action': 'manage_addTransformForm'},
{'name': 'TransformsChain', 'action': 'manage_addTransformsChainForm'},
)
manage_addTransformForm = PageTemplateFile('addTransform', _www)
manage_addTransformsChainForm = PageTemplateFile('addTransformsChain', _www)
manage_addTransformsChainForm = PageTemplateFile(
'addTransformsChain', _www)
manage_cacheForm = PageTemplateFile('setCacheTime', _www)
manage_editTransformationPolicyForm = PageTemplateFile('editTransformationPolicy', _www)
manage_editTransformationPolicyForm = PageTemplateFile(
'editTransformationPolicy', _www)
manage_reloadAllTransforms = PageTemplateFile('reloadAllTransforms', _www)
manage_options = ((Folder.manage_options[0],) + Folder.manage_options[2:] +
(
{ 'label' : 'Caches',
'action' : 'manage_cacheForm'},
{ 'label' : 'Policy',
'action' : 'manage_editTransformationPolicyForm'},
{ 'label' : 'Reload transforms',
'action' : 'manage_reloadAllTransforms'},
)
)
manage_options = (
(Folder.manage_options[0], ) + Folder.manage_options[2:] +
({'label': 'Caches', 'action': 'manage_cacheForm'},
{'label': 'Policy', 'action': 'manage_editTransformationPolicyForm'},
{'label': 'Reload transforms',
'action': 'manage_reloadAllTransforms'},
))
security = ClassSecurityInfo()
......@@ -81,7 +74,7 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
self.max_sec_in_cache = max_sec_in_cache
self._new_style_pt = 1
# mimetype oriented conversions (iengine interface) ########################
# mimetype oriented conversions (iengine interface)
def unregisterTransform(self, name):
""" unregister a transform
......@@ -113,7 +106,7 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
target_mimetype = str(target_mimetype)
if object is not None:
cache = Cache(object)
cache = Cache(object, context=context)
data = cache.getCache(target_mimetype)
if data is not None:
time, data = data
......@@ -126,8 +119,8 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
registry = getToolByName(self, 'mimetypes_registry')
if not getattr(aq_base(registry), 'classify', None):
# avoid problems when importing a site with an old mimetype registry
# XXX return None or orig?
# avoid problems when importing a site with an old mimetype
# registry
return None
orig_mt = registry.classify(orig,
......@@ -135,8 +128,9 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
filename=kwargs.get('filename'))
orig_mt = str(orig_mt)
if not orig_mt:
log('Unable to guess input mime type (filename=%s, mimetype=%s)' %(
kwargs.get('mimetype'), kwargs.get('filename')), severity=WARNING)
log('Unable to guess input mime type (filename=%s, mimetype=%s)' %
(kwargs.get('mimetype'), kwargs.get('filename')),
severity=WARNING)
return None
target_mt = registry.lookup(target_mimetype)
......@@ -151,9 +145,7 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
# If orig_mt and target_mt are the same, we only allow
# a one-hop transform, a.k.a. filter.
# XXX disabled filtering for now
filter_only = False
if orig_mt == str(target_mt):
filter_only = True
data.setData(orig)
md = data.getMetadata()
md['mimetype'] = str(orig_mt)
......@@ -171,9 +163,9 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
path = self._findPath(orig_mt, target_mt)
if not path:
log('NO PATH FROM %s TO %s : %s' % (orig_mt, target_mimetype, path),
severity=WARNING)
return None #XXX raise TransformError
log('NO PATH FROM %s TO %s : %s' %
(orig_mt, target_mimetype, path), severity=WARNING)
return None
if len(path) > 1:
## create a chain on the fly (sly)
......@@ -183,7 +175,8 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
else:
transform = path[0]
result = transform.convert(orig, data, context=context, usedby=usedby, **kwargs)
result = transform.convert(orig, data, context=context,
usedby=usedby, **kwargs)
self._setMetaData(result, transform)
# set cache if possible
......@@ -307,8 +300,8 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
(output, transform.name())
raise TransformException(msg)
if len(mto) > 1:
msg = 'Wildcarding not allowed in transform\'s output '\
'MIME type'
msg = ("Wildcarding not allowed in transform's output "
"MIME type")
raise TransformException(msg)
for mt2 in mto[0].mimetypes:
......@@ -342,36 +335,124 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
"""return the shortest path for transformation from orig mimetype to
target mimetype
"""
path = []
if not self._mtmap:
return None
# naive algorithm :
# find all possible paths with required transforms
# take the shortest
#
# it should be enough since we should not have so much possible paths
shortest, winner = 9999, None
for path in self._getPaths(str(orig), str(target), required_transforms):
if len(path) < shortest:
winner = path
shortest = len(path)
return winner
def _getPaths(self, orig, target, requirements, path=None, result=None, searched_orig_list=None):
"""return a all path for transformation from orig mimetype to
target mimetype
"""
# don't search the same orig again, otherwise infinite loop occurs.
if searched_orig_list is None:
searched_orig_list = []
if orig in searched_orig_list:
orig = str(orig)
target = str(target)
# First, let's deal with required transforms.
if required_transforms:
# Let's decompose paths, then.
required_transform = required_transforms.pop(0)
# The first path must lead to one of the inputs supported
# by this first required transform.
# Which input types are supported by this transform ?
supportedInputs = {}
for input, outputs in self._mtmap.items():
for output, transforms in outputs.items():
for transform in transforms:
if transform.name() == required_transform:
supportedInputs[input] = 'ok'
# BTW, let's remember the output type
transformOutput = output
# and remember the transform, it is
# useful later
requiredTransform = transform
# Which of these inputs will be reachable with the
# shortest path ?
shortest = 9999 # big enough, I guess
shortestFirstPath = None
for supportedInput in supportedInputs.keys():
# We start from orig
firstOrig = orig
# And want to reach supportedInput
firstTarget = supportedInput
# What's the shortest path ?
firstPath = self._findPath(firstOrig, firstTarget)
if firstPath is not None:
if len(firstPath) < shortest:
# Here is a path which is shorter than others
# which also reach the required transform.
shortest = len(firstPath)
shortestFirstPath = firstPath
if shortestFirstPath == None:
return None # there is no path leading to this transform
# Then we have to take this transform.
secondPath = [requiredTransform]
# From the output of this transform, we then have to
# reach our target, possible through other required
# transforms.
thirdOrig = transformOutput
thirdTarget = target
thirdPath = self._findPath(thirdOrig, thirdTarget,
required_transforms)
if thirdPath is None:
return None # no path
# Final result is the concatenation of these 3 parts
return shortestFirstPath + secondPath + thirdPath
if orig == target:
return []
# Now let's efficiently find the shortest path from orig
# to target (without required transforms).
# The overall idea is that we build all possible paths
# starting from orig and of given length. And we increment
# this length until one of these paths reaches our target or
# until all reachable types have been reached.
currentPathLength = 0
pathToType = {orig: []} # all paths we know, by end of path.
def typesWithPathOfLength(length):
'''Returns the lists of known paths of a given length'''
result = []
for type_, path in pathToType.items():
if len(path) == length:
result.append(type_)
return result
else:
searched_orig_list.append(orig)
# We will start exploring paths which start from types
# reachable in zero steps. That is paths which start from
# orig.
typesToStartFrom = typesWithPathOfLength(currentPathLength)
# Explore paths while there are new paths to be explored
while len(typesToStartFrom) > 0:
for startingType in typesToStartFrom:
# Where can we go in one step starting from here ?
outputs = self._mtmap.get(startingType)
if outputs:
for reachedType, transforms in outputs.items():
# Does this lead to a type we never reached before ?
if reachedType not in pathToType.keys() and transforms:
# Yes, we did not know any path reaching this type
# Let's remember the path to here
pathToType[reachedType] = (
pathToType[startingType] + [transforms[0]])
if reachedType == target:
# This is the first time we reach our target.
# We have our shortest path to target.
return pathToType[target]
# We explored all possible paths of length currentPathLength
# Let's increment that length.
currentPathLength += 1
# What are the next types to start from ?
typesToStartFrom = typesWithPathOfLength(currentPathLength)
# We are done exploring paths starting from orig
# and this exploration did not reach our target.
# Hence there is no path from orig to target.
return None
def _getPaths(self, orig, target, requirements, path=None, result=None):
"""return some of the paths for transformation from orig mimetype to
target mimetype with the guarantee that the shortest path is included.
If target is the same as orig, then returns an empty path.
"""
shortest = 9999
if result:
for okPath in result:
shortest = min(shortest, len(okPath))
if orig == target:
return [[]]
if path is None:
result = []
path = []
......@@ -380,9 +461,9 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
if outputs is None:
return result
registry = getToolByName(self, 'mimetypes_registry')
mto = registry.lookup(target)
# target mimetype aliases
registry = getToolByName(self, 'mimetypes_registry')
mto = registry.lookup(target)
# target mimetype aliases
target_aliases = mto[0].mimetypes
path.append(None)
......@@ -400,8 +481,14 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
if o_mt in target_aliases:
if not requirements:
result.append(path[:])
if len(path[:]) < shortest:
# here is a shorter one !
shortest = len(path)
else:
self._getPaths(o_mt, target, requirements, path, result, searched_orig_list)
if len(path) < shortest:
# keep exploring this path, it is still short enough
self._getPaths(o_mt, target, requirements,
path, result)
if required:
requirements.append(name)
path.pop()
......@@ -414,14 +501,11 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
transform tool is added
"""
Folder.manage_afterAdd(self, item, container)
transforms.initialize(self)
# XXX required?
#try:
# # first initialization
# transforms.initialize(self)
#except:
# # may fail on copy
# pass
try:
initialize(self)
except TransformException:
# may fail on copy or zexp import
pass
security.declareProtected(ManagePortal, 'manage_addTransform')
def manage_addTransform(self, id, module, REQUEST=None):
......@@ -465,28 +549,31 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
reloaded.append((id, o.module))
return reloaded
# Policy handling methods #################################################
# Policy handling methods
def manage_addPolicy(self, output_mimetype, required_transforms, REQUEST=None):
def manage_addPolicy(self, output_mimetype, required_transforms,
REQUEST=None):
""" add a policy for a given output mime types"""
registry = getToolByName(self, 'mimetypes_registry')
if not registry.lookup(output_mimetype):
raise TransformException('Unknown MIME type')
if self._policies.has_key(output_mimetype):
if output_mimetype in self._policies:
msg = 'A policy for output %s is yet defined' % output_mimetype
raise TransformException(msg)
required_transforms = tuple(required_transforms)
self._policies[output_mimetype] = required_transforms
if REQUEST is not None:
REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_editTransformationPolicyForm')
REQUEST['RESPONSE'].redirect(self.absolute_url() +
'/manage_editTransformationPolicyForm')
def manage_delPolicies(self, outputs, REQUEST=None):
""" remove policies for given output mime types"""
for mimetype in outputs:
del self._policies[mimetype]
if REQUEST is not None:
REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_editTransformationPolicyForm')
REQUEST['RESPONSE'].redirect(self.absolute_url() +
'/manage_editTransformationPolicyForm')
def listPolicies(self):
""" return the list of defined policies
......@@ -498,20 +585,21 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
self._policies = PersistentMapping()
return self._policies.items()
# mimetype oriented conversions (iengine interface) ########################
# mimetype oriented conversions (iengine interface)
def registerTransform(self, transform):
"""register a new transform
transform isn't a Zope Transform (the wrapper) but the wrapped transform
the persistence wrapper will be created here
transform isn't a Zope Transform (the wrapper) but the wrapped
transform the persistence wrapper will be created here
"""
# needed when call from transform.transforms.initialize which
# register non zope transform
module = str(transform.__module__)
transform = Transform(transform.name(), module, transform)
if not ITransform.providedBy(transform):
raise TransformException('%s does not implement ITransform' % transform)
raise TransformException('%s does not implement ITransform' %
transform)
name = transform.name()
__traceback_info__ = (name, transform)
if name not in self.objectIds():
......@@ -539,8 +627,9 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
# available mimetypes ####################################################
def listAvailableTextInputs(self):
""" Returns a list of mimetypes that can be used as input for textfields
by building a list of the inputs beginning with "text/" of all transforms.
"""Returns a list of mimetypes that can be used as input for textfields
by building a list of the inputs beginning with "text/" of all
transforms.
"""
available_types = []
candidate_transforms = [object[1] for object in self.objectItems()]
......
......@@ -3,10 +3,16 @@
from time import time
from Acquisition import aq_base
_marker = object()
class Cache:
def __init__(self, context, _id='_v_transform_cache'):
self.context = context
def __init__(self, obj, context=None, _id='_v_transform_cache'):
self.obj = obj
if context is None:
self.context = obj
else:
self.context = context
self._id =_id
def _genCacheKey(self, identifier, *args):
......@@ -17,17 +23,19 @@ class Cache:
key = key.replace('+', '_')
key = key.replace('-', '_')
key = key.replace(' ', '_')
if hasattr(aq_base(self.context), 'absolute_url'):
return key, self.context.absolute_url()
return key
def setCache(self, key, value):
"""cache a value indexed by key"""
if not value.isCacheable():
return
context = self.context
obj = self.obj
key = self._genCacheKey(key)
if getattr(aq_base(context), self._id, None) is None:
setattr(context, self._id, {})
getattr(context, self._id)[key] = (time(), value)
if getattr(aq_base(obj), self._id, None) is None:
setattr(obj, self._id, {})
getattr(obj, self._id)[key] = (time(), value)
return key
def getCache(self, key):
......@@ -36,9 +44,9 @@ class Cache:
return None if not present
else return a tuple (time spent in cache, value)
"""
context = self.context
obj = self.obj
key = self._genCacheKey(key)
dict = getattr(context, self._id, None)
dict = getattr(obj, self._id, None)
if dict is None :
return None
try:
......@@ -46,18 +54,18 @@ class Cache:
return time() - orig_time, value
except TypeError:
return None
def purgeCache(self, key=None):
"""Remove cache
"""
context = self.context
obj = self.obj
id = self._id
if not shasattr(context, id):
if getattr(obj, id, _marker) is _marker:
return
if key is None:
delattr(context, id)
delattr(obj, id)
else:
cache = getattr(context, id)
cache = getattr(obj, id)
key = self._genCacheKey(key)
if cache.has_key(key):
del cache[key]
......@@ -87,28 +87,30 @@ class popentransform:
def convert(self, data, cache, **kwargs):
command = "%s %s" % (self.binary, self.binaryArgs)
if not self.useStdin:
tmpfile, tmpname = tempfile.mkstemp(text=False) # create tmp
os.write(tmpfile, data) # write data to tmp using a file descriptor
os.close(tmpfile) # close it so the other process can read it
command = command % { 'infile' : tmpname } # apply tmp name to command
cin, couterr = os.popen4(command, 'b')
if self.useStdin:
cin.write(str(data))
status = cin.close()
out = self.getData(couterr)
couterr.close()
if not self.useStdin:
# remove tmp file
os.unlink(tmpname)
cache.setData(out)
return cache
tmpname = None
try:
if not self.useStdin:
tmpfile, tmpname = tempfile.mkstemp(text=False) # create tmp
os.write(tmpfile, data) # write data to tmp using a file descriptor
os.close(tmpfile) # close it so the other process can read it
command = command % { 'infile' : tmpname } # apply tmp name to command
cin, couterr = os.popen4(command, 'b')
if self.useStdin:
cin.write(str(data))
status = cin.close()
out = self.getData(couterr)
couterr.close()
cache.setData(out)
return cache
finally:
if not self.useStdin and tmpname is not None:
# remove tmp file
os.unlink(tmpname)
from subprocess import Popen, PIPE
import shlex
......
import re
import os
import sys
from sgmllib import SGMLParser
from sgmllib import SGMLParser, SGMLParseError
try:
# Need to be imported before win32api to avoid dll loading
......@@ -207,7 +207,26 @@ class StrippingParser( SGMLParser ):
self.result = "%s</%s>" % (self.result, tag)
remTag = '</%s>' % tag
def parse_declaration(self, i):
"""Fix handling of CDATA sections. Code borrowed from BeautifulSoup.
"""
j = None
if self.rawdata[i:i+9] == '<![CDATA[':
k = self.rawdata.find(']]>', i)
if k == -1:
k = len(self.rawdata)
data = self.rawdata[i+9:k]
j = k+3
self.result.append("<![CDATA[%s]]>" % data)
else:
try:
j = SGMLParser.parse_declaration(self, i)
except SGMLParseError:
toHandle = self.rawdata[i:]
self.result.append(toHandle)
j = i + len(toHandle)
return j
def scrubHTML( html ):
""" Strip illegal HTML tags from string text. """
......
## Testing Markdown
`code` and _italic_ and *bold* and even a [link](http://plone.org).
Fööbär
......@@ -15,6 +15,10 @@
</tr>
</table>
<p>This is a text used as a blind text.</p>
<div><![CDATA[
Some CDATA text.
]]>
</div>
<ul>
<li>A sample list item1</li>
<li>A sample list item2</li>
......
<A name=1></a>Chapter 44<br>
Writing Basic Unit Tests<br>
Difficulty<br>
Newcomer<br>
Skills<br>
• All you need to know is some Python.<br>
Problem/Task<br>
As you know by now, Zope 3 gains its incredible stability from testing any code in great detail. The<br>currently most common method is to write unit tests. This chapter introduces unit tests – which<br>are Zope 3 independent – and introduces some of the subtleties.<br>
Solution<br>
44.1<br>
Implementing the Sample Class<br>
Before we can write tests, we have to write some code that we can test. Here, we will implement<br>a simple class called Sample with a public attribute title and description that is accessed<br>via getDescription() and mutated using setDescription(). Further, the description must be<br>either a regular or unicode string.<br>
Since this code will not depend on Zope, open a file named test sample.py anywhere and add<br>
the following class:<br>
1 Sample(object):<br>
2<br>
&quot;&quot;&quot;A trivial Sample object.&quot;&quot;&quot;<br>
3<br>
4<br>
title = None<br>
5<br>
6<br>
def __init__(self):<br>
7<br>
&quot;&quot;&quot;Initialize object.&quot;&quot;&quot;<br>
8<br>
self._description = ’’<br>
9<br>
1<br>
<hr>
<A name=2></a>2<br>
CHAPTER 44. WRITING BASIC UNIT TESTS<br>
10<br>
def setDescription(self, value):<br>
11<br>
&quot;&quot;&quot;Change the value of the description.&quot;&quot;&quot;<br>
12<br>
assert isinstance(value, (str, unicode))<br>
13<br>
self._description = value<br>
14<br>
15<br>
def getDescription(self):<br>
16<br>
&quot;&quot;&quot;Change the value of the description.&quot;&quot;&quot;<br>
17<br>
return self._description<br>
Line 4: The title is just publicly declared and a value of None is given. Therefore this is just<br>a regular attribute.<br>
Line 8: The actual description string will be stored in description.<br>
Line 12: Make sure that the description is only a regular or unicode string, like it was stated in<br>the requirements.<br>
If you wish you can now manually test the class with the interactive Python shell. Just start<br>
Python by entering python in your shell prompt. Note that you should be in the directory in<br>which test sample.py is located when starting Python (an alternative is of course to specify the<br>directory in your PYTHONPATH.)<br>
1 &gt;&gt;&gt; from test_sample import Sample<br>2 &gt;&gt;&gt; sample = Sample()<br>
3 &gt;&gt;&gt; print sample.title<br>4 None<br>
5 &gt;&gt;&gt; sample.title = ’Title’<br>
6 &gt;&gt;&gt; print sample.title<br>7 Title<br>
8 &gt;&gt;&gt; print sample.getDescription()<br>9<br>
10 &gt;&gt;&gt; sample.setDescription(’Hello World’)<br>
11 &gt;&gt;&gt; print sample.getDescription()<br>12 Hello World<br>
13 &gt;&gt;&gt; sample.setDescription(None)<br>
14 Traceback (most recent call last):<br>
15<br>
File &quot;&lt;stdin&gt;&quot;, line 1, in ?<br>
16<br>
File &quot;test_sample.py&quot;, line 31, in setDescription<br>
17<br>
assert isinstance(value, (str, unicode))<br>
18 AssertionError<br>
As you can see in the last test, non-string object types are not allowed as descriptions and an<br>
AssertionError is raised.<br>
44.2<br>
Writing the Unit Tests<br>
The goal of writing the unit tests is to convert this informal, manual, and interactive testing session<br>into a formal test class. Python provides already a module called unittest for this purpose, which<br>is a port of the Java-based unit testing product, JUnit, by Kent Beck and Erich Gamma. There are<br>three levels to the testing framework (this list deviates a bit from the original definitions as found<br>in the Python library documentation. 1).<br>
1 http://www.python.org/doc/current/lib/module-unittest.html<br>
<hr>
<A name=3></a>44.2. WRITING THE UNIT TESTS<br>
3<br>
The smallest unit is obviously the “test”, which is a single method in a TestCase class that<br>
tests the behavior of a small piece of code or a particular aspect of an implementation. The “test<br>case” is then a collection tests that share the same setup/inputs. On top of all of this sits the “test<br>suite” which is a collection of test cases and/or other test suites. Test suites combine tests that<br>should be executed together. With the correct setup (as shown in the example below), you can<br>then execute test suites. For large projects like Zope 3, it is useful to know that there is also the<br>concept of a test runner, which manages the test run of all or a set of tests. The runner provides<br>useful feedback to the application, so that various user interaces can be developed on top of it.<br>
But enough about the theory. In the following example, which you can simply put into the same<br>
file as your code above, you will see a test in common Zope 3 style.<br>
1 import unittest<br>2<br>
3 class SampleTest(unittest.TestCase):<br>4<br>
&quot;&quot;&quot;Test the Sample class&quot;&quot;&quot;<br>
5<br>
6<br>
def test_title(self):<br>
7<br>
sample = Sample()<br>
8<br>
self.assertEqual(sample.title, None)<br>
9<br>
sample.title = ’Sample Title’<br>
10<br>
self.assertEqual(sample.title, ’Sample Title’)<br>
11<br>
12<br>
def test_getDescription(self):<br>
13<br>
sample = Sample()<br>
14<br>
self.assertEqual(sample.getDescription(), ’’)<br>
15<br>
sample._description = &quot;Description&quot;<br>
16<br>
self.assertEqual(sample.getDescription(), ’Description’)<br>
17<br>
18<br>
def test_setDescription(self):<br>
19<br>
sample = Sample()<br>
20<br>
self.assertEqual(sample._description, ’’)<br>
21<br>
sample.setDescription(’Description’)<br>
22<br>
self.assertEqual(sample._description, ’Description’)<br>
23<br>
sample.setDescription(u’Description2’)<br>
24<br>
self.assertEqual(sample._description, u’Description2’)<br>
25<br>
self.assertRaises(AssertionError, sample.setDescription, None)<br>
26<br>
27<br>
28 def test_suite():<br>29<br>
return unittest.TestSuite((<br>
30<br>
unittest.makeSuite(SampleTest),<br>
31<br>
))<br>
32<br>
33 if __name__ == ’__main__’:<br>34<br>
unittest.main(defaultTest=’test_suite’)<br>
Line 3–4: We usually develop test classes which must inherit from TestCase. While often not<br>done, it is a good idea to give the class a meaningful docstring that describes the purpose of the<br>tests it includes.<br>
Line 6, 12 &amp; 18: When a test case is run, a method called runTests() is executed. While it<br>is possible to overrride this method to run tests differently, the default option will look for any<br>method whose name starts with test and execute it as a single test. This way we can create<br>a “test method” for each aspect, method, function or property of the code to be tested. This<br>default is very sensible and is used everywhere in Zope 3.<br>
<hr>
<A name=4></a>4<br>
CHAPTER 44. WRITING BASIC UNIT TESTS<br>
Note that there is no docstring for test methods. This is intentional. If a docstring is specified,<br>it is used instead of the method name to identify the test. When specifying a docstring, we have<br>noticed that it is very difficult to identify the test later; therefore the method name is a much<br>better choice.<br>
Line 8, 10, 14, . . . : The TestCase class implements a handful of methods that aid you with the<br>testing. Here are some of the most frequently used ones. For a complete list see the standard<br>Python documentation referenced above.<br>
• assertEqual(first,second[,msg])<br>
Checks whether the first and second value are equal. If the test fails, the msg or None<br>is returned.<br>
• assertNotEqual(first,second[,msg])<br>
This is simply the opposite to assertEqual() by checking for non-equality.<br>
• assertRaises(exception,callable,...)<br>
You expect the callable to raise exception when executed. After the callable you can<br>specify any amount of positional and keyword arguments for the callable. If you expect<br>a group of exceptions from the execution, you can make exception a tuple of possible<br>exceptions.<br>
• assert (expr[,msg])<br>
Assert checks whether the specified expression executes correctly. If not, the test fails and<br>msg or None is returned.<br>
• failUnlessEqual()<br>
This testing method is equivalent to assertEqual().<br>
• failUnless(expr[,msg])<br>
This method is equivalent to assert (expr[,msg]).<br>
• failif()<br>
This is the opposite to failUnless().<br>
• fail([msg])<br>
Fails the running test without any evaluation. This is commonly used when testing various<br>possible execution paths at once and you would like to signify a failure if an improper path<br>was taken.<br>
Line 6–10: This method tests the title attribute of the Sample class. The first test should<br>be of course that the attribute exists and has the expected initial value (line 8). Then the title<br>attribute is changed and we check whether the value was really stored. This might seem like<br>overkill, but later you might change the title in a way that it uses properties instead. Then it<br>becomes very important to check whether this test still passes.<br>
Line 12–16: First we simply check that getDescription() returns the correct default value.<br>Since we do not want to use other API calls like setDescription() we set a new value of the<br>description via the implementation-internal description attribute (line 15). This is okay! Unit<br>tests can make use of implementation-specific attributes and methods. Finally we just check that<br>the correct value is returned.<br>
<hr>
<A name=5></a>44.3. RUNNING THE TESTS<br>
5<br>
Line 18–25: On line 21–24 it is checked that both regular and unicode strings are set correctly.<br>In the last line of the test we make sure that no other type of objects can be set as a description<br>and that an error is raised.<br>
28–31: This method returns a test suite that includes all test cases created in this module. It is<br>used by the Zope 3 test runner when it picks up all available tests. You would basically add the<br>line unittest.makeSuite(TestCaseClass) for each additional test case.<br>
33–34: In order to make the test module runnable by itself, you can execute unittest.main()<br>when the module is run.<br>
44.3<br>
Running the Tests<br>
You can run the test by simply calling pythontest sample.py from the directory you saved the<br>file in. Here is the result you should see:<br>
.<br>--------------------------------------------------------------------<br>n 3 tests in 0.001s<br>
The three dots represent the three tests that were run. If a test had failed, it would have been<br>
reported pointing out the failing test and providing a small traceback.<br>
When using the default Zope 3 test runner, tests will be picked up as long as they follow some<br>
conventions.<br>
• The tests must either be in a package or be a module called tests.<br>
• If tests is a package, then all test modules inside must also have a name starting with test,<br>
as it is the case with our name test sample.py.<br>
• The test module must be somewhere in the Zope 3 source tree, since the test runner looks<br>
only for files there.<br>
In our case, you could simply create a tests package in ZOPE3/src (do not forget the<br>
init .<br>
py file). Then place the test sample.py file into this directory.<br>
You you can use the test runner to run only the sample tests as follows from the Zope 3 root<br>
directory:<br>
python test.py -vp tests.test_sample<br>
The -v option stands for verbose mode, so that detailed information about a test failure is<br>
provided. The -p option enables a progress bar that tells you how many tests out of all have been<br>completed. There are many more options that can be specified. You can get a full list of them with<br>the option -h: pythontest.py-h.<br>
The output of the call above is as follows:<br>
nfiguration file found.<br>nning UNIT tests at level 1<br>nning UNIT tests from /opt/zope/Zope3<br>
3/3 (100.0%): test_title (tests.test_sample.SampleTest)<br>
--------------------------------------------------------------------<br>n 3 tests in 0.002s<br>
<hr>
<A name=6></a>6<br>
CHAPTER 44. WRITING BASIC UNIT TESTS<br>
nning FUNCTIONAL tests at level 1<br>nning FUNCTIONAL tests from /opt/zope/Zope3<br>
--------------------------------------------------------------------<br>n 0 tests in 0.000s<br>
Line 1: The test runner uses a configuration file for some setup. This allows developers to use<br>the test runner for other projects as well. This message simply tells us that the configuration file<br>was found.<br>
Line 2–8: The unit tests are run. On line 4 you can see the progress bar.<br>
Line 9–15: The functional tests are run, since the default test runner runs both types of tests.<br>Since we do not have any functional tests in the specified module, there are no tests to run. To<br>just run the unit tests, use option -u and -f for just running the functional tests. See “Writing<br>Functional Tests” for more detials on functional tests.<br>
<hr>
<A name=7></a>44.3. RUNNING THE TESTS<br>
7<br>
Exercises<br>
1. It is not very common to do the setup – in our case sample=Sample() – in every test<br>
method. Instead there exists a method called setUp() and its counterpart tearDown that<br>are run before and after each test, respectively. Change the test code above, so that it uses<br>the setUp() method. In later chapters and the rest of the book we will frequently use this<br>method of setting up tests.<br>
2. Currently the test setDescription() test only verifies that None is not allowed as input<br>
value.<br>
(a) Improve the test, so that all other builtin types are tested as well.<br>
(b) Also, make sure that any objects inheriting from str or unicode pass as valid values.<br>
<hr>
\ No newline at end of file
<A name=1></a>Chapter&nbsp;44<br>Writing&nbsp;Basic&nbsp;Unit&nbsp;Tests<br>Difficulty<br>Newcomer<br>Skills<br>&nbsp;All&nbsp;you&nbsp;need&nbsp;to&nbsp;know&nbsp;is&nbsp;some&nbsp;Python.<br>Problem/Task<br>As&nbsp;you&nbsp;know&nbsp;by&nbsp;now,&nbsp;Zope&nbsp;3&nbsp;gains&nbsp;its&nbsp;incredible&nbsp;stability&nbsp;from&nbsp;testing&nbsp;any&nbsp;code&nbsp;in&nbsp;great&nbsp;detail.&nbsp;The<br>currently&nbsp;most&nbsp;common&nbsp;method&nbsp;is&nbsp;to&nbsp;write&nbsp;unit&nbsp;tests.&nbsp;This&nbsp;chapter&nbsp;introduces&nbsp;unit&nbsp;tests&nbsp;&nbsp;which<br>are&nbsp;Zope&nbsp;3&nbsp;independent&nbsp;&nbsp;and&nbsp;introduces&nbsp;some&nbsp;of&nbsp;the&nbsp;subtleties.<br>Solution<br>44.1<br>Implementing&nbsp;the&nbsp;Sample&nbsp;Class<br>Before&nbsp;we&nbsp;can&nbsp;write&nbsp;tests,&nbsp;we&nbsp;have&nbsp;to&nbsp;write&nbsp;some&nbsp;code&nbsp;that&nbsp;we&nbsp;can&nbsp;test.&nbsp;Here,&nbsp;we&nbsp;will&nbsp;implement<br>a&nbsp;simple&nbsp;class&nbsp;called&nbsp;Sample&nbsp;with&nbsp;a&nbsp;public&nbsp;attribute&nbsp;title&nbsp;and&nbsp;description&nbsp;that&nbsp;is&nbsp;accessed<br>via&nbsp;getDescription()&nbsp;and&nbsp;mutated&nbsp;using&nbsp;setDescription().&nbsp;Further,&nbsp;the&nbsp;description&nbsp;must&nbsp;be<br>either&nbsp;a&nbsp;regular&nbsp;or&nbsp;unicode&nbsp;string.<br>Since&nbsp;this&nbsp;code&nbsp;will&nbsp;not&nbsp;depend&nbsp;on&nbsp;Zope,&nbsp;open&nbsp;a&nbsp;file&nbsp;named&nbsp;test&nbsp;sample.py&nbsp;anywhere&nbsp;and&nbsp;add<br>the&nbsp;following&nbsp;class:<br>1&nbsp;Sample(object):<br>2<br>&quot;&quot;&quot;A&nbsp;trivial&nbsp;Sample&nbsp;object.&quot;&quot;&quot;<br>3<br>4<br>title&nbsp;=&nbsp;None<br>5<br>6<br>def&nbsp;__init__(self):<br>7<br>&quot;&quot;&quot;Initialize&nbsp;object.&quot;&quot;&quot;<br>8<br>self._description&nbsp;=&nbsp;’’<br>9<br>1<br><hr><A name=2></a>2<br>CHAPTER&nbsp;44.&nbsp;WRITING&nbsp;BASIC&nbsp;UNIT&nbsp;TESTS<br>10<br>def&nbsp;setDescription(self,&nbsp;value):<br>11<br>&quot;&quot;&quot;Change&nbsp;the&nbsp;value&nbsp;of&nbsp;the&nbsp;description.&quot;&quot;&quot;<br>12<br>assert&nbsp;isinstance(value,&nbsp;(str,&nbsp;unicode))<br>13<br>self._description&nbsp;=&nbsp;value<br>14<br>15<br>def&nbsp;getDescription(self):<br>16<br>&quot;&quot;&quot;Change&nbsp;the&nbsp;value&nbsp;of&nbsp;the&nbsp;description.&quot;&quot;&quot;<br>17<br>return&nbsp;self._description<br>Line&nbsp;4:&nbsp;The&nbsp;title&nbsp;is&nbsp;just&nbsp;publicly&nbsp;declared&nbsp;and&nbsp;a&nbsp;value&nbsp;of&nbsp;None&nbsp;is&nbsp;given.&nbsp;Therefore&nbsp;this&nbsp;is&nbsp;just<br>a&nbsp;regular&nbsp;attribute.<br>Line&nbsp;8:&nbsp;The&nbsp;actual&nbsp;description&nbsp;string&nbsp;will&nbsp;be&nbsp;stored&nbsp;in&nbsp;description.<br>Line&nbsp;12:&nbsp;Make&nbsp;sure&nbsp;that&nbsp;the&nbsp;description&nbsp;is&nbsp;only&nbsp;a&nbsp;regular&nbsp;or&nbsp;unicode&nbsp;string,&nbsp;like&nbsp;it&nbsp;was&nbsp;stated&nbsp;in<br>the&nbsp;requirements.<br>If&nbsp;you&nbsp;wish&nbsp;you&nbsp;can&nbsp;now&nbsp;manually&nbsp;test&nbsp;the&nbsp;class&nbsp;with&nbsp;the&nbsp;interactive&nbsp;Python&nbsp;shell.&nbsp;Just&nbsp;start<br>Python&nbsp;by&nbsp;entering&nbsp;python&nbsp;in&nbsp;your&nbsp;shell&nbsp;prompt.&nbsp;Note&nbsp;that&nbsp;you&nbsp;should&nbsp;be&nbsp;in&nbsp;the&nbsp;directory&nbsp;in<br>which&nbsp;test&nbsp;sample.py&nbsp;is&nbsp;located&nbsp;when&nbsp;starting&nbsp;Python&nbsp;(an&nbsp;alternative&nbsp;is&nbsp;of&nbsp;course&nbsp;to&nbsp;specify&nbsp;the<br>directory&nbsp;in&nbsp;your&nbsp;PYTHONPATH.)<br>1&nbsp;&gt;&gt;&gt;&nbsp;from&nbsp;test_sample&nbsp;import&nbsp;Sample<br>2&nbsp;&gt;&gt;&gt;&nbsp;sample&nbsp;=&nbsp;Sample()<br>3&nbsp;&gt;&gt;&gt;&nbsp;print&nbsp;sample.title<br>4&nbsp;None<br>5&nbsp;&gt;&gt;&gt;&nbsp;sample.title&nbsp;=&nbsp;’Title’<br>6&nbsp;&gt;&gt;&gt;&nbsp;print&nbsp;sample.title<br>7&nbsp;Title<br>8&nbsp;&gt;&gt;&gt;&nbsp;print&nbsp;sample.getDescription()<br>9<br>10&nbsp;&gt;&gt;&gt;&nbsp;sample.setDescription(’Hello&nbsp;World’)<br>11&nbsp;&gt;&gt;&gt;&nbsp;print&nbsp;sample.getDescription()<br>12&nbsp;Hello&nbsp;World<br>13&nbsp;&gt;&gt;&gt;&nbsp;sample.setDescription(None)<br>14&nbsp;Traceback&nbsp;(most&nbsp;recent&nbsp;call&nbsp;last):<br>15<br>File&nbsp;&quot;&lt;stdin&gt;&quot;,&nbsp;line&nbsp;1,&nbsp;in&nbsp;?<br>16<br>File&nbsp;&quot;test_sample.py&quot;,&nbsp;line&nbsp;31,&nbsp;in&nbsp;setDescription<br>17<br>assert&nbsp;isinstance(value,&nbsp;(str,&nbsp;unicode))<br>18&nbsp;AssertionError<br>As&nbsp;you&nbsp;can&nbsp;see&nbsp;in&nbsp;the&nbsp;last&nbsp;test,&nbsp;non-string&nbsp;object&nbsp;types&nbsp;are&nbsp;not&nbsp;allowed&nbsp;as&nbsp;descriptions&nbsp;and&nbsp;an<br>AssertionError&nbsp;is&nbsp;raised.<br>44.2<br>Writing&nbsp;the&nbsp;Unit&nbsp;Tests<br>The&nbsp;goal&nbsp;of&nbsp;writing&nbsp;the&nbsp;unit&nbsp;tests&nbsp;is&nbsp;to&nbsp;convert&nbsp;this&nbsp;informal,&nbsp;manual,&nbsp;and&nbsp;interactive&nbsp;testing&nbsp;session<br>into&nbsp;a&nbsp;formal&nbsp;test&nbsp;class.&nbsp;Python&nbsp;provides&nbsp;already&nbsp;a&nbsp;module&nbsp;called&nbsp;unittest&nbsp;for&nbsp;this&nbsp;purpose,&nbsp;which<br>is&nbsp;a&nbsp;port&nbsp;of&nbsp;the&nbsp;Java-based&nbsp;unit&nbsp;testing&nbsp;product,&nbsp;JUnit,&nbsp;by&nbsp;Kent&nbsp;Beck&nbsp;and&nbsp;Erich&nbsp;Gamma.&nbsp;There&nbsp;are<br>three&nbsp;levels&nbsp;to&nbsp;the&nbsp;testing&nbsp;framework&nbsp;(this&nbsp;list&nbsp;deviates&nbsp;a&nbsp;bit&nbsp;from&nbsp;the&nbsp;original&nbsp;definitions&nbsp;as&nbsp;found<br>in&nbsp;the&nbsp;Python&nbsp;library&nbsp;documentation.&nbsp;1).<br>1&nbsp;http://www.python.org/doc/current/lib/module-unittest.html<br><hr><A name=3></a>44.2.&nbsp;WRITING&nbsp;THE&nbsp;UNIT&nbsp;TESTS<br>3<br>The&nbsp;smallest&nbsp;unit&nbsp;is&nbsp;obviously&nbsp;the&nbsp;“test”,&nbsp;which&nbsp;is&nbsp;a&nbsp;single&nbsp;method&nbsp;in&nbsp;a&nbsp;TestCase&nbsp;class&nbsp;that<br>tests&nbsp;the&nbsp;behavior&nbsp;of&nbsp;a&nbsp;small&nbsp;piece&nbsp;of&nbsp;code&nbsp;or&nbsp;a&nbsp;particular&nbsp;aspect&nbsp;of&nbsp;an&nbsp;implementation.&nbsp;The&nbsp;“test<br>case”&nbsp;is&nbsp;then&nbsp;a&nbsp;collection&nbsp;tests&nbsp;that&nbsp;share&nbsp;the&nbsp;same&nbsp;setup/inputs.&nbsp;On&nbsp;top&nbsp;of&nbsp;all&nbsp;of&nbsp;this&nbsp;sits&nbsp;the&nbsp;“test<br>suite”&nbsp;which&nbsp;is&nbsp;a&nbsp;collection&nbsp;of&nbsp;test&nbsp;cases&nbsp;and/or&nbsp;other&nbsp;test&nbsp;suites.&nbsp;Test&nbsp;suites&nbsp;combine&nbsp;tests&nbsp;that<br>should&nbsp;be&nbsp;executed&nbsp;together.&nbsp;With&nbsp;the&nbsp;correct&nbsp;setup&nbsp;(as&nbsp;shown&nbsp;in&nbsp;the&nbsp;example&nbsp;below),&nbsp;you&nbsp;can<br>then&nbsp;execute&nbsp;test&nbsp;suites.&nbsp;For&nbsp;large&nbsp;projects&nbsp;like&nbsp;Zope&nbsp;3,&nbsp;it&nbsp;is&nbsp;useful&nbsp;to&nbsp;know&nbsp;that&nbsp;there&nbsp;is&nbsp;also&nbsp;the<br>concept&nbsp;of&nbsp;a&nbsp;test&nbsp;runner,&nbsp;which&nbsp;manages&nbsp;the&nbsp;test&nbsp;run&nbsp;of&nbsp;all&nbsp;or&nbsp;a&nbsp;set&nbsp;of&nbsp;tests.&nbsp;The&nbsp;runner&nbsp;provides<br>useful&nbsp;feedback&nbsp;to&nbsp;the&nbsp;application,&nbsp;so&nbsp;that&nbsp;various&nbsp;user&nbsp;interaces&nbsp;can&nbsp;be&nbsp;developed&nbsp;on&nbsp;top&nbsp;of&nbsp;it.<br>But&nbsp;enough&nbsp;about&nbsp;the&nbsp;theory.&nbsp;In&nbsp;the&nbsp;following&nbsp;example,&nbsp;which&nbsp;you&nbsp;can&nbsp;simply&nbsp;put&nbsp;into&nbsp;the&nbsp;same<br>file&nbsp;as&nbsp;your&nbsp;code&nbsp;above,&nbsp;you&nbsp;will&nbsp;see&nbsp;a&nbsp;test&nbsp;in&nbsp;common&nbsp;Zope&nbsp;3&nbsp;style.<br>1&nbsp;import&nbsp;unittest<br>2<br>3&nbsp;class&nbsp;SampleTest(unittest.TestCase):<br>4<br>&quot;&quot;&quot;Test&nbsp;the&nbsp;Sample&nbsp;class&quot;&quot;&quot;<br>5<br>6<br>def&nbsp;test_title(self):<br>7<br>sample&nbsp;=&nbsp;Sample()<br>8<br>self.assertEqual(sample.title,&nbsp;None)<br>9<br>sample.title&nbsp;=&nbsp;’Sample&nbsp;Title’<br>10<br>self.assertEqual(sample.title,&nbsp;’Sample&nbsp;Title’)<br>11<br>12<br>def&nbsp;test_getDescription(self):<br>13<br>sample&nbsp;=&nbsp;Sample()<br>14<br>self.assertEqual(sample.getDescription(),&nbsp;’’)<br>15<br>sample._description&nbsp;=&nbsp;&quot;Description&quot;<br>16<br>self.assertEqual(sample.getDescription(),&nbsp;’Description’)<br>17<br>18<br>def&nbsp;test_setDescription(self):<br>19<br>sample&nbsp;=&nbsp;Sample()<br>20<br>self.assertEqual(sample._description,&nbsp;’’)<br>21<br>sample.setDescription(’Description’)<br>22<br>self.assertEqual(sample._description,&nbsp;’Description’)<br>23<br>sample.setDescription(u’Description2’)<br>24<br>self.assertEqual(sample._description,&nbsp;u’Description2’)<br>25<br>self.assertRaises(AssertionError,&nbsp;sample.setDescription,&nbsp;None)<br>26<br>27<br>28&nbsp;def&nbsp;test_suite():<br>29<br>return&nbsp;unittest.TestSuite((<br>30<br>unittest.makeSuite(SampleTest),<br>31<br>))<br>32<br>33&nbsp;if&nbsp;__name__&nbsp;==&nbsp;’__main__’:<br>34<br>unittest.main(defaultTest=’test_suite’)<br>Line&nbsp;3–4:&nbsp;We&nbsp;usually&nbsp;develop&nbsp;test&nbsp;classes&nbsp;which&nbsp;must&nbsp;inherit&nbsp;from&nbsp;TestCase.&nbsp;While&nbsp;often&nbsp;not<br>done,&nbsp;it&nbsp;is&nbsp;a&nbsp;good&nbsp;idea&nbsp;to&nbsp;give&nbsp;the&nbsp;class&nbsp;a&nbsp;meaningful&nbsp;docstring&nbsp;that&nbsp;describes&nbsp;the&nbsp;purpose&nbsp;of&nbsp;the<br>tests&nbsp;it&nbsp;includes.<br>Line&nbsp;6,&nbsp;12&nbsp;&amp;&nbsp;18:&nbsp;When&nbsp;a&nbsp;test&nbsp;case&nbsp;is&nbsp;run,&nbsp;a&nbsp;method&nbsp;called&nbsp;runTests()&nbsp;is&nbsp;executed.&nbsp;While&nbsp;it<br>is&nbsp;possible&nbsp;to&nbsp;overrride&nbsp;this&nbsp;method&nbsp;to&nbsp;run&nbsp;tests&nbsp;differently,&nbsp;the&nbsp;default&nbsp;option&nbsp;will&nbsp;look&nbsp;for&nbsp;any<br>method&nbsp;whose&nbsp;name&nbsp;starts&nbsp;with&nbsp;test&nbsp;and&nbsp;execute&nbsp;it&nbsp;as&nbsp;a&nbsp;single&nbsp;test.&nbsp;This&nbsp;way&nbsp;we&nbsp;can&nbsp;create<br>a&nbsp;“test&nbsp;method”&nbsp;for&nbsp;each&nbsp;aspect,&nbsp;method,&nbsp;function&nbsp;or&nbsp;property&nbsp;of&nbsp;the&nbsp;code&nbsp;to&nbsp;be&nbsp;tested.&nbsp;This<br>default&nbsp;is&nbsp;very&nbsp;sensible&nbsp;and&nbsp;is&nbsp;used&nbsp;everywhere&nbsp;in&nbsp;Zope&nbsp;3.<br><hr><A name=4></a>4<br>CHAPTER&nbsp;44.&nbsp;WRITING&nbsp;BASIC&nbsp;UNIT&nbsp;TESTS<br>Note&nbsp;that&nbsp;there&nbsp;is&nbsp;no&nbsp;docstring&nbsp;for&nbsp;test&nbsp;methods.&nbsp;This&nbsp;is&nbsp;intentional.&nbsp;If&nbsp;a&nbsp;docstring&nbsp;is&nbsp;specified,<br>it&nbsp;is&nbsp;used&nbsp;instead&nbsp;of&nbsp;the&nbsp;method&nbsp;name&nbsp;to&nbsp;identify&nbsp;the&nbsp;test.&nbsp;When&nbsp;specifying&nbsp;a&nbsp;docstring,&nbsp;we&nbsp;have<br>noticed&nbsp;that&nbsp;it&nbsp;is&nbsp;very&nbsp;difficult&nbsp;to&nbsp;identify&nbsp;the&nbsp;test&nbsp;later;&nbsp;therefore&nbsp;the&nbsp;method&nbsp;name&nbsp;is&nbsp;a&nbsp;much<br>better&nbsp;choice.<br>Line&nbsp;8,&nbsp;10,&nbsp;14,&nbsp;.&nbsp;.&nbsp;.&nbsp;:&nbsp;The&nbsp;TestCase&nbsp;class&nbsp;implements&nbsp;a&nbsp;handful&nbsp;of&nbsp;methods&nbsp;that&nbsp;aid&nbsp;you&nbsp;with&nbsp;the<br>testing.&nbsp;Here&nbsp;are&nbsp;some&nbsp;of&nbsp;the&nbsp;most&nbsp;frequently&nbsp;used&nbsp;ones.&nbsp;For&nbsp;a&nbsp;complete&nbsp;list&nbsp;see&nbsp;the&nbsp;standard<br>Python&nbsp;documentation&nbsp;referenced&nbsp;above.<br>&nbsp;assertEqual(first,second[,msg])<br>Checks&nbsp;whether&nbsp;the&nbsp;first&nbsp;and&nbsp;second&nbsp;value&nbsp;are&nbsp;equal.&nbsp;If&nbsp;the&nbsp;test&nbsp;fails,&nbsp;the&nbsp;msg&nbsp;or&nbsp;None<br>is&nbsp;returned.<br>&nbsp;assertNotEqual(first,second[,msg])<br>This&nbsp;is&nbsp;simply&nbsp;the&nbsp;opposite&nbsp;to&nbsp;assertEqual()&nbsp;by&nbsp;checking&nbsp;for&nbsp;non-equality.<br>&nbsp;assertRaises(exception,callable,...)<br>You&nbsp;expect&nbsp;the&nbsp;callable&nbsp;to&nbsp;raise&nbsp;exception&nbsp;when&nbsp;executed.&nbsp;After&nbsp;the&nbsp;callable&nbsp;you&nbsp;can<br>specify&nbsp;any&nbsp;amount&nbsp;of&nbsp;positional&nbsp;and&nbsp;keyword&nbsp;arguments&nbsp;for&nbsp;the&nbsp;callable.&nbsp;If&nbsp;you&nbsp;expect<br>a&nbsp;group&nbsp;of&nbsp;exceptions&nbsp;from&nbsp;the&nbsp;execution,&nbsp;you&nbsp;can&nbsp;make&nbsp;exception&nbsp;a&nbsp;tuple&nbsp;of&nbsp;possible<br>exceptions.<br>&nbsp;assert&nbsp;(expr[,msg])<br>Assert&nbsp;checks&nbsp;whether&nbsp;the&nbsp;specified&nbsp;expression&nbsp;executes&nbsp;correctly.&nbsp;If&nbsp;not,&nbsp;the&nbsp;test&nbsp;fails&nbsp;and<br>msg&nbsp;or&nbsp;None&nbsp;is&nbsp;returned.<br>&nbsp;failUnlessEqual()<br>This&nbsp;testing&nbsp;method&nbsp;is&nbsp;equivalent&nbsp;to&nbsp;assertEqual().<br>&nbsp;failUnless(expr[,msg])<br>This&nbsp;method&nbsp;is&nbsp;equivalent&nbsp;to&nbsp;assert&nbsp;(expr[,msg]).<br>&nbsp;failif()<br>This&nbsp;is&nbsp;the&nbsp;opposite&nbsp;to&nbsp;failUnless().<br>&nbsp;fail([msg])<br>Fails&nbsp;the&nbsp;running&nbsp;test&nbsp;without&nbsp;any&nbsp;evaluation.&nbsp;This&nbsp;is&nbsp;commonly&nbsp;used&nbsp;when&nbsp;testing&nbsp;various<br>possible&nbsp;execution&nbsp;paths&nbsp;at&nbsp;once&nbsp;and&nbsp;you&nbsp;would&nbsp;like&nbsp;to&nbsp;signify&nbsp;a&nbsp;failure&nbsp;if&nbsp;an&nbsp;improper&nbsp;path<br>was&nbsp;taken.<br>Line&nbsp;6–10:&nbsp;This&nbsp;method&nbsp;tests&nbsp;the&nbsp;title&nbsp;attribute&nbsp;of&nbsp;the&nbsp;Sample&nbsp;class.&nbsp;The&nbsp;first&nbsp;test&nbsp;should<br>be&nbsp;of&nbsp;course&nbsp;that&nbsp;the&nbsp;attribute&nbsp;exists&nbsp;and&nbsp;has&nbsp;the&nbsp;expected&nbsp;initial&nbsp;value&nbsp;(line&nbsp;8).&nbsp;Then&nbsp;the&nbsp;title<br>attribute&nbsp;is&nbsp;changed&nbsp;and&nbsp;we&nbsp;check&nbsp;whether&nbsp;the&nbsp;value&nbsp;was&nbsp;really&nbsp;stored.&nbsp;This&nbsp;might&nbsp;seem&nbsp;like<br>overkill,&nbsp;but&nbsp;later&nbsp;you&nbsp;might&nbsp;change&nbsp;the&nbsp;title&nbsp;in&nbsp;a&nbsp;way&nbsp;that&nbsp;it&nbsp;uses&nbsp;properties&nbsp;instead.&nbsp;Then&nbsp;it<br>becomes&nbsp;very&nbsp;important&nbsp;to&nbsp;check&nbsp;whether&nbsp;this&nbsp;test&nbsp;still&nbsp;passes.<br>Line&nbsp;12–16:&nbsp;First&nbsp;we&nbsp;simply&nbsp;check&nbsp;that&nbsp;getDescription()&nbsp;returns&nbsp;the&nbsp;correct&nbsp;default&nbsp;value.<br>Since&nbsp;we&nbsp;do&nbsp;not&nbsp;want&nbsp;to&nbsp;use&nbsp;other&nbsp;API&nbsp;calls&nbsp;like&nbsp;setDescription()&nbsp;we&nbsp;set&nbsp;a&nbsp;new&nbsp;value&nbsp;of&nbsp;the<br>description&nbsp;via&nbsp;the&nbsp;implementation-internal&nbsp;description&nbsp;attribute&nbsp;(line&nbsp;15).&nbsp;This&nbsp;is&nbsp;okay!&nbsp;Unit<br>tests&nbsp;can&nbsp;make&nbsp;use&nbsp;of&nbsp;implementation-specific&nbsp;attributes&nbsp;and&nbsp;methods.&nbsp;Finally&nbsp;we&nbsp;just&nbsp;check&nbsp;that<br>the&nbsp;correct&nbsp;value&nbsp;is&nbsp;returned.<br><hr><A name=5></a>44.3.&nbsp;RUNNING&nbsp;THE&nbsp;TESTS<br>5<br>Line&nbsp;18–25:&nbsp;On&nbsp;line&nbsp;21–24&nbsp;it&nbsp;is&nbsp;checked&nbsp;that&nbsp;both&nbsp;regular&nbsp;and&nbsp;unicode&nbsp;strings&nbsp;are&nbsp;set&nbsp;correctly.<br>In&nbsp;the&nbsp;last&nbsp;line&nbsp;of&nbsp;the&nbsp;test&nbsp;we&nbsp;make&nbsp;sure&nbsp;that&nbsp;no&nbsp;other&nbsp;type&nbsp;of&nbsp;objects&nbsp;can&nbsp;be&nbsp;set&nbsp;as&nbsp;a&nbsp;description<br>and&nbsp;that&nbsp;an&nbsp;error&nbsp;is&nbsp;raised.<br>28–31:&nbsp;This&nbsp;method&nbsp;returns&nbsp;a&nbsp;test&nbsp;suite&nbsp;that&nbsp;includes&nbsp;all&nbsp;test&nbsp;cases&nbsp;created&nbsp;in&nbsp;this&nbsp;module.&nbsp;It&nbsp;is<br>used&nbsp;by&nbsp;the&nbsp;Zope&nbsp;3&nbsp;test&nbsp;runner&nbsp;when&nbsp;it&nbsp;picks&nbsp;up&nbsp;all&nbsp;available&nbsp;tests.&nbsp;You&nbsp;would&nbsp;basically&nbsp;add&nbsp;the<br>line&nbsp;unittest.makeSuite(TestCaseClass)&nbsp;for&nbsp;each&nbsp;additional&nbsp;test&nbsp;case.<br>33–34:&nbsp;In&nbsp;order&nbsp;to&nbsp;make&nbsp;the&nbsp;test&nbsp;module&nbsp;runnable&nbsp;by&nbsp;itself,&nbsp;you&nbsp;can&nbsp;execute&nbsp;unittest.main()<br>when&nbsp;the&nbsp;module&nbsp;is&nbsp;run.<br>44.3<br>Running&nbsp;the&nbsp;Tests<br>You&nbsp;can&nbsp;run&nbsp;the&nbsp;test&nbsp;by&nbsp;simply&nbsp;calling&nbsp;pythontest&nbsp;sample.py&nbsp;from&nbsp;the&nbsp;directory&nbsp;you&nbsp;saved&nbsp;the<br>file&nbsp;in.&nbsp;Here&nbsp;is&nbsp;the&nbsp;result&nbsp;you&nbsp;should&nbsp;see:<br>.<br>--------------------------------------------------------------------<br>n&nbsp;3&nbsp;tests&nbsp;in&nbsp;0.001s<br>The&nbsp;three&nbsp;dots&nbsp;represent&nbsp;the&nbsp;three&nbsp;tests&nbsp;that&nbsp;were&nbsp;run.&nbsp;If&nbsp;a&nbsp;test&nbsp;had&nbsp;failed,&nbsp;it&nbsp;would&nbsp;have&nbsp;been<br>reported&nbsp;pointing&nbsp;out&nbsp;the&nbsp;failing&nbsp;test&nbsp;and&nbsp;providing&nbsp;a&nbsp;small&nbsp;traceback.<br>When&nbsp;using&nbsp;the&nbsp;default&nbsp;Zope&nbsp;3&nbsp;test&nbsp;runner,&nbsp;tests&nbsp;will&nbsp;be&nbsp;picked&nbsp;up&nbsp;as&nbsp;long&nbsp;as&nbsp;they&nbsp;follow&nbsp;some<br>conventions.<br>&nbsp;The&nbsp;tests&nbsp;must&nbsp;either&nbsp;be&nbsp;in&nbsp;a&nbsp;package&nbsp;or&nbsp;be&nbsp;a&nbsp;module&nbsp;called&nbsp;tests.<br>&nbsp;If&nbsp;tests&nbsp;is&nbsp;a&nbsp;package,&nbsp;then&nbsp;all&nbsp;test&nbsp;modules&nbsp;inside&nbsp;must&nbsp;also&nbsp;have&nbsp;a&nbsp;name&nbsp;starting&nbsp;with&nbsp;test,<br>as&nbsp;it&nbsp;is&nbsp;the&nbsp;case&nbsp;with&nbsp;our&nbsp;name&nbsp;test&nbsp;sample.py.<br>&nbsp;The&nbsp;test&nbsp;module&nbsp;must&nbsp;be&nbsp;somewhere&nbsp;in&nbsp;the&nbsp;Zope&nbsp;3&nbsp;source&nbsp;tree,&nbsp;since&nbsp;the&nbsp;test&nbsp;runner&nbsp;looks<br>only&nbsp;for&nbsp;files&nbsp;there.<br>In&nbsp;our&nbsp;case,&nbsp;you&nbsp;could&nbsp;simply&nbsp;create&nbsp;a&nbsp;tests&nbsp;package&nbsp;in&nbsp;ZOPE3/src&nbsp;(do&nbsp;not&nbsp;forget&nbsp;the<br>init&nbsp;.<br>py&nbsp;file).&nbsp;Then&nbsp;place&nbsp;the&nbsp;test&nbsp;sample.py&nbsp;file&nbsp;into&nbsp;this&nbsp;directory.<br>You&nbsp;you&nbsp;can&nbsp;use&nbsp;the&nbsp;test&nbsp;runner&nbsp;to&nbsp;run&nbsp;only&nbsp;the&nbsp;sample&nbsp;tests&nbsp;as&nbsp;follows&nbsp;from&nbsp;the&nbsp;Zope&nbsp;3&nbsp;root<br>directory:<br>python&nbsp;test.py&nbsp;-vp&nbsp;tests.test_sample<br>The&nbsp;-v&nbsp;option&nbsp;stands&nbsp;for&nbsp;verbose&nbsp;mode,&nbsp;so&nbsp;that&nbsp;detailed&nbsp;information&nbsp;about&nbsp;a&nbsp;test&nbsp;failure&nbsp;is<br>provided.&nbsp;The&nbsp;-p&nbsp;option&nbsp;enables&nbsp;a&nbsp;progress&nbsp;bar&nbsp;that&nbsp;tells&nbsp;you&nbsp;how&nbsp;many&nbsp;tests&nbsp;out&nbsp;of&nbsp;all&nbsp;have&nbsp;been<br>completed.&nbsp;There&nbsp;are&nbsp;many&nbsp;more&nbsp;options&nbsp;that&nbsp;can&nbsp;be&nbsp;specified.&nbsp;You&nbsp;can&nbsp;get&nbsp;a&nbsp;full&nbsp;list&nbsp;of&nbsp;them&nbsp;with<br>the&nbsp;option&nbsp;-h:&nbsp;pythontest.py-h.<br>The&nbsp;output&nbsp;of&nbsp;the&nbsp;call&nbsp;above&nbsp;is&nbsp;as&nbsp;follows:<br>nfiguration&nbsp;file&nbsp;found.<br>nning&nbsp;UNIT&nbsp;tests&nbsp;at&nbsp;level&nbsp;1<br>nning&nbsp;UNIT&nbsp;tests&nbsp;from&nbsp;/opt/zope/Zope3<br>3/3&nbsp;(100.0%):&nbsp;test_title&nbsp;(tests.test_sample.SampleTest)<br>--------------------------------------------------------------------<br>n&nbsp;3&nbsp;tests&nbsp;in&nbsp;0.002s<br><hr><A name=6></a>6<br>CHAPTER&nbsp;44.&nbsp;WRITING&nbsp;BASIC&nbsp;UNIT&nbsp;TESTS<br>nning&nbsp;FUNCTIONAL&nbsp;tests&nbsp;at&nbsp;level&nbsp;1<br>nning&nbsp;FUNCTIONAL&nbsp;tests&nbsp;from&nbsp;/opt/zope/Zope3<br>--------------------------------------------------------------------<br>n&nbsp;0&nbsp;tests&nbsp;in&nbsp;0.000s<br>Line&nbsp;1:&nbsp;The&nbsp;test&nbsp;runner&nbsp;uses&nbsp;a&nbsp;configuration&nbsp;file&nbsp;for&nbsp;some&nbsp;setup.&nbsp;This&nbsp;allows&nbsp;developers&nbsp;to&nbsp;use<br>the&nbsp;test&nbsp;runner&nbsp;for&nbsp;other&nbsp;projects&nbsp;as&nbsp;well.&nbsp;This&nbsp;message&nbsp;simply&nbsp;tells&nbsp;us&nbsp;that&nbsp;the&nbsp;configuration&nbsp;file<br>was&nbsp;found.<br>Line&nbsp;2–8:&nbsp;The&nbsp;unit&nbsp;tests&nbsp;are&nbsp;run.&nbsp;On&nbsp;line&nbsp;4&nbsp;you&nbsp;can&nbsp;see&nbsp;the&nbsp;progress&nbsp;bar.<br>Line&nbsp;9–15:&nbsp;The&nbsp;functional&nbsp;tests&nbsp;are&nbsp;run,&nbsp;since&nbsp;the&nbsp;default&nbsp;test&nbsp;runner&nbsp;runs&nbsp;both&nbsp;types&nbsp;of&nbsp;tests.<br>Since&nbsp;we&nbsp;do&nbsp;not&nbsp;have&nbsp;any&nbsp;functional&nbsp;tests&nbsp;in&nbsp;the&nbsp;specified&nbsp;module,&nbsp;there&nbsp;are&nbsp;no&nbsp;tests&nbsp;to&nbsp;run.&nbsp;To<br>just&nbsp;run&nbsp;the&nbsp;unit&nbsp;tests,&nbsp;use&nbsp;option&nbsp;-u&nbsp;and&nbsp;-f&nbsp;for&nbsp;just&nbsp;running&nbsp;the&nbsp;functional&nbsp;tests.&nbsp;See&nbsp;“Writing<br>Functional&nbsp;Tests”&nbsp;for&nbsp;more&nbsp;detials&nbsp;on&nbsp;functional&nbsp;tests.<br><hr><A name=7></a>44.3.&nbsp;RUNNING&nbsp;THE&nbsp;TESTS<br>7<br>Exercises<br>1.&nbsp;It&nbsp;is&nbsp;not&nbsp;very&nbsp;common&nbsp;to&nbsp;do&nbsp;the&nbsp;setup&nbsp;&nbsp;in&nbsp;our&nbsp;case&nbsp;sample=Sample()&nbsp;&nbsp;in&nbsp;every&nbsp;test<br>method.&nbsp;Instead&nbsp;there&nbsp;exists&nbsp;a&nbsp;method&nbsp;called&nbsp;setUp()&nbsp;and&nbsp;its&nbsp;counterpart&nbsp;tearDown&nbsp;that<br>are&nbsp;run&nbsp;before&nbsp;and&nbsp;after&nbsp;each&nbsp;test,&nbsp;respectively.&nbsp;Change&nbsp;the&nbsp;test&nbsp;code&nbsp;above,&nbsp;so&nbsp;that&nbsp;it&nbsp;uses<br>the&nbsp;setUp()&nbsp;method.&nbsp;In&nbsp;later&nbsp;chapters&nbsp;and&nbsp;the&nbsp;rest&nbsp;of&nbsp;the&nbsp;book&nbsp;we&nbsp;will&nbsp;frequently&nbsp;use&nbsp;this<br>method&nbsp;of&nbsp;setting&nbsp;up&nbsp;tests.<br>2.&nbsp;Currently&nbsp;the&nbsp;test&nbsp;setDescription()&nbsp;test&nbsp;only&nbsp;verifies&nbsp;that&nbsp;None&nbsp;is&nbsp;not&nbsp;allowed&nbsp;as&nbsp;input<br>value.<br>(a)&nbsp;Improve&nbsp;the&nbsp;test,&nbsp;so&nbsp;that&nbsp;all&nbsp;other&nbsp;builtin&nbsp;types&nbsp;are&nbsp;tested&nbsp;as&nbsp;well.<br>(b)&nbsp;Also,&nbsp;make&nbsp;sure&nbsp;that&nbsp;any&nbsp;objects&nbsp;inheriting&nbsp;from&nbsp;str&nbsp;or&nbsp;unicode&nbsp;pass&nbsp;as&nbsp;valid&nbsp;values.<br><hr>
\ No newline at end of file
<A name=1></a>Chapter 44<br>
Writing Basic Unit Tests<br>
Difficulty<br>
Newcomer<br>
Skills<br>
• All you need to know is some Python.<br>
Problem/Task<br>
As you know by now, Zope 3 gains its incredible stability from testing any code in great detail. The<br>currently most common method is to write unit tests. This chapter introduces unit tests – which<br>are Zope 3 independent – and introduces some of the subtleties.<br>
Solution<br>
44.1<br>
Implementing the Sample Class<br>
Before we can write tests, we have to write some code that we can test. Here, we will implement<br>a simple class called Sample with a public attribute title and description that is accessed<br>via getDescription() and mutated using setDescription(). Further, the description must be<br>either a regular or unicode string.<br>
Since this code will not depend on Zope, open a file named test sample.py anywhere and add<br>
the following class:<br>
1 Sample(object):<br>
2<br>
&quot;&quot;&quot;A trivial Sample object.&quot;&quot;&quot;<br>
3<br>
4<br>
title = None<br>
5<br>
6<br>
def __init__(self):<br>
7<br>
&quot;&quot;&quot;Initialize object.&quot;&quot;&quot;<br>
8<br>
self._description = ’’<br>
9<br>
1<br>
<hr>
<A name=2></a>2<br>
CHAPTER 44. WRITING BASIC UNIT TESTS<br>
10<br>
def setDescription(self, value):<br>
11<br>
&quot;&quot;&quot;Change the value of the description.&quot;&quot;&quot;<br>
12<br>
assert isinstance(value, (str, unicode))<br>
13<br>
self._description = value<br>
14<br>
15<br>
def getDescription(self):<br>
16<br>
&quot;&quot;&quot;Change the value of the description.&quot;&quot;&quot;<br>
17<br>
return self._description<br>
Line 4: The title is just publicly declared and a value of None is given. Therefore this is just<br>a regular attribute.<br>
Line 8: The actual description string will be stored in description.<br>
Line 12: Make sure that the description is only a regular or unicode string, like it was stated in<br>the requirements.<br>
If you wish you can now manually test the class with the interactive Python shell. Just start<br>
Python by entering python in your shell prompt. Note that you should be in the directory in<br>which test sample.py is located when starting Python (an alternative is of course to specify the<br>directory in your PYTHONPATH.)<br>
1 &gt;&gt;&gt; from test_sample import Sample<br>2 &gt;&gt;&gt; sample = Sample()<br>
3 &gt;&gt;&gt; print sample.title<br>4 None<br>
5 &gt;&gt;&gt; sample.title = ’Title’<br>
6 &gt;&gt;&gt; print sample.title<br>7 Title<br>
8 &gt;&gt;&gt; print sample.getDescription()<br>9<br>
10 &gt;&gt;&gt; sample.setDescription(’Hello World’)<br>
11 &gt;&gt;&gt; print sample.getDescription()<br>12 Hello World<br>
13 &gt;&gt;&gt; sample.setDescription(None)<br>
14 Traceback (most recent call last):<br>
15<br>
File &quot;&lt;stdin&gt;&quot;, line 1, in ?<br>
16<br>
File &quot;test_sample.py&quot;, line 31, in setDescription<br>
17<br>
assert isinstance(value, (str, unicode))<br>
18 AssertionError<br>
As you can see in the last test, non-string object types are not allowed as descriptions and an<br>
AssertionError is raised.<br>
44.2<br>
Writing the Unit Tests<br>
The goal of writing the unit tests is to convert this informal, manual, and interactive testing session<br>into a formal test class. Python provides already a module called unittest for this purpose, which<br>is a port of the Java-based unit testing product, JUnit, by Kent Beck and Erich Gamma. There are<br>three levels to the testing framework (this list deviates a bit from the original definitions as found<br>in the Python library documentation. 1).<br>
1 http://www.python.org/doc/current/lib/module-unittest.html<br>
<hr>
<A name=3></a>44.2. WRITING THE UNIT TESTS<br>
3<br>
The smallest unit is obviously the “test”, which is a single method in a TestCase class that<br>
tests the behavior of a small piece of code or a particular aspect of an implementation. The “test<br>case” is then a collection tests that share the same setup/inputs. On top of all of this sits the “test<br>suite” which is a collection of test cases and/or other test suites. Test suites combine tests that<br>should be executed together. With the correct setup (as shown in the example below), you can<br>then execute test suites. For large projects like Zope 3, it is useful to know that there is also the<br>concept of a test runner, which manages the test run of all or a set of tests. The runner provides<br>useful feedback to the application, so that various user interaces can be developed on top of it.<br>
But enough about the theory. In the following example, which you can simply put into the same<br>
file as your code above, you will see a test in common Zope 3 style.<br>
1 import unittest<br>2<br>
3 class SampleTest(unittest.TestCase):<br>4<br>
&quot;&quot;&quot;Test the Sample class&quot;&quot;&quot;<br>
5<br>
6<br>
def test_title(self):<br>
7<br>
sample = Sample()<br>
8<br>
self.assertEqual(sample.title, None)<br>
9<br>
sample.title = ’Sample Title’<br>
10<br>
self.assertEqual(sample.title, ’Sample Title’)<br>
11<br>
12<br>
def test_getDescription(self):<br>
13<br>
sample = Sample()<br>
14<br>
self.assertEqual(sample.getDescription(), ’’)<br>
15<br>
sample._description = &quot;Description&quot;<br>
16<br>
self.assertEqual(sample.getDescription(), ’Description’)<br>
17<br>
18<br>
def test_setDescription(self):<br>
19<br>
sample = Sample()<br>
20<br>
self.assertEqual(sample._description, ’’)<br>
21<br>
sample.setDescription(’Description’)<br>
22<br>
self.assertEqual(sample._description, ’Description’)<br>
23<br>
sample.setDescription(u’Description2’)<br>
24<br>
self.assertEqual(sample._description, u’Description2’)<br>
25<br>
self.assertRaises(AssertionError, sample.setDescription, None)<br>
26<br>
27<br>
28 def test_suite():<br>29<br>
return unittest.TestSuite((<br>
30<br>
unittest.makeSuite(SampleTest),<br>
31<br>
))<br>
32<br>
33 if __name__ == ’__main__’:<br>34<br>
unittest.main(defaultTest=’test_suite’)<br>
Line 3–4: We usually develop test classes which must inherit from TestCase. While often not<br>done, it is a good idea to give the class a meaningful docstring that describes the purpose of the<br>tests it includes.<br>
Line 6, 12 &amp; 18: When a test case is run, a method called runTests() is executed. While it<br>is possible to overrride this method to run tests differently, the default option will look for any<br>method whose name starts with test and execute it as a single test. This way we can create<br>a “test method” for each aspect, method, function or property of the code to be tested. This<br>default is very sensible and is used everywhere in Zope 3.<br>
<hr>
<A name=4></a>4<br>
CHAPTER 44. WRITING BASIC UNIT TESTS<br>
Note that there is no docstring for test methods. This is intentional. If a docstring is specified,<br>it is used instead of the method name to identify the test. When specifying a docstring, we have<br>noticed that it is very difficult to identify the test later; therefore the method name is a much<br>better choice.<br>
Line 8, 10, 14, . . . : The TestCase class implements a handful of methods that aid you with the<br>testing. Here are some of the most frequently used ones. For a complete list see the standard<br>Python documentation referenced above.<br>
• assertEqual(first,second[,msg])<br>
Checks whether the first and second value are equal. If the test fails, the msg or None<br>is returned.<br>
• assertNotEqual(first,second[,msg])<br>
This is simply the opposite to assertEqual() by checking for non-equality.<br>
• assertRaises(exception,callable,...)<br>
You expect the callable to raise exception when executed. After the callable you can<br>specify any amount of positional and keyword arguments for the callable. If you expect<br>a group of exceptions from the execution, you can make exception a tuple of possible<br>exceptions.<br>
• assert (expr[,msg])<br>
Assert checks whether the specified expression executes correctly. If not, the test fails and<br>msg or None is returned.<br>
• failUnlessEqual()<br>
This testing method is equivalent to assertEqual().<br>
• failUnless(expr[,msg])<br>
This method is equivalent to assert (expr[,msg]).<br>
• failif()<br>
This is the opposite to failUnless().<br>
• fail([msg])<br>
Fails the running test without any evaluation. This is commonly used when testing various<br>possible execution paths at once and you would like to signify a failure if an improper path<br>was taken.<br>
Line 6–10: This method tests the title attribute of the Sample class. The first test should<br>be of course that the attribute exists and has the expected initial value (line 8). Then the title<br>attribute is changed and we check whether the value was really stored. This might seem like<br>overkill, but later you might change the title in a way that it uses properties instead. Then it<br>becomes very important to check whether this test still passes.<br>
Line 12–16: First we simply check that getDescription() returns the correct default value.<br>Since we do not want to use other API calls like setDescription() we set a new value of the<br>description via the implementation-internal description attribute (line 15). This is okay! Unit<br>tests can make use of implementation-specific attributes and methods. Finally we just check that<br>the correct value is returned.<br>
<hr>
<A name=5></a>44.3. RUNNING THE TESTS<br>
5<br>
Line 18–25: On line 21–24 it is checked that both regular and unicode strings are set correctly.<br>In the last line of the test we make sure that no other type of objects can be set as a description<br>and that an error is raised.<br>
28–31: This method returns a test suite that includes all test cases created in this module. It is<br>used by the Zope 3 test runner when it picks up all available tests. You would basically add the<br>line unittest.makeSuite(TestCaseClass) for each additional test case.<br>
33–34: In order to make the test module runnable by itself, you can execute unittest.main()<br>when the module is run.<br>
44.3<br>
Running the Tests<br>
You can run the test by simply calling pythontest sample.py from the directory you saved the<br>file in. Here is the result you should see:<br>
.<br>--------------------------------------------------------------------<br>n 3 tests in 0.001s<br>
The three dots represent the three tests that were run. If a test had failed, it would have been<br>
reported pointing out the failing test and providing a small traceback.<br>
When using the default Zope 3 test runner, tests will be picked up as long as they follow some<br>
conventions.<br>
• The tests must either be in a package or be a module called tests.<br>
• If tests is a package, then all test modules inside must also have a name starting with test,<br>
as it is the case with our name test sample.py.<br>
• The test module must be somewhere in the Zope 3 source tree, since the test runner looks<br>
only for files there.<br>
In our case, you could simply create a tests package in ZOPE3/src (do not forget the<br>
init .<br>
py file). Then place the test sample.py file into this directory.<br>
You you can use the test runner to run only the sample tests as follows from the Zope 3 root<br>
directory:<br>
python test.py -vp tests.test_sample<br>
The -v option stands for verbose mode, so that detailed information about a test failure is<br>
provided. The -p option enables a progress bar that tells you how many tests out of all have been<br>completed. There are many more options that can be specified. You can get a full list of them with<br>the option -h: pythontest.py-h.<br>
The output of the call above is as follows:<br>
nfiguration file found.<br>nning UNIT tests at level 1<br>nning UNIT tests from /opt/zope/Zope3<br>
3/3 (100.0%): test_title (tests.test_sample.SampleTest)<br>
--------------------------------------------------------------------<br>n 3 tests in 0.002s<br>
<hr>
<A name=6></a>6<br>
CHAPTER 44. WRITING BASIC UNIT TESTS<br>
nning FUNCTIONAL tests at level 1<br>nning FUNCTIONAL tests from /opt/zope/Zope3<br>
--------------------------------------------------------------------<br>n 0 tests in 0.000s<br>
Line 1: The test runner uses a configuration file for some setup. This allows developers to use<br>the test runner for other projects as well. This message simply tells us that the configuration file<br>was found.<br>
Line 2–8: The unit tests are run. On line 4 you can see the progress bar.<br>
Line 9–15: The functional tests are run, since the default test runner runs both types of tests.<br>Since we do not have any functional tests in the specified module, there are no tests to run. To<br>just run the unit tests, use option -u and -f for just running the functional tests. See “Writing<br>Functional Tests” for more detials on functional tests.<br>
<hr>
<A name=7></a>44.3. RUNNING THE TESTS<br>
7<br>
Exercises<br>
1. It is not very common to do the setup – in our case sample=Sample() – in every test<br>
method. Instead there exists a method called setUp() and its counterpart tearDown that<br>are run before and after each test, respectively. Change the test code above, so that it uses<br>the setUp() method. In later chapters and the rest of the book we will frequently use this<br>method of setting up tests.<br>
2. Currently the test setDescription() test only verifies that None is not allowed as input<br>
value.<br>
(a) Improve the test, so that all other builtin types are tested as well.<br>
(b) Also, make sure that any objects inheriting from str or unicode pass as valid values.<br>
<hr>
\ No newline at end of file
<A name=1></a>Chapter&nbsp;44<br>Writing&nbsp;Basic&nbsp;Unit&nbsp;Tests<br>Difficulty<br>Newcomer<br>Skills<br>•&nbsp;All&nbsp;you&nbsp;need&nbsp;to&nbsp;know&nbsp;is&nbsp;some&nbsp;Python.<br>Problem/Task<br>As&nbsp;you&nbsp;know&nbsp;by&nbsp;now,&nbsp;Zope&nbsp;3&nbsp;gains&nbsp;its&nbsp;incredible&nbsp;stability&nbsp;from&nbsp;testing&nbsp;any&nbsp;code&nbsp;in&nbsp;great&nbsp;detail.&nbsp;The<br>currently&nbsp;most&nbsp;common&nbsp;method&nbsp;is&nbsp;to&nbsp;write&nbsp;unit&nbsp;tests.&nbsp;This&nbsp;chapter&nbsp;introduces&nbsp;unit&nbsp;tests&nbsp;–&nbsp;which<br>are&nbsp;Zope&nbsp;3&nbsp;independent&nbsp;–&nbsp;and&nbsp;introduces&nbsp;some&nbsp;of&nbsp;the&nbsp;subtleties.<br>Solution<br>44.1<br>Implementing&nbsp;the&nbsp;Sample&nbsp;Class<br>Before&nbsp;we&nbsp;can&nbsp;write&nbsp;tests,&nbsp;we&nbsp;have&nbsp;to&nbsp;write&nbsp;some&nbsp;code&nbsp;that&nbsp;we&nbsp;can&nbsp;test.&nbsp;Here,&nbsp;we&nbsp;will&nbsp;implement<br>a&nbsp;simple&nbsp;class&nbsp;called&nbsp;Sample&nbsp;with&nbsp;a&nbsp;public&nbsp;attribute&nbsp;title&nbsp;and&nbsp;description&nbsp;that&nbsp;is&nbsp;accessed<br>via&nbsp;getDescription()&nbsp;and&nbsp;mutated&nbsp;using&nbsp;setDescription().&nbsp;Further,&nbsp;the&nbsp;description&nbsp;must&nbsp;be<br>either&nbsp;a&nbsp;regular&nbsp;or&nbsp;unicode&nbsp;string.<br>Since&nbsp;this&nbsp;code&nbsp;will&nbsp;not&nbsp;depend&nbsp;on&nbsp;Zope,&nbsp;open&nbsp;a&nbsp;file&nbsp;named&nbsp;test&nbsp;sample.py&nbsp;anywhere&nbsp;and&nbsp;add<br>the&nbsp;following&nbsp;class:<br>1&nbsp;Sample(object):<br>2<br>&quot;&quot;&quot;A&nbsp;trivial&nbsp;Sample&nbsp;object.&quot;&quot;&quot;<br>3<br>4<br>title&nbsp;=&nbsp;None<br>5<br>6<br>def&nbsp;__init__(self):<br>7<br>&quot;&quot;&quot;Initialize&nbsp;object.&quot;&quot;&quot;<br>8<br>self._description&nbsp;=&nbsp;’’<br>9<br>1<br><hr><A name=2></a>2<br>CHAPTER&nbsp;44.&nbsp;WRITING&nbsp;BASIC&nbsp;UNIT&nbsp;TESTS<br>10<br>def&nbsp;setDescription(self,&nbsp;value):<br>11<br>&quot;&quot;&quot;Change&nbsp;the&nbsp;value&nbsp;of&nbsp;the&nbsp;description.&quot;&quot;&quot;<br>12<br>assert&nbsp;isinstance(value,&nbsp;(str,&nbsp;unicode))<br>13<br>self._description&nbsp;=&nbsp;value<br>14<br>15<br>def&nbsp;getDescription(self):<br>16<br>&quot;&quot;&quot;Change&nbsp;the&nbsp;value&nbsp;of&nbsp;the&nbsp;description.&quot;&quot;&quot;<br>17<br>return&nbsp;self._description<br>Line&nbsp;4:&nbsp;The&nbsp;title&nbsp;is&nbsp;just&nbsp;publicly&nbsp;declared&nbsp;and&nbsp;a&nbsp;value&nbsp;of&nbsp;None&nbsp;is&nbsp;given.&nbsp;Therefore&nbsp;this&nbsp;is&nbsp;just<br>a&nbsp;regular&nbsp;attribute.<br>Line&nbsp;8:&nbsp;The&nbsp;actual&nbsp;description&nbsp;string&nbsp;will&nbsp;be&nbsp;stored&nbsp;in&nbsp;description.<br>Line&nbsp;12:&nbsp;Make&nbsp;sure&nbsp;that&nbsp;the&nbsp;description&nbsp;is&nbsp;only&nbsp;a&nbsp;regular&nbsp;or&nbsp;unicode&nbsp;string,&nbsp;like&nbsp;it&nbsp;was&nbsp;stated&nbsp;in<br>the&nbsp;requirements.<br>If&nbsp;you&nbsp;wish&nbsp;you&nbsp;can&nbsp;now&nbsp;manually&nbsp;test&nbsp;the&nbsp;class&nbsp;with&nbsp;the&nbsp;interactive&nbsp;Python&nbsp;shell.&nbsp;Just&nbsp;start<br>Python&nbsp;by&nbsp;entering&nbsp;python&nbsp;in&nbsp;your&nbsp;shell&nbsp;prompt.&nbsp;Note&nbsp;that&nbsp;you&nbsp;should&nbsp;be&nbsp;in&nbsp;the&nbsp;directory&nbsp;in<br>which&nbsp;test&nbsp;sample.py&nbsp;is&nbsp;located&nbsp;when&nbsp;starting&nbsp;Python&nbsp;(an&nbsp;alternative&nbsp;is&nbsp;of&nbsp;course&nbsp;to&nbsp;specify&nbsp;the<br>directory&nbsp;in&nbsp;your&nbsp;PYTHONPATH.)<br>1&nbsp;&gt;&gt;&gt;&nbsp;from&nbsp;test_sample&nbsp;import&nbsp;Sample<br>2&nbsp;&gt;&gt;&gt;&nbsp;sample&nbsp;=&nbsp;Sample()<br>3&nbsp;&gt;&gt;&gt;&nbsp;print&nbsp;sample.title<br>4&nbsp;None<br>5&nbsp;&gt;&gt;&gt;&nbsp;sample.title&nbsp;=&nbsp;’Title’<br>6&nbsp;&gt;&gt;&gt;&nbsp;print&nbsp;sample.title<br>7&nbsp;Title<br>8&nbsp;&gt;&gt;&gt;&nbsp;print&nbsp;sample.getDescription()<br>9<br>10&nbsp;&gt;&gt;&gt;&nbsp;sample.setDescription(’Hello&nbsp;World’)<br>11&nbsp;&gt;&gt;&gt;&nbsp;print&nbsp;sample.getDescription()<br>12&nbsp;Hello&nbsp;World<br>13&nbsp;&gt;&gt;&gt;&nbsp;sample.setDescription(None)<br>14&nbsp;Traceback&nbsp;(most&nbsp;recent&nbsp;call&nbsp;last):<br>15<br>File&nbsp;&quot;&lt;stdin&gt;&quot;,&nbsp;line&nbsp;1,&nbsp;in&nbsp;?<br>16<br>File&nbsp;&quot;test_sample.py&quot;,&nbsp;line&nbsp;31,&nbsp;in&nbsp;setDescription<br>17<br>assert&nbsp;isinstance(value,&nbsp;(str,&nbsp;unicode))<br>18&nbsp;AssertionError<br>As&nbsp;you&nbsp;can&nbsp;see&nbsp;in&nbsp;the&nbsp;last&nbsp;test,&nbsp;non-string&nbsp;object&nbsp;types&nbsp;are&nbsp;not&nbsp;allowed&nbsp;as&nbsp;descriptions&nbsp;and&nbsp;an<br>AssertionError&nbsp;is&nbsp;raised.<br>44.2<br>Writing&nbsp;the&nbsp;Unit&nbsp;Tests<br>The&nbsp;goal&nbsp;of&nbsp;writing&nbsp;the&nbsp;unit&nbsp;tests&nbsp;is&nbsp;to&nbsp;convert&nbsp;this&nbsp;informal,&nbsp;manual,&nbsp;and&nbsp;interactive&nbsp;testing&nbsp;session<br>into&nbsp;a&nbsp;formal&nbsp;test&nbsp;class.&nbsp;Python&nbsp;provides&nbsp;already&nbsp;a&nbsp;module&nbsp;called&nbsp;unittest&nbsp;for&nbsp;this&nbsp;purpose,&nbsp;which<br>is&nbsp;a&nbsp;port&nbsp;of&nbsp;the&nbsp;Java-based&nbsp;unit&nbsp;testing&nbsp;product,&nbsp;JUnit,&nbsp;by&nbsp;Kent&nbsp;Beck&nbsp;and&nbsp;Erich&nbsp;Gamma.&nbsp;There&nbsp;are<br>three&nbsp;levels&nbsp;to&nbsp;the&nbsp;testing&nbsp;framework&nbsp;(this&nbsp;list&nbsp;deviates&nbsp;a&nbsp;bit&nbsp;from&nbsp;the&nbsp;original&nbsp;definitions&nbsp;as&nbsp;found<br>in&nbsp;the&nbsp;Python&nbsp;library&nbsp;documentation.&nbsp;1).<br>1&nbsp;http://www.python.org/doc/current/lib/module-unittest.html<br><hr><A name=3></a>44.2.&nbsp;WRITING&nbsp;THE&nbsp;UNIT&nbsp;TESTS<br>3<br>The&nbsp;smallest&nbsp;unit&nbsp;is&nbsp;obviously&nbsp;the&nbsp;“test”,&nbsp;which&nbsp;is&nbsp;a&nbsp;single&nbsp;method&nbsp;in&nbsp;a&nbsp;TestCase&nbsp;class&nbsp;that<br>tests&nbsp;the&nbsp;behavior&nbsp;of&nbsp;a&nbsp;small&nbsp;piece&nbsp;of&nbsp;code&nbsp;or&nbsp;a&nbsp;particular&nbsp;aspect&nbsp;of&nbsp;an&nbsp;implementation.&nbsp;The&nbsp;“test<br>case”&nbsp;is&nbsp;then&nbsp;a&nbsp;collection&nbsp;tests&nbsp;that&nbsp;share&nbsp;the&nbsp;same&nbsp;setup/inputs.&nbsp;On&nbsp;top&nbsp;of&nbsp;all&nbsp;of&nbsp;this&nbsp;sits&nbsp;the&nbsp;“test<br>suite”&nbsp;which&nbsp;is&nbsp;a&nbsp;collection&nbsp;of&nbsp;test&nbsp;cases&nbsp;and/or&nbsp;other&nbsp;test&nbsp;suites.&nbsp;Test&nbsp;suites&nbsp;combine&nbsp;tests&nbsp;that<br>should&nbsp;be&nbsp;executed&nbsp;together.&nbsp;With&nbsp;the&nbsp;correct&nbsp;setup&nbsp;(as&nbsp;shown&nbsp;in&nbsp;the&nbsp;example&nbsp;below),&nbsp;you&nbsp;can<br>then&nbsp;execute&nbsp;test&nbsp;suites.&nbsp;For&nbsp;large&nbsp;projects&nbsp;like&nbsp;Zope&nbsp;3,&nbsp;it&nbsp;is&nbsp;useful&nbsp;to&nbsp;know&nbsp;that&nbsp;there&nbsp;is&nbsp;also&nbsp;the<br>concept&nbsp;of&nbsp;a&nbsp;test&nbsp;runner,&nbsp;which&nbsp;manages&nbsp;the&nbsp;test&nbsp;run&nbsp;of&nbsp;all&nbsp;or&nbsp;a&nbsp;set&nbsp;of&nbsp;tests.&nbsp;The&nbsp;runner&nbsp;provides<br>useful&nbsp;feedback&nbsp;to&nbsp;the&nbsp;application,&nbsp;so&nbsp;that&nbsp;various&nbsp;user&nbsp;interaces&nbsp;can&nbsp;be&nbsp;developed&nbsp;on&nbsp;top&nbsp;of&nbsp;it.<br>But&nbsp;enough&nbsp;about&nbsp;the&nbsp;theory.&nbsp;In&nbsp;the&nbsp;following&nbsp;example,&nbsp;which&nbsp;you&nbsp;can&nbsp;simply&nbsp;put&nbsp;into&nbsp;the&nbsp;same<br>file&nbsp;as&nbsp;your&nbsp;code&nbsp;above,&nbsp;you&nbsp;will&nbsp;see&nbsp;a&nbsp;test&nbsp;in&nbsp;common&nbsp;Zope&nbsp;3&nbsp;style.<br>1&nbsp;import&nbsp;unittest<br>2<br>3&nbsp;class&nbsp;SampleTest(unittest.TestCase):<br>4<br>&quot;&quot;&quot;Test&nbsp;the&nbsp;Sample&nbsp;class&quot;&quot;&quot;<br>5<br>6<br>def&nbsp;test_title(self):<br>7<br>sample&nbsp;=&nbsp;Sample()<br>8<br>self.assertEqual(sample.title,&nbsp;None)<br>9<br>sample.title&nbsp;=&nbsp;’Sample&nbsp;Title’<br>10<br>self.assertEqual(sample.title,&nbsp;’Sample&nbsp;Title’)<br>11<br>12<br>def&nbsp;test_getDescription(self):<br>13<br>sample&nbsp;=&nbsp;Sample()<br>14<br>self.assertEqual(sample.getDescription(),&nbsp;’’)<br>15<br>sample._description&nbsp;=&nbsp;&quot;Description&quot;<br>16<br>self.assertEqual(sample.getDescription(),&nbsp;’Description’)<br>17<br>18<br>def&nbsp;test_setDescription(self):<br>19<br>sample&nbsp;=&nbsp;Sample()<br>20<br>self.assertEqual(sample._description,&nbsp;’’)<br>21<br>sample.setDescription(’Description’)<br>22<br>self.assertEqual(sample._description,&nbsp;’Description’)<br>23<br>sample.setDescription(u’Description2’)<br>24<br>self.assertEqual(sample._description,&nbsp;u’Description2’)<br>25<br>self.assertRaises(AssertionError,&nbsp;sample.setDescription,&nbsp;None)<br>26<br>27<br>28&nbsp;def&nbsp;test_suite():<br>29<br>return&nbsp;unittest.TestSuite((<br>30<br>unittest.makeSuite(SampleTest),<br>31<br>))<br>32<br>33&nbsp;if&nbsp;__name__&nbsp;==&nbsp;’__main__’:<br>34<br>unittest.main(defaultTest=’test_suite’)<br>Line&nbsp;3–4:&nbsp;We&nbsp;usually&nbsp;develop&nbsp;test&nbsp;classes&nbsp;which&nbsp;must&nbsp;inherit&nbsp;from&nbsp;TestCase.&nbsp;While&nbsp;often&nbsp;not<br>done,&nbsp;it&nbsp;is&nbsp;a&nbsp;good&nbsp;idea&nbsp;to&nbsp;give&nbsp;the&nbsp;class&nbsp;a&nbsp;meaningful&nbsp;docstring&nbsp;that&nbsp;describes&nbsp;the&nbsp;purpose&nbsp;of&nbsp;the<br>tests&nbsp;it&nbsp;includes.<br>Line&nbsp;6,&nbsp;12&nbsp;&amp;&nbsp;18:&nbsp;When&nbsp;a&nbsp;test&nbsp;case&nbsp;is&nbsp;run,&nbsp;a&nbsp;method&nbsp;called&nbsp;runTests()&nbsp;is&nbsp;executed.&nbsp;While&nbsp;it<br>is&nbsp;possible&nbsp;to&nbsp;overrride&nbsp;this&nbsp;method&nbsp;to&nbsp;run&nbsp;tests&nbsp;differently,&nbsp;the&nbsp;default&nbsp;option&nbsp;will&nbsp;look&nbsp;for&nbsp;any<br>method&nbsp;whose&nbsp;name&nbsp;starts&nbsp;with&nbsp;test&nbsp;and&nbsp;execute&nbsp;it&nbsp;as&nbsp;a&nbsp;single&nbsp;test.&nbsp;This&nbsp;way&nbsp;we&nbsp;can&nbsp;create<br>a&nbsp;“test&nbsp;method”&nbsp;for&nbsp;each&nbsp;aspect,&nbsp;method,&nbsp;function&nbsp;or&nbsp;property&nbsp;of&nbsp;the&nbsp;code&nbsp;to&nbsp;be&nbsp;tested.&nbsp;This<br>default&nbsp;is&nbsp;very&nbsp;sensible&nbsp;and&nbsp;is&nbsp;used&nbsp;everywhere&nbsp;in&nbsp;Zope&nbsp;3.<br><hr><A name=4></a>4<br>CHAPTER&nbsp;44.&nbsp;WRITING&nbsp;BASIC&nbsp;UNIT&nbsp;TESTS<br>Note&nbsp;that&nbsp;there&nbsp;is&nbsp;no&nbsp;docstring&nbsp;for&nbsp;test&nbsp;methods.&nbsp;This&nbsp;is&nbsp;intentional.&nbsp;If&nbsp;a&nbsp;docstring&nbsp;is&nbsp;specified,<br>it&nbsp;is&nbsp;used&nbsp;instead&nbsp;of&nbsp;the&nbsp;method&nbsp;name&nbsp;to&nbsp;identify&nbsp;the&nbsp;test.&nbsp;When&nbsp;specifying&nbsp;a&nbsp;docstring,&nbsp;we&nbsp;have<br>noticed&nbsp;that&nbsp;it&nbsp;is&nbsp;very&nbsp;difficult&nbsp;to&nbsp;identify&nbsp;the&nbsp;test&nbsp;later;&nbsp;therefore&nbsp;the&nbsp;method&nbsp;name&nbsp;is&nbsp;a&nbsp;much<br>better&nbsp;choice.<br>Line&nbsp;8,&nbsp;10,&nbsp;14,&nbsp;.&nbsp;.&nbsp;.&nbsp;:&nbsp;The&nbsp;TestCase&nbsp;class&nbsp;implements&nbsp;a&nbsp;handful&nbsp;of&nbsp;methods&nbsp;that&nbsp;aid&nbsp;you&nbsp;with&nbsp;the<br>testing.&nbsp;Here&nbsp;are&nbsp;some&nbsp;of&nbsp;the&nbsp;most&nbsp;frequently&nbsp;used&nbsp;ones.&nbsp;For&nbsp;a&nbsp;complete&nbsp;list&nbsp;see&nbsp;the&nbsp;standard<br>Python&nbsp;documentation&nbsp;referenced&nbsp;above.<br>•&nbsp;assertEqual(first,second[,msg])<br>Checks&nbsp;whether&nbsp;the&nbsp;first&nbsp;and&nbsp;second&nbsp;value&nbsp;are&nbsp;equal.&nbsp;If&nbsp;the&nbsp;test&nbsp;fails,&nbsp;the&nbsp;msg&nbsp;or&nbsp;None<br>is&nbsp;returned.<br>•&nbsp;assertNotEqual(first,second[,msg])<br>This&nbsp;is&nbsp;simply&nbsp;the&nbsp;opposite&nbsp;to&nbsp;assertEqual()&nbsp;by&nbsp;checking&nbsp;for&nbsp;non-equality.<br>•&nbsp;assertRaises(exception,callable,...)<br>You&nbsp;expect&nbsp;the&nbsp;callable&nbsp;to&nbsp;raise&nbsp;exception&nbsp;when&nbsp;executed.&nbsp;After&nbsp;the&nbsp;callable&nbsp;you&nbsp;can<br>specify&nbsp;any&nbsp;amount&nbsp;of&nbsp;positional&nbsp;and&nbsp;keyword&nbsp;arguments&nbsp;for&nbsp;the&nbsp;callable.&nbsp;If&nbsp;you&nbsp;expect<br>a&nbsp;group&nbsp;of&nbsp;exceptions&nbsp;from&nbsp;the&nbsp;execution,&nbsp;you&nbsp;can&nbsp;make&nbsp;exception&nbsp;a&nbsp;tuple&nbsp;of&nbsp;possible<br>exceptions.<br>•&nbsp;assert&nbsp;(expr[,msg])<br>Assert&nbsp;checks&nbsp;whether&nbsp;the&nbsp;specified&nbsp;expression&nbsp;executes&nbsp;correctly.&nbsp;If&nbsp;not,&nbsp;the&nbsp;test&nbsp;fails&nbsp;and<br>msg&nbsp;or&nbsp;None&nbsp;is&nbsp;returned.<br>•&nbsp;failUnlessEqual()<br>This&nbsp;testing&nbsp;method&nbsp;is&nbsp;equivalent&nbsp;to&nbsp;assertEqual().<br>•&nbsp;failUnless(expr[,msg])<br>This&nbsp;method&nbsp;is&nbsp;equivalent&nbsp;to&nbsp;assert&nbsp;(expr[,msg]).<br>•&nbsp;failif()<br>This&nbsp;is&nbsp;the&nbsp;opposite&nbsp;to&nbsp;failUnless().<br>•&nbsp;fail([msg])<br>Fails&nbsp;the&nbsp;running&nbsp;test&nbsp;without&nbsp;any&nbsp;evaluation.&nbsp;This&nbsp;is&nbsp;commonly&nbsp;used&nbsp;when&nbsp;testing&nbsp;various<br>possible&nbsp;execution&nbsp;paths&nbsp;at&nbsp;once&nbsp;and&nbsp;you&nbsp;would&nbsp;like&nbsp;to&nbsp;signify&nbsp;a&nbsp;failure&nbsp;if&nbsp;an&nbsp;improper&nbsp;path<br>was&nbsp;taken.<br>Line&nbsp;6–10:&nbsp;This&nbsp;method&nbsp;tests&nbsp;the&nbsp;title&nbsp;attribute&nbsp;of&nbsp;the&nbsp;Sample&nbsp;class.&nbsp;The&nbsp;first&nbsp;test&nbsp;should<br>be&nbsp;of&nbsp;course&nbsp;that&nbsp;the&nbsp;attribute&nbsp;exists&nbsp;and&nbsp;has&nbsp;the&nbsp;expected&nbsp;initial&nbsp;value&nbsp;(line&nbsp;8).&nbsp;Then&nbsp;the&nbsp;title<br>attribute&nbsp;is&nbsp;changed&nbsp;and&nbsp;we&nbsp;check&nbsp;whether&nbsp;the&nbsp;value&nbsp;was&nbsp;really&nbsp;stored.&nbsp;This&nbsp;might&nbsp;seem&nbsp;like<br>overkill,&nbsp;but&nbsp;later&nbsp;you&nbsp;might&nbsp;change&nbsp;the&nbsp;title&nbsp;in&nbsp;a&nbsp;way&nbsp;that&nbsp;it&nbsp;uses&nbsp;properties&nbsp;instead.&nbsp;Then&nbsp;it<br>becomes&nbsp;very&nbsp;important&nbsp;to&nbsp;check&nbsp;whether&nbsp;this&nbsp;test&nbsp;still&nbsp;passes.<br>Line&nbsp;12–16:&nbsp;First&nbsp;we&nbsp;simply&nbsp;check&nbsp;that&nbsp;getDescription()&nbsp;returns&nbsp;the&nbsp;correct&nbsp;default&nbsp;value.<br>Since&nbsp;we&nbsp;do&nbsp;not&nbsp;want&nbsp;to&nbsp;use&nbsp;other&nbsp;API&nbsp;calls&nbsp;like&nbsp;setDescription()&nbsp;we&nbsp;set&nbsp;a&nbsp;new&nbsp;value&nbsp;of&nbsp;the<br>description&nbsp;via&nbsp;the&nbsp;implementation-internal&nbsp;description&nbsp;attribute&nbsp;(line&nbsp;15).&nbsp;This&nbsp;is&nbsp;okay!&nbsp;Unit<br>tests&nbsp;can&nbsp;make&nbsp;use&nbsp;of&nbsp;implementation-specific&nbsp;attributes&nbsp;and&nbsp;methods.&nbsp;Finally&nbsp;we&nbsp;just&nbsp;check&nbsp;that<br>the&nbsp;correct&nbsp;value&nbsp;is&nbsp;returned.<br><hr><A name=5></a>44.3.&nbsp;RUNNING&nbsp;THE&nbsp;TESTS<br>5<br>Line&nbsp;18–25:&nbsp;On&nbsp;line&nbsp;21–24&nbsp;it&nbsp;is&nbsp;checked&nbsp;that&nbsp;both&nbsp;regular&nbsp;and&nbsp;unicode&nbsp;strings&nbsp;are&nbsp;set&nbsp;correctly.<br>In&nbsp;the&nbsp;last&nbsp;line&nbsp;of&nbsp;the&nbsp;test&nbsp;we&nbsp;make&nbsp;sure&nbsp;that&nbsp;no&nbsp;other&nbsp;type&nbsp;of&nbsp;objects&nbsp;can&nbsp;be&nbsp;set&nbsp;as&nbsp;a&nbsp;description<br>and&nbsp;that&nbsp;an&nbsp;error&nbsp;is&nbsp;raised.<br>28–31:&nbsp;This&nbsp;method&nbsp;returns&nbsp;a&nbsp;test&nbsp;suite&nbsp;that&nbsp;includes&nbsp;all&nbsp;test&nbsp;cases&nbsp;created&nbsp;in&nbsp;this&nbsp;module.&nbsp;It&nbsp;is<br>used&nbsp;by&nbsp;the&nbsp;Zope&nbsp;3&nbsp;test&nbsp;runner&nbsp;when&nbsp;it&nbsp;picks&nbsp;up&nbsp;all&nbsp;available&nbsp;tests.&nbsp;You&nbsp;would&nbsp;basically&nbsp;add&nbsp;the<br>line&nbsp;unittest.makeSuite(TestCaseClass)&nbsp;for&nbsp;each&nbsp;additional&nbsp;test&nbsp;case.<br>33–34:&nbsp;In&nbsp;order&nbsp;to&nbsp;make&nbsp;the&nbsp;test&nbsp;module&nbsp;runnable&nbsp;by&nbsp;itself,&nbsp;you&nbsp;can&nbsp;execute&nbsp;unittest.main()<br>when&nbsp;the&nbsp;module&nbsp;is&nbsp;run.<br>44.3<br>Running&nbsp;the&nbsp;Tests<br>You&nbsp;can&nbsp;run&nbsp;the&nbsp;test&nbsp;by&nbsp;simply&nbsp;calling&nbsp;pythontest&nbsp;sample.py&nbsp;from&nbsp;the&nbsp;directory&nbsp;you&nbsp;saved&nbsp;the<br>file&nbsp;in.&nbsp;Here&nbsp;is&nbsp;the&nbsp;result&nbsp;you&nbsp;should&nbsp;see:<br>.<br>--------------------------------------------------------------------<br>n&nbsp;3&nbsp;tests&nbsp;in&nbsp;0.001s<br>The&nbsp;three&nbsp;dots&nbsp;represent&nbsp;the&nbsp;three&nbsp;tests&nbsp;that&nbsp;were&nbsp;run.&nbsp;If&nbsp;a&nbsp;test&nbsp;had&nbsp;failed,&nbsp;it&nbsp;would&nbsp;have&nbsp;been<br>reported&nbsp;pointing&nbsp;out&nbsp;the&nbsp;failing&nbsp;test&nbsp;and&nbsp;providing&nbsp;a&nbsp;small&nbsp;traceback.<br>When&nbsp;using&nbsp;the&nbsp;default&nbsp;Zope&nbsp;3&nbsp;test&nbsp;runner,&nbsp;tests&nbsp;will&nbsp;be&nbsp;picked&nbsp;up&nbsp;as&nbsp;long&nbsp;as&nbsp;they&nbsp;follow&nbsp;some<br>conventions.<br>•&nbsp;The&nbsp;tests&nbsp;must&nbsp;either&nbsp;be&nbsp;in&nbsp;a&nbsp;package&nbsp;or&nbsp;be&nbsp;a&nbsp;module&nbsp;called&nbsp;tests.<br>•&nbsp;If&nbsp;tests&nbsp;is&nbsp;a&nbsp;package,&nbsp;then&nbsp;all&nbsp;test&nbsp;modules&nbsp;inside&nbsp;must&nbsp;also&nbsp;have&nbsp;a&nbsp;name&nbsp;starting&nbsp;with&nbsp;test,<br>as&nbsp;it&nbsp;is&nbsp;the&nbsp;case&nbsp;with&nbsp;our&nbsp;name&nbsp;test&nbsp;sample.py.<br>•&nbsp;The&nbsp;test&nbsp;module&nbsp;must&nbsp;be&nbsp;somewhere&nbsp;in&nbsp;the&nbsp;Zope&nbsp;3&nbsp;source&nbsp;tree,&nbsp;since&nbsp;the&nbsp;test&nbsp;runner&nbsp;looks<br>only&nbsp;for&nbsp;files&nbsp;there.<br>In&nbsp;our&nbsp;case,&nbsp;you&nbsp;could&nbsp;simply&nbsp;create&nbsp;a&nbsp;tests&nbsp;package&nbsp;in&nbsp;ZOPE3/src&nbsp;(do&nbsp;not&nbsp;forget&nbsp;the<br>init&nbsp;.<br>py&nbsp;file).&nbsp;Then&nbsp;place&nbsp;the&nbsp;test&nbsp;sample.py&nbsp;file&nbsp;into&nbsp;this&nbsp;directory.<br>You&nbsp;you&nbsp;can&nbsp;use&nbsp;the&nbsp;test&nbsp;runner&nbsp;to&nbsp;run&nbsp;only&nbsp;the&nbsp;sample&nbsp;tests&nbsp;as&nbsp;follows&nbsp;from&nbsp;the&nbsp;Zope&nbsp;3&nbsp;root<br>directory:<br>python&nbsp;test.py&nbsp;-vp&nbsp;tests.test_sample<br>The&nbsp;-v&nbsp;option&nbsp;stands&nbsp;for&nbsp;verbose&nbsp;mode,&nbsp;so&nbsp;that&nbsp;detailed&nbsp;information&nbsp;about&nbsp;a&nbsp;test&nbsp;failure&nbsp;is<br>provided.&nbsp;The&nbsp;-p&nbsp;option&nbsp;enables&nbsp;a&nbsp;progress&nbsp;bar&nbsp;that&nbsp;tells&nbsp;you&nbsp;how&nbsp;many&nbsp;tests&nbsp;out&nbsp;of&nbsp;all&nbsp;have&nbsp;been<br>completed.&nbsp;There&nbsp;are&nbsp;many&nbsp;more&nbsp;options&nbsp;that&nbsp;can&nbsp;be&nbsp;specified.&nbsp;You&nbsp;can&nbsp;get&nbsp;a&nbsp;full&nbsp;list&nbsp;of&nbsp;them&nbsp;with<br>the&nbsp;option&nbsp;-h:&nbsp;pythontest.py-h.<br>The&nbsp;output&nbsp;of&nbsp;the&nbsp;call&nbsp;above&nbsp;is&nbsp;as&nbsp;follows:<br>nfiguration&nbsp;file&nbsp;found.<br>nning&nbsp;UNIT&nbsp;tests&nbsp;at&nbsp;level&nbsp;1<br>nning&nbsp;UNIT&nbsp;tests&nbsp;from&nbsp;/opt/zope/Zope3<br>3/3&nbsp;(100.0%):&nbsp;test_title&nbsp;(tests.test_sample.SampleTest)<br>--------------------------------------------------------------------<br>n&nbsp;3&nbsp;tests&nbsp;in&nbsp;0.002s<br><hr><A name=6></a>6<br>CHAPTER&nbsp;44.&nbsp;WRITING&nbsp;BASIC&nbsp;UNIT&nbsp;TESTS<br>nning&nbsp;FUNCTIONAL&nbsp;tests&nbsp;at&nbsp;level&nbsp;1<br>nning&nbsp;FUNCTIONAL&nbsp;tests&nbsp;from&nbsp;/opt/zope/Zope3<br>--------------------------------------------------------------------<br>n&nbsp;0&nbsp;tests&nbsp;in&nbsp;0.000s<br>Line&nbsp;1:&nbsp;The&nbsp;test&nbsp;runner&nbsp;uses&nbsp;a&nbsp;configuration&nbsp;file&nbsp;for&nbsp;some&nbsp;setup.&nbsp;This&nbsp;allows&nbsp;developers&nbsp;to&nbsp;use<br>the&nbsp;test&nbsp;runner&nbsp;for&nbsp;other&nbsp;projects&nbsp;as&nbsp;well.&nbsp;This&nbsp;message&nbsp;simply&nbsp;tells&nbsp;us&nbsp;that&nbsp;the&nbsp;configuration&nbsp;file<br>was&nbsp;found.<br>Line&nbsp;2–8:&nbsp;The&nbsp;unit&nbsp;tests&nbsp;are&nbsp;run.&nbsp;On&nbsp;line&nbsp;4&nbsp;you&nbsp;can&nbsp;see&nbsp;the&nbsp;progress&nbsp;bar.<br>Line&nbsp;9–15:&nbsp;The&nbsp;functional&nbsp;tests&nbsp;are&nbsp;run,&nbsp;since&nbsp;the&nbsp;default&nbsp;test&nbsp;runner&nbsp;runs&nbsp;both&nbsp;types&nbsp;of&nbsp;tests.<br>Since&nbsp;we&nbsp;do&nbsp;not&nbsp;have&nbsp;any&nbsp;functional&nbsp;tests&nbsp;in&nbsp;the&nbsp;specified&nbsp;module,&nbsp;there&nbsp;are&nbsp;no&nbsp;tests&nbsp;to&nbsp;run.&nbsp;To<br>just&nbsp;run&nbsp;the&nbsp;unit&nbsp;tests,&nbsp;use&nbsp;option&nbsp;-u&nbsp;and&nbsp;-f&nbsp;for&nbsp;just&nbsp;running&nbsp;the&nbsp;functional&nbsp;tests.&nbsp;See&nbsp;“Writing<br>Functional&nbsp;Tests”&nbsp;for&nbsp;more&nbsp;detials&nbsp;on&nbsp;functional&nbsp;tests.<br><hr><A name=7></a>44.3.&nbsp;RUNNING&nbsp;THE&nbsp;TESTS<br>7<br>Exercises<br>1.&nbsp;It&nbsp;is&nbsp;not&nbsp;very&nbsp;common&nbsp;to&nbsp;do&nbsp;the&nbsp;setup&nbsp;–&nbsp;in&nbsp;our&nbsp;case&nbsp;sample=Sample()&nbsp;–&nbsp;in&nbsp;every&nbsp;test<br>method.&nbsp;Instead&nbsp;there&nbsp;exists&nbsp;a&nbsp;method&nbsp;called&nbsp;setUp()&nbsp;and&nbsp;its&nbsp;counterpart&nbsp;tearDown&nbsp;that<br>are&nbsp;run&nbsp;before&nbsp;and&nbsp;after&nbsp;each&nbsp;test,&nbsp;respectively.&nbsp;Change&nbsp;the&nbsp;test&nbsp;code&nbsp;above,&nbsp;so&nbsp;that&nbsp;it&nbsp;uses<br>the&nbsp;setUp()&nbsp;method.&nbsp;In&nbsp;later&nbsp;chapters&nbsp;and&nbsp;the&nbsp;rest&nbsp;of&nbsp;the&nbsp;book&nbsp;we&nbsp;will&nbsp;frequently&nbsp;use&nbsp;this<br>method&nbsp;of&nbsp;setting&nbsp;up&nbsp;tests.<br>2.&nbsp;Currently&nbsp;the&nbsp;test&nbsp;setDescription()&nbsp;test&nbsp;only&nbsp;verifies&nbsp;that&nbsp;None&nbsp;is&nbsp;not&nbsp;allowed&nbsp;as&nbsp;input<br>value.<br>(a)&nbsp;Improve&nbsp;the&nbsp;test,&nbsp;so&nbsp;that&nbsp;all&nbsp;other&nbsp;builtin&nbsp;types&nbsp;are&nbsp;tested&nbsp;as&nbsp;well.<br>(b)&nbsp;Also,&nbsp;make&nbsp;sure&nbsp;that&nbsp;any&nbsp;objects&nbsp;inheriting&nbsp;from&nbsp;str&nbsp;or&nbsp;unicode&nbsp;pass&nbsp;as&nbsp;valid&nbsp;values.<br><hr>
\ No newline at end of file
......@@ -3,4 +3,5 @@
<h2> Testing Markdown </h2>
<p> <code>code</code> and <em>italic</em> and <em>bold</em> and even a <a href="http://plone.org">link</a>.
</p>
<p>Fööbär</p>
<h2 class="title">Heading 1</h2>
<p>Some text.</p>
<div class="section">
<h3><a id="heading-2" name="heading-2">Heading 2</a></h3>
<p>Some text, bla ble bli blo blu. Yes, i know this is <a class="reference" href="http://www.example.com">Stupid</a>.</p>
<div class="section" id="heading-2">
<h3>Heading 2</h3>
<p>Some text, bla ble bli blo blu. Yes, i know this is<a class="reference external" href="http://www.example.com">Stupid</a>.</p>
</div>
<h2 class="title">Title</h2>
<h3 class="subtitle">Subtitle</h3>
<p>This is a test document to make sure subtitle gets the right heading.</p>
<div class="section">
<h3><a id="now-the-real-heading" name="now-the-real-heading">Now the real heading</a></h3>
<div class="section" id="now-the-real-heading">
<h3>Now the real heading</h3>
<p>The brown fox jumped over the lazy dog.</p>
<div class="section">
<h4><a id="with-a-subheading" name="with-a-subheading">With a subheading</a></h4>
<p>Some text, bla ble bli blo blu. Yes, i know this is <a class="reference" href="http://www.example.com">Stupid</a>.</p>
<div class="section" id="with-a-subheading">
<h4>With a subheading</h4>
<p>Some text, bla ble bli blo blu. Yes, i know this is<a class="reference external" href="http://www.example.com">Stupid</a>.</p>
</div>
</div>
......@@ -6,6 +6,10 @@
</tr>
</table>
<p>This is a text used as a blind text.</p>
<div><![CDATA[
Some CDATA text.
]]>
</div>
<ul>
<li>A sample list item1</li>
<li>A sample list item2</li>
......
......@@ -67,6 +67,15 @@ class DummyHtmlFilter2(BaseTransform):
data.setData("<div class='dummy'>%s</div>" % orig)
return data
class QuxToVHost(DummyHtmlFilter1):
__name__ = 'qux_to_vhost'
def convert(self, orig, data, context, **kwargs):
data.setData(re.sub('qux', context.REQUEST['SERVER_URL'], orig))
return data
class TransformNoIO(BaseTransform):
implements(ITransform)
......@@ -223,6 +232,52 @@ class TestEngine(ATSiteTestCase):
out = self.engine.convertTo(mt, other_data, mimetype=mt, object=self)
self.failUnlessEqual(out.getData(), other_data, out.getData())
def testCacheWithVHost(self):
"""Ensure that the transform cache key includes virtual
hosting so that transforms which are dependent on the virtual
hosting don't get invalid data from the cache. This happens,
for example, in the resolve UID functionality used by visual
editors."""
mt = 'text/x-html-safe'
self.engine.registerTransform(QuxToVHost())
required = ['qux_to_vhost']
self.engine.manage_addPolicy(mt, required)
data = '<a href="qux">vhost link</a>'
out = self.engine.convertTo(
mt, data, mimetype='text/html', object=self.folder,
context=self.folder)
self.failUnlessEqual(
out.getData(), '<a href="http://nohost">vhost link</a>',
out.getData())
# Test when object is not a context
out = self.engine.convertTo(
mt, data, mimetype='text/html', object=self,
context=self.folder)
self.failUnlessEqual(
out.getData(), '<a href="http://nohost">vhost link</a>',
out.getData())
# Change the virtual hosting
self.folder.REQUEST['SERVER_URL'] = 'http://otherhost'
out = self.engine.convertTo(
mt, data, mimetype='text/html', object=self.folder,
context=self.folder)
self.failUnlessEqual(
out.getData(), '<a href="http://otherhost">vhost link</a>',
out.getData())
# Test when object is not a context
out = self.engine.convertTo(
mt, data, mimetype='text/html', object=self,
context=self.folder)
self.failUnlessEqual(
out.getData(), '<a href="http://otherhost">vhost link</a>',
out.getData())
def test_suite():
from unittest import TestSuite, makeSuite
......
......@@ -16,6 +16,87 @@ class TestGraph(ATSiteTestCase):
out = self.engine.convertTo('text/plain', data, filename=FILE_PATH)
self.failUnless(out.getData())
def testFindPath(self):
originalMap = self.engine._mtmap
"""
The dummy map used for this test corresponds to a graph
depicted in ASCII art below :
+---+
| |
| v
+-->1<-->2-->4-->6<--7
^ ^ |
| | |
v | |
3<---+ |
^ |
| |
v |
5<-------+
"""
# we need a DummyTransform class
class DT:
def __init__(self, name):
self._name = name
def name(self):
return self._name
dummyMap1 = {
'1': { '1': [DT('transform1-1')],
'2': [DT('transform1-2')],
'3': [DT('transform1-3')]},
'2': { '1': [DT('transform2-1')],
'3': [DT('transform2-3')],
'4': [DT('transform2-4')]},
'3': { '1': [DT('transform3-1')],
'2': [DT('transform3-2')],
'5': [DT('transform3-5')]},
'4': { '5': [DT('transform4-5')],
'6': [DT('transform4-6')]},
'5': { '3': [DT('transform5-3')]},
'7': { '6': [DT('transform7-6')]}
}
expectedPathes = {
'1-1': [],
'1-2': ['transform1-2'],
'1-3': ['transform1-3'],
'1-4': ['transform1-2', 'transform2-4'],
'1-5': ['transform1-3', 'transform3-5'],
'1-6': ['transform1-2', 'transform2-4', 'transform4-6'],
'1-7': None,
'2-1': ['transform2-1'],
'2-2': [],
'2-4': ['transform2-4'],
'4-2': ['transform4-5', 'transform5-3', 'transform3-2'],
'5-3': ['transform5-3']
}
self.engine._mtmap = dummyMap1
for orig in ['1','2','3','4','5','6','7']:
for target in ['1','2','3','4','5','6','7']:
# build the name of the path
pathName = orig + '-' + target
# do we have any expectation for this path ?
if pathName in expectedPathes.keys():
# we do. Here is the expected shortest path
expectedPath = expectedPathes[pathName]
# what's the shortest path according to the engine ?
gotPath = self.engine._findPath(orig,target)
# just keep the name of the transforms, please
if gotPath is not None:
gotPath = [transform.name() for transform in gotPath]
# this must be the same as in our expectation
self.assertEquals(expectedPath, gotPath)
self.engine._mtmap = originalMap
def testFindPathWithEmptyTransform(self):
""" _findPath should not throw "index out of range" when dealing with
empty transforms list
"""
dummyMap = {'1': {'2': []}}
self.engine._mtmap = dummyMap
self.engine._findPath('1','2')
def testIdentity(self):
orig = 'Some text'
converted = self.engine.convertTo(
......
import os
import logging
from Testing import ZopeTestCase
from Products.Archetypes.tests.atsitetestcase import ATSiteTestCase
from Products.CMFCore.utils import getToolByName
from utils import input_file_path, output_file_path, normalize_html,\
load, matching_inputs
from Products.PortalTransforms.data import datastream
from Products.PortalTransforms.interfaces import IDataStream
from Products.PortalTransforms.interfaces import idatastream
from Products.MimetypesRegistry.MimeTypesTool import MimeTypesTool
from Products.PortalTransforms.TransformEngine import TransformTool
from Products.PortalTransforms.libtransforms.utils import MissingBinary
from Products.PortalTransforms.transforms.image_to_gif import image_to_gif
......@@ -24,7 +21,6 @@ from Products.PortalTransforms.transforms.textile_to_html import HAS_TEXTILE
from Products.PortalTransforms.transforms.markdown_to_html import HAS_MARKDOWN
from os.path import exists
import sys
# we have to set locale because lynx output is locale sensitive !
os.environ['LC_ALL'] = 'C'
logger = logging.getLogger('PortalTransforms')
......@@ -59,9 +55,11 @@ class TransformTest(ATSiteTestCase):
got = self.normalize(got)
output.close()
self.assertEquals(got, expected,
got_start = got.strip()[:30]
expected_start = expected.strip()[:30]
self.assertEquals(got_start, expected_start,
'[%s]\n\n!=\n\n[%s]\n\nIN %s(%s)' % (
got, expected, self.transform.name(), self.input))
got_start, expected_start, self.transform.name(), self.input))
self.assertEquals(self.subobjects, len(res_data.getSubObjects()),
'%s\n\n!=\n\n%s\n\nIN %s(%s)' % (
self.subobjects, len(res_data.getSubObjects()),
......@@ -70,13 +68,13 @@ class TransformTest(ATSiteTestCase):
def testSame(self):
try:
self.do_convert(filename=self.input)
except MissingBinary, e:
except MissingBinary:
pass
def testSameNoFilename(self):
try:
self.do_convert()
except MissingBinary, e:
except MissingBinary:
pass
def __repr__(self):
......@@ -86,12 +84,13 @@ class PILTransformsTest(ATSiteTestCase):
def afterSetUp(self):
ATSiteTestCase.afterSetUp(self)
self.pt = self.portal.portal_transforms
self.mimetypes_registry = getToolByName(self.portal, 'mimetypes_registry')
def test_image_to_bmp(self):
self.pt.registerTransform(image_to_bmp())
imgFile = open(input_file_path('logo.jpg'), 'rb')
data = imgFile.read()
self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/jpeg')
self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/jpeg')
data = self.pt.convertTo(target_mimetype='image/x-ms-bmp',orig=data)
self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/x-ms-bmp')
......@@ -99,7 +98,7 @@ class PILTransformsTest(ATSiteTestCase):
self.pt.registerTransform(image_to_gif())
imgFile = open(input_file_path('logo.png'), 'rb')
data = imgFile.read()
self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/png')
self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/png')
data = self.pt.convertTo(target_mimetype='image/gif',orig=data)
self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/gif')
......@@ -107,7 +106,7 @@ class PILTransformsTest(ATSiteTestCase):
self.pt.registerTransform(image_to_jpeg())
imgFile = open(input_file_path('logo.gif'), 'rb')
data = imgFile.read()
self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/gif')
self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/gif')
data = self.pt.convertTo(target_mimetype='image/jpeg',orig=data)
self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/jpeg')
......@@ -115,7 +114,7 @@ class PILTransformsTest(ATSiteTestCase):
self.pt.registerTransform(image_to_png())
imgFile = open(input_file_path('logo.jpg'), 'rb')
data = imgFile.read()
self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/jpeg')
self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/jpeg')
data = self.pt.convertTo(target_mimetype='image/png',orig=data)
self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/png')
......@@ -123,7 +122,7 @@ class PILTransformsTest(ATSiteTestCase):
self.pt.registerTransform(image_to_pcx())
imgFile = open(input_file_path('logo.gif'), 'rb')
data = imgFile.read()
self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/gif')
self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/gif')
data = self.pt.convertTo(target_mimetype='image/pcx',orig=data)
self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/pcx')
......@@ -131,7 +130,7 @@ class PILTransformsTest(ATSiteTestCase):
self.pt.registerTransform(image_to_ppm())
imgFile = open(input_file_path('logo.png'), 'rb')
data = imgFile.read()
self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/png')
self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/png')
data = self.pt.convertTo(target_mimetype='image/x-portable-pixmap',orig=data)
self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/x-portable-pixmap')
......@@ -139,7 +138,7 @@ class PILTransformsTest(ATSiteTestCase):
self.pt.registerTransform(image_to_tiff())
imgFile = open(input_file_path('logo.jpg'), 'rb')
data = imgFile.read()
self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/jpeg')
self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/jpeg')
data = self.pt.convertTo(target_mimetype='image/tiff',orig=data)
self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/tiff')
......
......@@ -5,6 +5,7 @@ from sys import modules
from os.path import join, abspath, dirname, basename
def normalize_html(s):
s = re.sub(r"&nbsp;", " ", s)
s = re.sub(r"\s+", " ", s)
s = re.sub(r"(?s)\s+<", "<", s)
s = re.sub(r"(?s)>\s+", ">", s)
......
"""
Uses the http://www.freewisdom.org/projects/python-markdown/ module to do its handy work
author: Tom Lazar <tom@tomster.org> at the archipelago sprint 2006
Uses the http://www.freewisdom.org/projects/python-markdown/ module
Author: Tom Lazar <tom@tomster.org> at the archipelago sprint 2006
"""
import os
from zope.interface import implements
from Products.CMFDefault.utils import bodyfinder
from Products.PortalTransforms.interfaces import ITransform
from Products.PortalTransforms.libtransforms.commandtransform import commandtransform
from Products.PortalTransforms.libtransforms.utils import bin_search
from Products.PortalTransforms.libtransforms.utils import sansext
from Products.PortalTransforms.utils import log
try:
......@@ -23,7 +16,7 @@ except ImportError:
log('markdown_to_html: Could not import python-markdown.')
else:
HAS_MARKDOWN = True
class markdown:
implements(ITransform)
......@@ -37,11 +30,16 @@ class markdown:
def convert(self, orig, data, **kwargs):
if HAS_MARKDOWN:
html = markdown_transformer.markdown(orig)
# markdown expects unicode input:
orig = unicode(orig.decode('utf-8'))
# PortalTransforms, however expects a string as result,
# so we encode the unicode result back to UTF8:
html = markdown_transformer.markdown(orig).encode('utf-8')
else:
html = orig
data.setData(html)
return data
def register():
return markdown()
import re, tempfile
import os, os.path
from Products.PortalTransforms.libtransforms.utils import bin_search, \
sansext, bodyfinder, scrubHTML
import os
from Products.PortalTransforms.libtransforms.utils import bodyfinder, scrubHTML
from Products.PortalTransforms.libtransforms.commandtransform import commandtransform
class document(commandtransform):
......
......@@ -31,7 +31,7 @@ VALID_TAGS['ins'] = 1
VALID_TAGS['del'] = 1
VALID_TAGS['q'] = 1
VALID_TAGS['map'] = 1
VALID_TAGS['area'] = 1
VALID_TAGS['area'] = 0
VALID_TAGS['abbr'] = 1
VALID_TAGS['acronym'] = 1
VALID_TAGS['var'] = 1
......@@ -71,6 +71,10 @@ VALID_TAGS['source'] = 1
VALID_TAGS['time'] = 1
VALID_TAGS['video'] = 1
# add some tags to nasty. These should also probably be backported to CMFDefault.
NASTY_TAGS['style'] = 1 # this helps improve Word HTML cleanup.
NASTY_TAGS['meta'] = 1 # allowed by parsers, but can cause unexpected behavior
msg_pat = """
<div class="system-message">
......@@ -203,7 +207,7 @@ class StrippingParser(HTMLParser):
if not self.raise_error: continue
else: raise IllegalHTML, 'Script event "%s" not allowed.' % k
elif v is None:
self.result.append(' %s' % (k,))
self.result.append(' %s' % k)
elif remove_script and hasScript(v):
if not self.raise_error: continue
else: raise IllegalHTML, 'Script URI "%s" not allowed.' % v
......@@ -238,6 +242,26 @@ class StrippingParser(HTMLParser):
self.result.append('</%s>' % tag)
#remTag = '</%s>' % tag
def parse_declaration(self, i):
"""Fix handling of CDATA sections. Code borrowed from BeautifulSoup.
"""
j = None
if self.rawdata[i:i+9] == '<![CDATA[':
k = self.rawdata.find(']]>', i)
if k == -1:
k = len(self.rawdata)
data = self.rawdata[i+9:k]
j = k+3
self.result.append("<![CDATA[%s]]>" % data)
else:
try:
j = HTMLParser.parse_declaration(self, i)
except HTMLParseError:
toHandle = self.rawdata[i:]
self.result.append(toHandle)
j = i + len(toHandle)
return j
def getResult(self):
return ''.join(self.result)
......@@ -262,13 +286,13 @@ def scrubHTML(html, valid=VALID_TAGS, nasty=NASTY_TAGS,
class SafeHTML:
"""Simple transform which uses CMFDefault functions to
clean potentially bad tags.
clean potentially bad tags.
Tags must explicit be allowed in valid_tags to pass. Only
the tags themself are removed, not their contents. If tags
are removed and in nasty_tags, they are removed with
all of their contents.
all of their contents.
Objects will not be transformed again with changed settings.
You need to clear the cache by e.g.
1.) restarting your zope or
......@@ -291,6 +315,10 @@ class SafeHTML:
'output': self.output,
'valid_tags': VALID_TAGS,
'nasty_tags': NASTY_TAGS,
'stripped_attributes': ['lang','valign','halign','border','frame','rules','cellspacing','cellpadding','bgcolor'],
'stripped_combinations': {'table th td': 'width height'},
'style_whitelist': ['text-align', 'list-style-type', 'float'],
'class_blacklist': [],
'remove_javascript': 1,
'disable_transform': 0,
'default_encoding': 'utf-8',
......@@ -310,6 +338,19 @@ class SafeHTML:
'everything they contain (like applet, object). ' +
'They are only deleted if they are not marked as valid_tags.',
('tag', 'value')),
'stripped_attributes': ('list',
'stripped_attributes',
'These attributes are stripped from any tag.'),
'stripped_combinations' : ('dict',
'stripped_combinations',
'These attributes are stripped from any tag.',
('tag', 'value')),
'style_whitelist': ('list',
'style_whitelist',
'These CSS styles are allowed in style attributes.'),
'class_blacklist': ('list',
'class_blacklist',
'These class names are not allowed in class attributes.'),
'remove_javascript' : ("int",
'remove_javascript',
'1 to remove javascript attributes that begin with on (e.g. onClick) ' +
......@@ -355,7 +396,9 @@ class SafeHTML:
repaired = 0
while True:
try:
orig = scrubHTML(
# Do 2 passes. This provides more reliable filtering of certain
# malicious HTML (cf upstream commit svn10522).
for repeat in range(2): orig = scrubHTML(
orig,
valid=self.config.get('valid_tags', {}),
nasty=self.config.get('nasty_tags', {}),
......@@ -366,6 +409,8 @@ class SafeHTML:
data.setData(msg_pat % ("Error", str(inst)))
break
except HTMLParseError:
if repeat:
raise # try to repair only on first pass
# ouch !
# HTMLParser is not able to parse very dirty HTML string
if not repaired:
......
......@@ -45,20 +45,23 @@ class word_to_html:
def convert(self, data, cache, **kwargs):
orig_file = 'unknown.doc'
doc = None
try:
doc = document(orig_file, data)
doc.convert()
html = doc.html()
doc = document(orig_file, data)
doc.convert()
html = doc.html()
path, images = doc.subObjects(doc.tmpdir)
objects = {}
if images:
doc.fixImages(path, images, objects)
path, images = doc.subObjects(doc.tmpdir)
objects = {}
if images:
doc.fixImages(path, images, objects)
doc.cleanDir(doc.tmpdir)
cache.setData(html)
cache.setSubObjects(objects)
return cache
cache.setData(html)
cache.setSubObjects(objects)
return cache
finally:
if doc is not None:
doc.cleanDir(doc.tmpdir)
def register():
return word_to_html()
......@@ -6,37 +6,37 @@ from Products.PortalTransforms.libtransforms.utils import bin_search, MissingBin
COMMAND_CONFIGS = (
('lynx_dump', '.html',
{'binary_path' : 'lynx',
'command_line' : '-dump %s',
'command_line' : '-dump %(input)s',
'inputs' : ('text/html',),
'output' : 'text/plain',
}),
('tidy_html', '.html',
{'binary_path' : 'tidy',
'command_line' : '%s',
'command_line' : '%(input)s',
'inputs' : ('text/html',),
'output' : 'text/html',
}),
('rtf_to_html', None,
{'binary_path' : 'unrtf',
'command_line' : '%s',
'command_line' : '%(input)s',
'inputs' : ('application/rtf',),
'output' : 'text/html',
}),
('ppt_to_html', None,
{'binary_path' : 'ppthtml',
'command_line' : '%s',
'command_line' : '%(input)s',
'inputs' : ('application/vnd.ms-powerpoint',),
'output' : 'text/html',
}),
('excel_to_html', None,
{'binary_path' : 'xlhtml',
'command_line' : '-nh -a %s',
'command_line' : '-nh -a %(input)s',
'inputs' : ('application/vnd.ms-excel',),
'output' : 'text/html',
}),
('ps_to_text', None,
{'binary_path' : 'ps2ascii',
'command_line' : '%s',
'command_line' : '%(input)s',
'inputs' : ('application/postscript',),
'output' : 'text/plain',
}),
......
......@@ -8,10 +8,10 @@ class TransformException(Exception):
FB_REGISTRY = None
# logging function
from zLOG import LOG, INFO
from zLOG import LOG, DEBUG
#logger = logging.getLogger('PortalTransforms')
def log(message, severity=INFO):
def log(message, severity=DEBUG):
LOG('PortalTransforms', severity, message)
#logger.log(severity, message)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment