CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-nose2

A testing framework that extends unittest with plugins and enhanced discovery capabilities

Pending
Overview
Eval results
Files

plugin-development.mddocs/

Plugin Development

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.

Capabilities

Plugin Base Class

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
        """

Session Management

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
        """

Plugin Interface and Hooks

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."""

Event Classes

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 = expected

Usage Examples

Simple Plugin

from 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]

Configuration-Based Plugin

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
        pass

Test Loader Plugin

from 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)

Result Plugin

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")

Plugin Registration

# 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'])

Plugin Configuration

# 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 = true

Advanced Plugin Patterns

from 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."""
        pass

Install with Tessl CLI

npx tessl i tessl/pypi-nose2

docs

configuration.md

index.md

plugin-development.md

test-discovery.md

test-tools.md

tile.json