A testing framework that extends unittest with plugins and enhanced discovery capabilities
—
Framework for creating custom plugins that extend nose2's functionality through a comprehensive hook system and event-driven architecture. Plugins can modify behavior at every stage of test execution.
All nose2 plugins must inherit from the Plugin base class and implement hook methods to extend functionality.
class Plugin:
"""
Base class for nose2 plugins.
All nose2 plugins must subclass this class and implement hook methods
to extend test execution functionality.
"""
# Class attributes for plugin configuration
commandLineSwitch: tuple # (short_opt, long_opt, help_text)
configSection: str # Config file section name
alwaysOn: bool # Auto-register plugin flag
# Instance attributes set during initialization
session: Session # Test run session
config: Config # Plugin configuration section
def __init__(self, **kwargs):
"""
Initialize plugin.
Config values should be extracted from self.config in __init__
for sphinx documentation generation to work properly.
"""
def register(self):
"""
Register plugin with session hooks.
Called automatically if alwaysOn=True or command line switch is used.
"""
def addOption(self, callback, short_opt, long_opt, help_text=None, nargs=0):
"""
Add command line option.
Parameters:
- callback: Function to call when option is used, or list to append values to
- short_opt: Single character short option (must be uppercase, no dashes)
- long_opt: Long option name (without dashes)
- help_text: Help text for option
- nargs: Number of arguments (default: 0)
"""
def addArgument(self, callback, short_opt, long_opt, help_text=None):
"""
Add command line option that takes one argument.
Parameters:
- callback: Function to call when option is used (receives one argument)
- short_opt: Single character short option (must be uppercase, no dashes)
- long_opt: Long option name (without dashes)
- help_text: Help text for option
"""The Session class coordinates plugin loading, configuration, and execution.
class Session:
"""
Configuration session that encapsulates all configuration for a test run.
"""
# Core attributes
argparse: argparse.ArgumentParser # Command line parser
pluginargs: argparse.ArgumentGroup # Plugin argument group
hooks: PluginInterface # Plugin hook interface
plugins: list # List of loaded plugins
config: ConfigParser # Configuration parser
# Test run configuration
verbosity: int # Verbosity level
startDir: str # Test discovery start directory
topLevelDir: str # Top-level project directory
testResult: PluggableTestResult # Test result instance
testLoader: PluggableTestLoader # Test loader instance
logLevel: int # Logging level
def __init__(self):
"""Initialize session with default configuration."""
def get(self, section):
"""
Get a config section.
Parameters:
- section: The section name to retrieve
Returns:
Config instance for the section
"""
def loadConfigFiles(self, *filenames):
"""
Load configuration from files.
Parameters:
- filenames: Configuration file paths to load
"""
def loadPlugins(self, plugins, exclude):
"""
Load plugins into the session.
Parameters:
- plugins: List of plugin module names to load
- exclude: List of plugin module names to exclude
"""
def setVerbosity(self, verbosity, verbose, quiet):
"""
Set verbosity level from configuration and command line.
Parameters:
- verbosity: Base verbosity level
- verbose: Number of -v flags
- quiet: Number of -q flags
"""
def isPluginLoaded(self, plugin_name):
"""
Check if a plugin is loaded.
Parameters:
- plugin_name: Full plugin module name
Returns:
True if plugin is loaded, False otherwise
"""The PluginInterface provides the hook system for plugin method calls.
class PluginInterface:
"""Interface for plugin method hooks."""
def register(self, method_name, plugin):
"""
Register a plugin method for a hook.
Parameters:
- method_name: Name of hook method
- plugin: Plugin instance to register
"""
# Hook methods (examples - many more available)
def loadTestsFromModule(self, event):
"""Called when loading tests from a module."""
def loadTestsFromName(self, event):
"""Called when loading tests from a name."""
def startTest(self, event):
"""Called when a test starts."""
def stopTest(self, event):
"""Called when a test stops."""
def testOutcome(self, event):
"""Called when a test completes with an outcome."""
def createTests(self, event):
"""Called to create the top-level test suite."""
def runnerCreated(self, event):
"""Called when test runner is created."""Events carry information between the test framework and plugins.
class Event:
"""Base class for all plugin events."""
handled: bool # Set to True if plugin handles the event
class LoadFromModuleEvent(Event):
"""Event fired when loading tests from a module."""
def __init__(self, loader, module):
self.loader = loader
self.module = module
self.extraTests = []
class StartTestEvent(Event):
"""Event fired when a test starts."""
def __init__(self, test, result, startTime):
self.test = test
self.result = result
self.startTime = startTime
class TestOutcomeEvent(Event):
"""Event fired when a test completes."""
def __init__(self, test, result, outcome, err=None, reason=None, expected=None):
self.test = test
self.result = result
self.outcome = outcome # 'error', 'failed', 'skipped', 'passed', 'subtest'
self.err = err
self.reason = reason
self.expected = expectedfrom nose2.events import Plugin
class TimingPlugin(Plugin):
"""Plugin that times test execution."""
configSection = 'timing'
commandLineSwitch = ('T', 'timing', 'Time test execution')
def __init__(self):
# Extract config values in __init__
self.enabled = self.config.as_bool('enabled', default=True)
self.threshold = self.config.as_float('threshold', default=1.0)
self.times = {}
def startTest(self, event):
"""Record test start time."""
import time
self.times[event.test] = time.time()
def stopTest(self, event):
"""Calculate and report test time."""
import time
if event.test in self.times:
elapsed = time.time() - self.times[event.test]
if elapsed > self.threshold:
print(f"SLOW: {event.test} took {elapsed:.2f}s")
del self.times[event.test]from nose2.events import Plugin
class DatabasePlugin(Plugin):
"""Plugin for database test setup."""
configSection = 'database'
alwaysOn = True # Always load this plugin
def __init__(self):
# Read configuration
self.db_url = self.config.as_str('url', default='sqlite:///:memory:')
self.reset_db = self.config.as_bool('reset', default=True)
self.fixtures = self.config.as_list('fixtures', default=[])
# Add command line options
self.addArgument('--db-url', help='Database URL')
self.addArgument('--no-db-reset', action='store_true',
help='Skip database reset')
def handleArgs(self, event):
"""Handle command line arguments."""
args = event.args
if hasattr(args, 'db_url') and args.db_url:
self.db_url = args.db_url
if hasattr(args, 'no_db_reset') and args.no_db_reset:
self.reset_db = False
def createTests(self, event):
"""Set up database before creating tests."""
if self.reset_db:
self.setup_database()
def setup_database(self):
"""Initialize database with fixtures."""
# Database setup code
passfrom nose2.events import Plugin
class CustomLoaderPlugin(Plugin):
"""Custom test loader plugin."""
configSection = 'custom-loader'
commandLineSwitch = ('C', 'custom-loader', 'Use custom test loader')
def __init__(self):
self.pattern = self.config.as_str('pattern', default='spec_*.py')
self.base_class = self.config.as_str('base_class', default='Specification')
def loadTestsFromModule(self, event):
"""Load tests using custom pattern."""
module = event.module
tests = []
# Custom loading logic
for name in dir(module):
obj = getattr(module, name)
if (isinstance(obj, type) and
issubclass(obj, unittest.TestCase) and
obj.__name__.startswith(self.base_class)):
suite = self.session.testLoader.loadTestsFromTestCase(obj)
tests.append(suite)
if tests:
event.extraTests.extend(tests)from nose2.events import Plugin
class CustomResultPlugin(Plugin):
"""Custom test result reporter."""
configSection = 'custom-result'
def __init__(self):
self.output_file = self.config.as_str('output', default='results.txt')
self.include_passed = self.config.as_bool('include_passed', default=False)
self.results = []
def testOutcome(self, event):
"""Record test outcomes."""
test_info = {
'test': str(event.test),
'outcome': event.outcome,
'error': str(event.err) if event.err else None,
'reason': event.reason
}
if event.outcome != 'passed' or self.include_passed:
self.results.append(test_info)
def afterTestRun(self, event):
"""Write results to file after test run."""
with open(self.output_file, 'w') as f:
for result in self.results:
f.write(f"{result['test']}: {result['outcome']}\n")
if result['error']:
f.write(f" Error: {result['error']}\n")
if result['reason']:
f.write(f" Reason: {result['reason']}\n")# In your plugin module (e.g., my_plugins.py)
from nose2.events import Plugin
class MyPlugin(Plugin):
configSection = 'my-plugin'
def __init__(self):
pass
def startTest(self, event):
print(f"Starting test: {event.test}")
# To use the plugin:
# 1. Via command line: nose2 --plugin my_plugins.MyPlugin
# 2. Via config file:
# [unittest]
# plugins = my_plugins.MyPlugin
# 3. Via programmatic loading:
# from nose2.main import PluggableTestProgram
# PluggableTestProgram(plugins=['my_plugins.MyPlugin'])# unittest.cfg or nose2.cfg
[unittest]
plugins = my_plugins.TimingPlugin
my_plugins.DatabasePlugin
[timing]
enabled = true
threshold = 0.5
[database]
url = postgresql://localhost/test_db
reset = true
fixtures = users.json
products.json
[my-plugin]
some_option = value
flag = truefrom nose2.events import Plugin
class LayeredPlugin(Plugin):
"""Plugin that works with test layers."""
def startTestRun(self, event):
"""Called at the start of the test run."""
self.setup_resources()
def stopTestRun(self, event):
"""Called at the end of the test run."""
self.cleanup_resources()
def startTestClass(self, event):
"""Called when starting tests in a test class."""
if hasattr(event.testClass, 'layer'):
self.setup_layer(event.testClass.layer)
def stopTestClass(self, event):
"""Called when finishing tests in a test class."""
if hasattr(event.testClass, 'layer'):
self.teardown_layer(event.testClass.layer)
def setup_resources(self):
"""Set up resources for entire test run."""
pass
def cleanup_resources(self):
"""Clean up resources after test run."""
pass
def setup_layer(self, layer):
"""Set up resources for a test layer."""
pass
def teardown_layer(self, layer):
"""Tear down resources for a test layer."""
passInstall with Tessl CLI
npx tessl i tessl/pypi-nose2