nose2

unit testingPythontesting frameworkunittest extensionpluginsparameterization

nose2

Overview

nose2 is a Python testing framework developed as the successor to the original nose testing framework, based on the unittest2 plugin branch. It provides a better plugin API and simplifies internal processes and interfaces. Since nose2 is based on unittest, you can start from the Python Standard Library's documentation for unittest and then use nose2 to add value on top of that.

Details

Key Features

Plugin System

  • Numerous plugins are incorporated directly into the nose2 module and loaded by default
  • Helps with test parameterization, organizing test fixtures into layers, capturing log messages, and reporting

Test Discovery and Execution

  • Looks for tests in Python files whose names start with test and runs every test function it discovers
  • Supports automatic test discovery

Enhanced Test Support

  • Supports more kinds of parameterized and generator tests than nose
  • Supports all test generators in test functions, test classes, and unittest TestCase subclasses
  • Does not require a separate package to be installed for test parameterization

Configuration-Driven Approach

  • Expects almost all configuration to be done via configuration files
  • Plugins should generally have only one command-line option (the option to activate the plugin)
  • Allows more repeatable test runs and keeps the command-line option set manageable

Pros and Cons

Pros

  1. unittest Compatibility: Full compatibility with Python's standard unittest library
  2. Rich Plugins: Provides many useful plugins by default
  3. Parameterized Tests: Supports test parameterization without additional packages
  4. Configuration Flexibility: Detailed customization through configuration files
  5. Gradual Migration: Can be gradually introduced to existing unittest-based tests

Cons

  1. Maintenance Status: Reduced active maintenance as of 2024
  2. Community Support: Limited community support compared to pytest
  3. New Project Recommendation: Maintainers recommend pytest for new projects
  4. Package-Level Fixtures: Like unittest2, package-level fixtures are not supported

Reference Links

Code Examples

Basic Test Class

import unittest

class TestCalculator(unittest.TestCase):
    
    def setUp(self):
        """Executed before each test method"""
        self.calculator = Calculator()
    
    def test_addition(self):
        """Test addition"""
        result = self.calculator.add(5, 3)
        self.assertEqual(result, 8)
    
    def test_subtraction(self):
        """Test subtraction"""
        result = self.calculator.subtract(10, 4)
        self.assertEqual(result, 6)
    
    def test_division_by_zero(self):
        """Test division by zero"""
        with self.assertRaises(ZeroDivisionError):
            self.calculator.divide(10, 0)
    
    def tearDown(self):
        """Executed after each test method"""
        self.calculator = None

if __name__ == '__main__':
    unittest.main()

Function-Based Tests

def test_string_operations():
    """Test string operations"""
    text = "Hello, World!"
    assert "World" in text
    assert len(text) == 13
    assert text.startswith("Hello")

def test_list_operations():
    """Test list operations"""
    numbers = [1, 2, 3, 4, 5]
    assert len(numbers) == 5
    assert 3 in numbers
    assert max(numbers) == 5

def test_dictionary_operations():
    """Test dictionary operations"""
    user_data = {"name": "Alice", "age": 30, "email": "[email protected]"}
    assert user_data["name"] == "Alice"
    assert "age" in user_data
    assert len(user_data) == 3

Parameterized Tests

import unittest
from nose2.tools import params

class TestMathOperations(unittest.TestCase):
    
    @params(
        (2, 3, 5),
        (10, 15, 25),
        (-1, 1, 0),
        (0, 0, 0),
    )
    def test_addition_params(self, a, b, expected):
        """Parameterized addition test"""
        calculator = Calculator()
        result = calculator.add(a, b)
        self.assertEqual(result, expected)
    
    @params(
        (10, 2, 5),
        (15, 3, 5),
        (100, 10, 10),
    )
    def test_division_params(self, dividend, divisor, expected):
        """Parameterized division test"""
        calculator = Calculator()
        result = calculator.divide(dividend, divisor)
        self.assertEqual(result, expected)

# Function-based parameterized tests
@params(
    ("hello", "world", "hello world"),
    ("foo", "bar", "foo bar"),
    ("", "test", " test"),
)
def test_string_concatenation(first, second, expected):
    """Parameterized string concatenation test"""
    processor = StringProcessor()
    result = processor.concatenate(first, second)
    assert result == expected

Layer Fixtures

import unittest
from nose2 import layers

class DatabaseLayer(layers.Layer):
    """Database layer fixture"""
    
    def setUp(self):
        """Layer setup - executed once"""
        self.connection = create_test_database()
        self.connection.execute("CREATE TABLE users (id INT, name TEXT)")
        print("Database initialized")
    
    def tearDown(self):
        """Layer teardown - executed once"""
        self.connection.close()
        print("Database closed")

# Create database layer instance
database_layer = DatabaseLayer()

class TestUserRepository(unittest.TestCase):
    
    layer = database_layer  # Specify layer
    
    def setUp(self):
        """Executed before each test"""
        self.repository = UserRepository(self.layer.connection)
        # Clean up test data
        self.layer.connection.execute("DELETE FROM users")
    
    def test_create_user(self):
        """Test user creation"""
        user = User(name="Alice", email="[email protected]")
        user_id = self.repository.create(user)
        self.assertIsNotNone(user_id)
        
        # Verify by retrieving from database
        saved_user = self.repository.get_by_id(user_id)
        self.assertEqual(saved_user.name, "Alice")
    
    def test_get_user_not_found(self):
        """Test retrieving non-existent user"""
        user = self.repository.get_by_id(999)
        self.assertIsNone(user)

Generator Tests

def test_number_operations():
    """Generator test example"""
    test_cases = [
        (lambda x: x * 2, 5, 10),
        (lambda x: x ** 2, 4, 16),
        (lambda x: x + 10, 15, 25),
    ]
    
    for operation, input_val, expected in test_cases:
        yield check_operation, operation, input_val, expected

def check_operation(operation, input_val, expected):
    """Execute individual test case"""
    result = operation(input_val)
    assert result == expected, f"Expected {expected}, got {result}"

def test_string_validations():
    """String validation generator test"""
    email_validator = EmailValidator()
    
    valid_emails = [
        "[email protected]",
        "[email protected]",
        "[email protected]"
    ]
    
    invalid_emails = [
        "invalid-email",
        "missing-at-sign.com",
        "@missing-local.com"
    ]
    
    # Test valid email addresses
    for email in valid_emails:
        yield check_valid_email, email_validator, email
    
    # Test invalid email addresses
    for email in invalid_emails:
        yield check_invalid_email, email_validator, email

def check_valid_email(validator, email):
    """Check valid email address"""
    assert validator.is_valid(email), f"Email {email} should be valid"

def check_invalid_email(validator, email):
    """Check invalid email address"""
    assert not validator.is_valid(email), f"Email {email} should be invalid"

nose2 Configuration File

# nose2.cfg or unittest.cfg
[unittest]
start-dir = tests
code-directories = src
test-file-pattern = test_*.py

# Test execution options
verbosity = 2
catch = True
buffer = True

# Plugin configuration
[log-capture]
always-on = True
clear-handlers = True
filter = -nose2
log-level = INFO

[coverage]
always-on = True
coverage = myproject
coverage-report = html
coverage-config = .coveragerc

[multiprocess]
always-on = False
processes = 4

[parameterized]
always-on = True

[printhooks]
always-on = False

Advanced Test Examples

import unittest
import tempfile
import os
from unittest.mock import patch, MagicMock

class TestFileProcessor(unittest.TestCase):
    
    def setUp(self):
        """Create temporary directory for tests"""
        self.temp_dir = tempfile.mkdtemp()
        self.processor = FileProcessor()
    
    def tearDown(self):
        """Clean up temporary directory"""
        import shutil
        shutil.rmtree(self.temp_dir)
    
    def test_process_file_success(self):
        """Test successful file processing"""
        # Create test file
        test_file = os.path.join(self.temp_dir, "test.txt")
        with open(test_file, "w") as f:
            f.write("Hello, World!")
        
        result = self.processor.process_file(test_file)
        self.assertTrue(result.success)
        self.assertEqual(result.lines_processed, 1)
    
    def test_process_nonexistent_file(self):
        """Test processing non-existent file"""
        nonexistent_file = os.path.join(self.temp_dir, "missing.txt")
        
        with self.assertRaises(FileNotFoundError):
            self.processor.process_file(nonexistent_file)
    
    @patch('mymodule.external_service')
    def test_process_with_external_service(self, mock_service):
        """Test with mocked external service"""
        # Configure mock
        mock_service.validate_content.return_value = True
        mock_service.upload_content.return_value = {"status": "success", "id": "123"}
        
        # Create test file
        test_file = os.path.join(self.temp_dir, "upload_test.txt")
        with open(test_file, "w") as f:
            f.write("Content to upload")
        
        result = self.processor.process_and_upload(test_file)
        
        # Assertions
        self.assertTrue(result.uploaded)
        self.assertEqual(result.upload_id, "123")
        
        # Verify mock calls
        mock_service.validate_content.assert_called_once()
        mock_service.upload_content.assert_called_once()
    
    def test_batch_processing(self):
        """Test batch processing"""
        # Create multiple test files
        test_files = []
        for i in range(3):
            file_path = os.path.join(self.temp_dir, f"batch_{i}.txt")
            with open(file_path, "w") as f:
                f.write(f"Content {i}")
            test_files.append(file_path)
        
        results = self.processor.process_batch(test_files)
        
        self.assertEqual(len(results), 3)
        for result in results:
            self.assertTrue(result.success)

Custom Assertions and Helpers

import unittest

class CustomAssertions:
    """Mixin for custom assertions"""
    
    def assertEmailValid(self, email):
        """Check email address validity"""
        import re
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        if not re.match(pattern, email):
            raise AssertionError(f"'{email}' is not a valid email address")
    
    def assertDateInRange(self, date_obj, start_date, end_date):
        """Check if date is within specified range"""
        if not (start_date <= date_obj <= end_date):
            raise AssertionError(
                f"Date {date_obj} is not between {start_date} and {end_date}"
            )
    
    def assertDictContainsSubset(self, subset, dictionary):
        """Check if dictionary contains subset"""
        for key, value in subset.items():
            if key not in dictionary:
                raise AssertionError(f"Key '{key}' not found in dictionary")
            if dictionary[key] != value:
                raise AssertionError(
                    f"Value for key '{key}': expected {value}, got {dictionary[key]}"
                )

class TestUserService(unittest.TestCase, CustomAssertions):
    
    def setUp(self):
        self.user_service = UserService()
    
    def test_create_user_with_valid_email(self):
        """Test user creation with valid email"""
        user_data = {
            "name": "John Doe",
            "email": "[email protected]",
            "age": 30
        }
        
        user = self.user_service.create_user(user_data)
        
        # Use custom assertions
        self.assertEmailValid(user.email)
        self.assertDictContainsSubset(user_data, user.to_dict())
        
        # Standard assertions
        self.assertEqual(user.name, "John Doe")
        self.assertIsNotNone(user.created_at)