Better Plone development quality through...

  • tests
  • fewer skin customizations and monkeypatches
  • more generic code
  • motivation
  • how to get there

Automated tests

Why do we care?

  • Tests help us make better quality code
  • Better quality means a more satisfied customer

Automated tests (2)

  • Testing through the web is no alternative:
    • Slower (think 3rd time you fake something TTW to test a feature)
    • Regression bugs
  • Automated tests mean better quality in less time

Tests: Test-driven development

  • Essential part of XP
  • Think about what you want to do
  • Think about how you test it
  • Test-first: Write a test and have it fail
  • Leads to better code, which allows for easy code changes (also refactoring) and helps in removing unneeded code (a.k.a. clutter)
  • Helps you find design/API problems

Tests: Debugging

  • A tested system has fewer bugs
  • pdb is your friend: Failing tests are very helpful for finding bugs. Put the pdb where the heart of the problem is -> immediate feedback!
  • When you have tests but you're still encountering many errors through the web, you're not testing hard enough. The problem with this situation is that people might ask: Why do we have tests at all? or Tests aren't that useful after all.

Tests: Unit tests

  • Test against interfaces of a unit (again: makes you think of what interface you want)
  • Test everything that you think could fail. One test per function is too little!
  • Add a test for every bug to show the bug and to help you fix the bug. And to avoid bug regression.

Tests: Example 1

Tests can be very good developer documentation if applied and documented properly. By these measures, this isn't quite the full shilling:

def testConnectAndDisconnect(self):
    triples = [(self.objs[0].UID(), self.objs[1].UID(), ruleset.getId())
               for ruleset in self.rulesets]
    processor.process(self.portal, connect=triples)
    for ruleset in self.rulesets:
        self.assertEquals(self.objs[0].getRefs(ruleset.getId())[0],
                          self.objs[1])

    processor.process(self.portal, disconnect=triples)
    self.assertEquals(len(self.objs[0].getRefs()), 0)
    self.assertEquals(len(self.objs[1].getBRefs()), 0)

This test is not documentation. Also, in case this test goes wrong, I'll have a hard time understanding what it does.

Tests: Example 2

An example for a test that doesn't really do something useful:

def test_tools(self):
    """
    """
    ids = self.portal.objectIds()
    self.failUnless('archetype_tool' in ids)

A couple of these and you are led to believe that the system is well tested when you run the tests.

Tests: Example 3

This is better:

# ...
# Note that 'f1' is a folder that's owned by the manager, but
# we still can delete and rename objects that we own inside
# that folder.

# anotheritem1 isn't owned by us.  So we can't folder_rename
# or folder_delete.
paths = request['paths'] = [pathOf(f1.anotheritem1)]
self.assertRaises(Unauthorized,
                  f1.folder_rename,
                  paths=paths,
                  new_ids=['mynewitem'],
                  new_titles=['Junk'])
f1.folder_delete()
self.failUnless('anotheritem1' in f1.contentIds())


# We own myitem1.  That means we can folder_rename and
# folder_delete it.
f1.folder_rename(paths=[pathOf(f1.myitem1)],
                 new_ids=['myfreshitem1'],
                 new_titles=['Junk'])
self.failUnless('myfreshitem1' in f1.contentIds())
self.failIf('myitem1' in f1.contentIds())

Tests: Example 4

And this is terriffic ;-):

class ReferenceSource(object):
    """A base implementation of an IReferenceSource adapter that
    relies on a IReferenceQuery local utility to look up targets and
    on an IReferenceStorage adapter to add references.

    Basic setup:

        >>> import archetypes.testing
        >>> archetypes.testing.setupPortal()
        >>> archetypes.testing.wireUp()

    We create an object that we adapt to our ReferenceSource:

        >>> obj = archetypes.testing.FakeObject('sourceobj')
        >>> source = ReferenceSource(obj)

Tests: Example 4 (cont)

Continued example:

We don't have any targets or relationships at this point:

    >>> source.getTargets()
    []
    >>> source.getRelationships()
    set([])

Let's add a target:

    >>> target = archetypes.testing.FakeObject('targetobj')
    >>> ref = source.addReference(target, 'pooh')
    >>> ref
    <archetypes.testing.FakeReference object at ...>
    >>> ref.source is obj
    True
    >>> ref.target is target
    True

Good things:

  • Serves as developer documentation
  • Uses mockup objects

Tests: Integration tests

  • Also known as functional tests
  • We need both unit tests and integrational tests; they test our software at different levels
  • Black-box tests: test the composition of units from the outside, in our case: from the HTTP level
  • Well suited for acceptance tests and user documentation if in the form of zope.testbrowser (traditional functional tests are not suited for that)
  • Can be run against live (preview) system

Tests: Example 5

A zope.testbrowser test fragment:

...

Now that everything is in place, we can start two seperate browser
sessions.

>>> from Products.Five.testbrowser import Browser
>>> browser1 = Browser()
>>> browser2 = Browser()

Let's login with the first browser:

>>> browser1.open('http://nohost/plone/')
>>> browser1.getLink('Log in').click()
>>> browser1.url
'http://nohost/plone/login_form'
>>> browser1.getControl(name='__ac_name').value = 'test_user_1_'
>>> browser1.getControl(name='__ac_password').value = 'secret'

Tests: Example 5 (cont)

Continued example:

>>> browser1.getControl(name='submit').click()
>>> "You are now logged in" in browser1.contents
True

At this point, the first browser has two cookies:

>>> printCookies(browser1) # doctest: +ELLIPSIS
__ac="..."
koekje="..."
>>> len(listCookies(browser2))
0

Tests: Traditional style

  • Uses Python's unittest.
  • Tests: Example 1 to 3
  • We know this style
  • Still, we have to write more and better tests in this style!

Tests: Doctest style

Tests: Doctest style (2)

  • Doctests can be in:
    • seperate text files (doctest.DocFileSuite), e.g. the "README.txt" we just saw (Emacs rst-mode)
    • docstrings of implementation (doctest.DocTestSuite), e.g. Tests: Example 4
      • easy to find: documentation where the code is
      • get crowded if you address too many corner cases
    • docstrings of test class

Skin Customizations

  • Allow for easy through the web rapid style development
  • But they have problems:
    • not easily testable
    • violate against DRY: lots of code duplication
    • too much logic in templates, python scripts
    • result in dependency problems between versions of Plone (breakage not easily testable)

Skin Customizations (2)

  • Skin customizations are not avoidable in some cases, but their number should be minimized. Examples:

    • for skins: try to do it with CSS only (DIYPloneStyle)
    • for template/python (script) logic: try to do it in views or unrestricted code
    • try to make it a configuration, try to push your changes down into Plone

Monkeypatches

  • Monkey patching is bad
  • Try to find other ways to do it (e.g. inheritance, adapters)

More generic code

  • By more generic code we mean reusable code
  • Reusable and tested components will be used by others who find it helpful
    • more features for things we need (think about Danny's UI enhancements for OrderableReferenceField)
    • share maintenance work with others
  • General code helps us being good open source citizens and contribute back

Motivate ourselves

What makes a motivated developer?

Motivate ourselves (2)

  • Doing something well is fun
  • Guessing and fearing breakage is not fun
  • Learning is fun. Besides that, it'll increase your personal value, which means $$$
  • $$$ is exciting

Motivate ourselves (3)

  • Python is exciting
  • Zope 2 is not exciting (too much guessing, hard to learn), but Python is still exciting
  • Zope 3 is new and exciting, and with it Python gets even more exciting
  • Doing something new is exciting
  • Contributing back to the community is exciting
  • Nice working environment and understanding for the other is good
  • Helping each other is good

Motivate customers

I think we're pretty good at motivating customers, but still:

  • Surpass their expectations, by showing them terrific work
  • Never leave QA work to the customer; if something isn't tested, it shouldn't be released to the customer
  • Large customers have higher expectations than merely Plone; we want to support their ideas and visions in the best possible way
  • A motivated customer is the best advertisement

How to get there

  • This is all nice and good, but how should we explain this to the customer?
    • It's not something that we need to explain to the customer
    • Don't let payability get in the way of progress
    • The customer expects us to learn in order to deliver quality software
    • We have to deliberately and continuously work on ourselves because of the nature of our business
    • Learning means we can do things better and faster (Others are getting better, too)

Hot to get there (2)

  • It will pay off sooner than you think!
    • Think how much time you spend bughunting
    • Bughunting is not fun
    • Good software designs are fun

How to get there (3)

  • Ask your colleagues if you don't know how to apply a certain technique!
    • Doesn't cost you anything
    • Means $$$

How to get there (4)

  • Pair program:
    • Team spirit
    • Learning is best done face to face
    • Pair programming is the best knowledge transfer
    • Take your time because you don't have anything better to do!
    • By transferring knowledge everyone grows
    • I think we should pair program all the time (any combination of skills works!)

The Zen of Python, by Tim Peters

  • Beautiful is better than ugly.
  • Explicit is better than implicit.
  • Simple is better than complex.
  • Complex is better than complicated.
  • Flat is better than nested.
  • Sparse is better than dense.
  • Readability counts.
  • Special cases aren't special enough to break the rules.
  • Although practicality beats purity.

The Zen of Python, by Tim Peters (2)

  • Errors should never pass silently.
  • Unless explicitly silenced.
  • In the face of ambiguity, refuse the temptation to guess.
  • There should be one-- and preferably only one --obvious way to do it.
  • Although that way may not be obvious at first unless you're Dutch.
  • Now is better than never.
  • Although never is often better than right now.
  • If the implementation is hard to explain, it's a bad idea.
  • If the implementation is easy to explain, it may be a good idea.
  • Namespaces are one honking great idea -- let's do more of those!