nose2
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
- unittest Compatibility: Full compatibility with Python's standard unittest library
- Rich Plugins: Provides many useful plugins by default
- Parameterized Tests: Supports test parameterization without additional packages
- Configuration Flexibility: Detailed customization through configuration files
- Gradual Migration: Can be gradually introduced to existing unittest-based tests
Cons
- Maintenance Status: Reduced active maintenance as of 2024
- Community Support: Limited community support compared to pytest
- New Project Recommendation: Maintainers recommend pytest for new projects
- Package-Level Fixtures: Like unittest2, package-level fixtures are not supported
Reference Links
- Official Documentation: nose2 Documentation
- PyPI: nose2 Package
- GitHub: nose2-dev/nose2
- Configuration Guide: nose2 Configuration
- Plugin Guide: nose2 Plugins
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)