Tests schreiben

Tests erstellen

Statt der bereits angelegten Datei src/vs.policy/vs/policy/tests.py erstellen wir ein eigenes tests-Modul:

$ rm -rf src/vs.policy/vs/policy/tests.py
$ mkdir src/vs.policy/vs/policy/tests
$ touch src/vs.policy/vs/policy/tests/__init__.py

Test-Fixture

Anschließend definieren wir im neu erstellten tests-Ordner zunächst ein Test-Fixture, eine gleichbleibende Testumgebung mit der Basisklasse TestCase, die an den Layer VS_POLICY_INTEGRATION gebunden wird. Hierzu erstellen wir im tests-Verzeichnis die Datei base.py mit folgendem Inhalt:

import unittest2 as unittest

from plone.testing import z2
from plone.app.testing import TEST_USER_NAME
from plone.app.testing import TEST_USER_PASSWORD

from vs.policy.tests import layer

def get_browser(app, loggedIn=True):
    browser = z2.Browser(app)
    if loggedIn:
        auth = 'Basic %s:%s' % (TEST_USER_NAME, TEST_USER_PASSWORD)
        browser.addHeader('Authorization', auth)
    return browser

class TestCase(unittest.TestCase):
    layer = layer.VS_POLICY_INTEGRATION

class FunctionalTestCase(unittest.TestCase):
    layer = layer.VS_POLICY_FUNCTIONAL

In layer.py werden anschließend die Test-Layer VS_POLICY_INTEGRATION und VS_POLICY_FUNCTIONAL definiert, die beide auf VS_POLICY_LAYER basieren:

from plone.app.testing import applyProfile
from plone.app.testing import PloneFixture
from plone.app.testing import PloneSandboxLayer
from plone.app.testing import PloneTestLifecycle
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.testing import z2
from zope.configuration import xmlconfig

class VsPolicyFixture(PloneFixture):
    # No sunburst please
    extensionProfiles = ()

VS_POLICY_FIXTURE = VsPolicyFixture()

class VsPolicyTestLifecycle(PloneTestLifecycle):
    defaultBases = (VS_POLICY_FIXTURE, )

class IntegrationTesting(VsPolicyTestLifecycle, z2.IntegrationTesting):
    pass

class FunctionalTesting(VsPolicyTestLifecycle, z2.FunctionalTesting):
    pass

class VsPolicyLayer(PloneSandboxLayer):
    defaultBases = (VS_POLICY_FIXTURE, )

    def setUpZope(self, app, configurationContext):
        import vs.policy

        xmlconfig.file("configure.zcml", vs.policy,
                       context=configurationContext)
        z2.installProduct(app, 'vs.policy')

    def tearDownZope(self, app):
        z2.uninstallProduct(app, 'vs.policy')

    def setUpPloneSite(self, portal):
        applyProfile(portal, 'vs.policy:default')

        setRoles(portal, TEST_USER_ID, ['Manager'])
        portal.invokeFactory('Folder', 'test-folder')
        setRoles(portal, TEST_USER_ID, ['Member'])

VS_POLICY_LAYER = VsPolicyLayer()
VS_POLICY_INTEGRATION = IntegrationTesting(
    bases=(VS_POLICY_LAYER, ), name="VsPolicyLayer:Integration")
VS_POLICY_FUNCTIONAL = FunctionalTesting(
    bases=(VS_POLICY_LAYER, ), name="VsPolicyLayer:Functional")

Tests

Die eigentlichen Tests werden in der Datei test_test.py definiert:

from vs.policy.tests.base import FunctionalTestCase

class TestTest(FunctionalTestCase):

    def test_test(self):
        self.assertTrue(True)

Unit Tests, die auf dem Python unittest-Modul, ZopeTestCase und PloneTestCase basieren, müssen sich an einige Namenskonventionen halten:

  • Alle Testdateien müssen mit test beginnen, z.B. test_setup.py.

  • In den Testdateien werden Klassen für Testfälle definiert, die ein oder mehrere Testmethoden enthalten können, die ebenfalls mit test beginnen müssen, z.B. test_portal_title.

  • Zunächst wird die Basisklasse importiert, dann die Klassen für die Testfälle und schließlich die Test Suite selbst definiert.

  • Jede Testsuite kann aus mehreren Testklassen bestehen. Wird die Testsuite ausgeführt, werden alle Testmethoden aller Testklassen der Test-Suite ausgeführt.

  • Innerhalb einer Testklasse kann die afterSetUp()-Methode unmittelbar vor jedem Test aufgerufen werden um Testdaten für diesen Test anzugeben. Nachdem der Test durchgeführt wurde, werden die Transaktionen zurückgenommen, so dass normalerweise keine Artefakte zurückbleiben.

  • Werden jedoch Änderungen außerhalb von Zope vorgenommen, müssen diese mit der Methode beforeTearDown() aufgeräumt werden.

  • Die in einer Testklasse verwendeten Methoden wie self.assertEqual() oder self.failUnless() sind Assertion-Methoden, und wenn eine von ihnen fehlschlägt, gilt der ganze Test als fehlgeschlagen.

Test- und Hilfsmethoden

Testmethoden überprüfen, ob etwas wahr oder falsch ist. Daher kann aus den Tests auch herausgelesen werden, wie sich Ihr Produkt verhalten soll, welche Fähigkeiten es enthält. Die Liste der Testmethoden ist ausführlich in der Python-Dokumentation für unittest.TestCaseObjects enthalten. Die häufigsten sind:

failUnless(expr)

stellt sicher, dass der Ausdruck expr wahr ist.

assertEqual(expr1, expr2)

stellt sicher,dass expr1 gleich expr2 ist.

assertRaises(exception, callable, ...)

stellt sicher, dass beim Aufruf von callable die Fehlermeldung exception ausgegeben wird.

Hinweis: callable sollte der Name einer Methode oder ein aufrufbares Objekt sein, nicht ein aktueller Aufruf, z.B.:

self.assertRaises(AttributeError, myObject.myMethod, someParameter)
fail()

Dies ist sinnvoll, wenn ein Test noch nicht fertiggestellt ist oder in einem if-Statement, das deutlich macht, dass der Test fehlgeschlagen ist.

ZopeTestCase und PloneTestCase fügen zu den Assertion-Methoden noch weitere hilfreiche Methoden und Variablen hinzu, die mit Zope interagieren. Hier nur kurz die wesentlichen Variablen:

self.portal

Die PloneSite, in der der Test ausgeführt wird.

self.folder

Der member-Ordner des Mitglieds, als der die Tests ausgeführt werden.

Und hier die wesentlichen Hilfsmethoden:

self.logout()

abmelden, d.i. die Rolle anonymous bekommen;

self.login()

sich erneut anmelden; wird ein Nutzername mit übergeben, erfolgt die Anmeldung als dieser Nutzer.

self.setRoles(roles)

durchläuft eine Liste von Rollen, die angenommen werden sollen.

self.setRoles((Manager,)) lässt Sie beispielsweise die Rolle des Managers für eine bestimmte Zeit annehmen.

self.setPermissions(permissions)

analog können auch Berechtigungen für den Testnutzer in self.folder angegeben werden;

self.setGroups(groups)

eine Liste von Gruppen, der der aktuelle Nutzer angehören soll.

Mehr über Unit Tests in Python erfahren Sie in der unittest-Python-Dokumentation.

Testen

Der Testrunner kann nun gestartet werden mit:

$ ./bin/test -s vs.policy

Wären die Tests geschrieben worden, bevor die Profile erstellt wurden, hätten beide Tests fehlschlagen müssen und der Testrunner folgendes ausgeben:

 AssertionError:"Welcome to Veit Schiele != ''
…
AssertionError:'Veit Schiele != 'Plone site'
Ran 2 tests with 2 failures and 0 errors

Nachdem die Profile angelegt wurden, sollte jedoch keiner der Tests fehlschlagen:

Ran 2 tests with 0 failures and 0 errors.

Filter

-s my.package, --package my.package, --dir my.package

durchsucht die angegebenen Verzeichnisse nach Tests.

-m test_setup, --module test_setup

spezifiziert ein Testmodul als regulären Ausdruck, z.B.:

$ ./bin/test -s my.package  -m 'test_setup'
-t  '.*installed.*', --test test_theme_installed

spezifiziert einen Testfilter als regulären Ausdruck, z.B.:

$ ./bin/test -s vs.policy -m '.*setup.*' -t '.*installed.*'

Hiermit werden im Paket vs.policy alle, mit installed endenden, Methoden in allen Testmodulen, die auf setup enden, durchlaufen.

-u, --unit

durchläuft ausschließlich Unit tests und ignoriert andere layer-Optionen.

-f, --non-unit

durchläuft alle Tests, die keine Unit Tests sind

Report

-v, --verbose

führt zu ausführlicherer Ausgabe

--ndiff

falls ein Doctest fehlschlägt, wird ndiff.py zur Darstellung der Unterschiede verwendet

--udiff

falls ein Doctest fehlschlägt, wird Unified Diff zur Darstellung der Unterschiede verwendet

--cdiff

falls ein Doctest fehlschlägt, wird Context Diff zur Darstellung der Unterschiede verwendet

Analyse

-d, post-mortem

stoppt die Ausführung nach dem ersten nicht-bestandenen Test und ermöglicht post-mortem-Debugging, d.h. die Debug-Session wird nur gestartet, wenn ein Test fehlschlägt.

Setup

--path src/my.package

fügt einen Pfad zu Pythons Suchpfad hinzu, wobei die Option mehrfach angegeben werden kann.

Weitere Optionen

Diese erhalten Sie mit:

$ ./bin/test --help

Wenn die relevanten Tests erfolgreich verliefen, sollten schließlich noch alle Tests durchgeführt werden um sicherzustellen, dass nicht an anderer Stelle etwas gebrochen ist. Wenn alle Tests erfolgreich durchlaufen wurden, erscheint eine Meldung:

Ran 10 tests with 0 failures and 0 errors in 4.830 seconds.

Falls nicht alle Tests erfolgreich durchlaufen wurden, ändert sich die Meldung:

Ran 10 tests with 2 failures and 3 errors in 9.688 seconds.

Dabei wurden dann zwei Tests nicht bestanden und drei Tests enthielten Fehler.

roadrunner

roadrunner ist ein Testrunner für Plone 2.5 bis 3.1, der die testgetriebene Entwicklung deutlich beschleunigen kann, da er vorab das Standard-Zope- und Plone-Environment für PloneTestCase läd. zur Installation wird einfach folgendes in die devel.cfg-Datei eingetragen:

[buildout]
parts =
    …
    roadrunner

[roadrunner]
recipe = roadrunner:plone
packages-under-test = vs.policy

Anschließend kann es wie der reguläre Zope-Testrunner aufgerufen werden:

$ ./bin/roadrunner -s vs.policy

Tipps & Tricks

  • Übernehmen Sie Tests z.B. aus Plone wenn diese Ihren eigenen Absichten entsprechen.

  • Dummy-Implementierungen sind häufig der einzige Weg um bestimmte Funktionen zu testen. Siehe auch CMFPlone/tests/dummy.py für einige Dummy-Objekt- Beispiele.

  • Tests können auch verwendet werden um Dinge auszuprobieren – sie sind eine sichere Umgebung.

  • Während des Debugging können print-Statements in den Test eingefügt werden um nachvollziehbare Hinweise im Terminal zu erhalten.

  • Es kann jedoch auch gleich der Python-Debugger in die Testmethoden importiert werden mit:

    import pdb; pdb.set_trace()
    

    Anschließend können Sie mit r schrittweise durch den Testkode gehen.

    Mehr zum Python-Debugger erfahren Sie in Debugging und in der Python-Dokumentation.