Developing a nose-test plugin to time python tests

by Mahmoud on February 28, 2011

Nose is a fantastic testing framework. What surprises me though, is that there’s no out of the box plugin to time tests to see which tests are the slowest, and most likely, problematic. After all, unit tests are supposed to be wicked fast. I googled, but nothing really came up except an insightful google-groups post.

I figured, what the hey, might as well just write a simple nose plugin to time the tests. I modeled it slightly off the xunit nose plugin. With the xunit plugin as a guiding example, I thought it was pretty trivial to write a nose plugin, definitely a testament to the great design chosen by nose.

Here’s the plugin below:

"""This plugin provides test timings to identify which tests might be
taking the most. From this information, it might be useful to couple
individual tests nose's `--with-profile` option to profile problematic
tests.

This plugin is heavily influenced by nose's `xunit` plugin.

Add this command to the way you execute nose::

--with-test-timer

After all tests are executed, they will be sorted in ascending order.

(c) 2011 - Mahmoud Abdelkader (http://github.com/mahmoudimus)

LICENSE:
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004

Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>

Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.

DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

0. You just DO WHAT THE FUCK YOU WANT TO.

"""

import operator
from time import time

import nose
from nose.plugins.base import Plugin


class TestTimer(Plugin):
    """This plugin provides test timings

"""

    name = 'test-timer'
    score = 1

    def _timeTaken(self):
        if hasattr(self, '_timer'):
            taken = time() - self._timer
        else:
            # test died before it ran (probably error in setup())
            # or success/failure added before test started probably
            # due to custom TestResult munging
            taken = 0.0
        return taken

    def options(self, parser, env):
        """Sets additional command line options."""
        super(TestTimer, self).options(parser, env)

    def configure(self, options, config):
        """Configures the test timer plugin."""
        super(TestTimer, self).configure(options, config)
        self.config = config
        self._timed_tests = {}

    def startTest(self, test):
        """Initializes a timer before starting a test."""
        self._timer = time()

    def report(self, stream):
        """Report the test times"""
        if not self.enabled:
            return
        d = sorted(self._timed_tests.iteritems(), key=operator.itemgetter(1))
        for test, time_taken in d:
            stream.writeln("%s: %0.4f" % (test, time_taken))

    def _register_time(self, test):
        self._timed_tests[test.id()] = self._timeTaken()

    def addError(self, test, err, capt=None):
        self._register_time(test)

    def addFailure(self, test, err, capt=None, tb_info=None):
        self._register_time(test)

    def addSuccess(self, test, capt=None):
        self._register_time(test)


if __name__ == '__main__':
    nose.main(addplugins=[TestTimer()])

Something cool about nose was that you don’t have to install a plugin package-wide using setuptools, but you can actually just dynamically add it during run-time. All you have to do is:

import nose
from yourplugin import YourPlugin

if __name__ == '__main__':
    nose.main(addplugins=[YourPlugin()])

This way, you can just execute your tests as normal, like so:

python nose-testtimers.py --with-test-timer -sv --debug=sqlalchemy.engine

and you get a nice little output of test times :)

  • Kamal

    What did your setup.py look like, i am trying to install, but have problems

  • http://mahmoudimus.com/ Mahmoud Abdelkader

    Hi Kamal. Thanks for responding. Here’s my setup.py:

    from setuptools import setup
    from setuptools import find_packages

    setup(
    name=’application’,
    version=’1.0′,
    description=”My Application”,
    author=”,
    author_email=”,
    url=’https://github.com/mahmoudimus/’ ,
    install_requires=[
    'werkzeug>=0.6.2',
    'mako>=0.3.6',
    'simplejson',
    'mock',
    'nose==1.0.0',
    'boto>=2.0a2',
    'SQLAlchemy>=0.6.3',
    ],
    setup_requires=[],
    packages=find_packages(),
    include_package_data=True,
    test_suite=’nose.collector’,
    zip_safe=False,
    entry_points={
    ‘nose.plugins.0.10′: [
    'with-test-timers = application.utils.test_timer:TestTimer',
    ]
    },
    )

    Hope this helps. This works because I have a file located at application/utils/test_timer.py, referenced under the entry_points keyword argument to the setup() function in my setup.py. In the test_timer.py file, I have the gist recipe I posted about above.

    You can see if this works by doing a nosetests --help and that should show you the --with-test-timers option.

    Let me know if you’re still having trouble.

  • http://www.prlog.org/11289974-phone-number-lookup-verizon-phone-number-reverse-lookup-to-get-information-you-need-quickly.html phone number lookup

    You really know how to bring an issue to light and make it important.

  • http://zhenech.myopenid.com/ Guest

    you may want to correct that, it’s `–with-test-timer`, as your plugin is called test-timer, not test-timers :)

  • http://mahmoudimus.com/ Mahmoud

    Ah, you’re right. Fixed.

blog comments powered by Disqus

Previous post: