Python Nose: Extending it.

Hello There!. This article is the sequel of the Starting with nose post.

On this article i will describe how to write a basic nosetests plugin.

I used this approach for a RedHat project that i had contributed in the past called Autotest. On this case, they were using non-standard nomenclatures and running their own in-house builtin tests runner.

The code of this contribution can be found on the following pull request and can be very helpful to start customizing your runner.

Also you can review a official reference here

Selectors

By default, nose uses a nose.selector.Selector instance to decide what is and is not a test. The default selector is fairly simple: for the most part, if an object’s name matches the testMatch regular expression defined in the active nose.config.Config instance, the object is selected as a test.

Every time you run the nosetests command by default all the files matching the test_* filename are marked as possible test candidates. This filter regex parameter is extensible by using:

$ nosetests --match='^Foo[\b_\./-])[Tt]est'

That pattern is fine for almost all the projects in that i had worked on, but could be not enough for projects using non default filenames or even on projects in which test cases levels are splitted based on the filename.

For this cases, you can create a custom selectors. Next there is pretty simple usage example for a custom selector:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from nose.selector import Selector

logger = logging.getLogger(__name__)

class CustomTestSelector(Selector):

    def wantDirectory(self, dirname):
    parts = dirname.split(os.path.sep)
        return 'tests' in parts

    def wantModule(self, module):
        return True

    def wantFile(self, filename):
        if not filename.endswith('_unittest.py'):
            return False
        return True

On the above example we are checking if the file is on the tests/ directory and the filename ends with the _unittest.py postfix.

As you can see, the Selector API looks pretty clear: everytime you receive a new filename the directory, module and filenames are evaluated if all the functions returns True then the filename is added to the test list to be executed.

A selector has no reason to be, if it is not hooked to a Plugin, for this reason the next step is to describe how to build a basic plugin.

Reference: Nose Selectors

Plugins

Plugins extends the default behavior of a Nose runner to supports test collection, selection, observation and reporting.

On this example case, we need to extend the default test selection mechanism, the plugin for this selector looks like this:

class CustomSelectorPlugin(Plugin):

    enabled = True
    name = 'custom_selector'

    def configure(self, options, config):
        self.result_stream = sys.stdout

        config.logStream = self.result_stream
        self.testrunner = nose.core.TextTestRunner(stream=self.result_stream,
                                                   descriptions=True,
                                                   verbosity=2,
                                                   config=config)

    def options(self, parser, env):
        parser.add_option("--skip-tests",
                          dest="skip_tests",
                          default=[],
                          help='A space separated list of tests to skip')

    def prepareTestLoader(self, loader):
        loader.selector = CustomTestSelector(loader.config)

     def finalize(self, result):
        log.info('Plugin finalized!')

As you can read we are overloading 3 methods from the base Plugin class; configure, options and prepareTestLoader. The configure method is used to set the testrunner argument and specify some stdout streaming parameters. The options method receives a parser, which is basically the same as a ArgParse object that you can add options. The prepareTestLoader function receives a loader instance that on this case will be replaced by our custom CustomTestSelector for filter our desired test cases.

Once this is done you need to register this plugin into the nose runner.

For other attributes that can be extended from the base Plugin class, you can review the official documentation about how to write and extend nose.

Reference: Nose Plugins

Registering the plugin in the nose runner.

If you are using setuptools, the plugin must be included in the entry points of your package setup file.

Your plugin will become available to your nosetests command once you run install or develop.

setup(name='CustomSelectorPlugin',  
    entry_points = {
        'nose.plugins.0.10': [
            'custom_selector = custom_selector:CustomSelectorPlugin'
            ]
        },
    )

Also you can do this programatically without setuptools.

from .plugin import CustomSelectorPlugin  
import nose

def run_test():  
    nose.main(addplugins=[CustomSelectorPlugin()])

def main():  
    run_test()

if __name__ == '__main__':  
    main()

Then you can execute this file directly and all the default nosetests options will be available, also your newly created plugin options will become available as options.

Conclusions

Nose offers a pretty good API for extend the default behavior of collection, selection and reporting. The provided nose API's are pretty clear and concise to understand also the available official documentation is almost self explanatory. Is highly recommended to just use the default patterns on your tests schemas , but if this is not your case, feel free to extend nose using a similar approach :).

Next steps

On the next article on this nose series we will cover how to run tests in parallel and how to use test attributes for improve the time spent running the tests.

Keep reading :)


Jorge Niedbalski

Dev and Ops , and might be the opposite.


View or Post Comments