nose2 Best Practices
nose2 is the modern successor to nose, built upon Python's unittest module. It offers powerful test discovery, a plugin architecture, and helpers for writing clean, maintainable test suites. This guide outlines essential best practices for leveraging nose2 effectively in your projects.
1. Project Layout & Naming
Always organize your tests in a dedicated tests/ directory at the root of your project. Name test files consistently using the test_*.py pattern. This ensures nose2's default discovery works reliably.
❌ BAD: Scattered tests or inconsistent naming
# Project root
my_module/
feature_a.py
feature_a_tests.py # Inconsistent naming
test_feature_b.py # Mixed with source
✅ GOOD: Dedicated tests/ directory and test_*.py naming
# Project root
my_module/
__init__.py
feature_a.py
feature_b.py
tests/
__init__.py
test_feature_a.py
test_feature_b.py
2. Configuration (pyproject.toml)
Centralize all nose2 configuration within your pyproject.toml file under the [tool.nose2] table. This keeps your project configuration tidy, version-controlled, and compatible with modern Python tooling. Avoid unittest.cfg or nose2.cfg for new projects.
❌ BAD: Using legacy .cfg files
# unittest.cfg
[unittest]
start-dir = tests
✅ GOOD: pyproject.toml for all configuration
# pyproject.toml
[tool.nose2]
start-dir = "tests"
code-directories = ["src"] # Add your source code directories to sys.path if needed
plugins = [
"nose2.plugins.coverage",
"nose2.plugins.params",
"nose2.plugins.junitxml",
]
exclude-plugins = [
"nose2.plugins.loader.functions", # Avoid loading plain functions as tests
]
[tool.nose2.coverage]
always-on = true
coverage-combine = true # Essential for multiprocessing
coverage-report = ["term", "html"]
coverage-config = ".coveragerc"
3. Test Structure & Assertions
All tests must inherit from unittest.TestCase. Inside your test methods, use self.assert* methods provided by unittest.TestCase for detailed failure reports. Avoid raw assert statements, as they offer less diagnostic information on failure.
❌ BAD: Raw assert statements
import unittest
class MyFeatureTest(unittest.TestCase):
def test_addition(self):
result = 1 + 1
assert result == 3, "Addition failed" # Vague error message
✅ GOOD: unittest.TestCase and self.assert* methods
import unittest
class MyFeatureTest(unittest.TestCase):
def test_addition(self0):
result = 1 + 1
self.assertEqual(result, 2, "Addition should be 2") # Specific error message
self.assertNotEqual(result, 3)
4. Setup/Teardown
Utilize setUp and tearDown methods within your unittest.TestCase classes for per-test setup and cleanup. For class-level setup/teardown, use setUpClass and tearDownClass. Avoid module-level setup()/teardown() functions unless maintaining legacy nose suites.
❌ BAD: Module-level setup/teardown (legacy nose style)
# test_db_operations.py
DB_CONNECTION = None
def setup_module():
global DB_CONNECTION
DB_CONNECTION = connect_to_db()
def teardown_module():
DB_CONNECTION.close()
class DatabaseTest(unittest.TestCase):
def test_query(self):
# ... use DB_CONNECTION ...
pass
✅ GOOD: Class-level setUp/tearDown for fixtures
import unittest
from my_module.db import connect_to_db, disconnect_from_db
class DatabaseTest(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
"""Connect to the database once for all tests in this class."""
cls.db_connection = connect_to_db()
@classmethod
def tearDownClass(cls) -> None:
"""Disconnect from the database after all tests in this class."""
disconnect_from_db(cls.db_connection)
def setUp(self) -> None:
"""Reset database state before each test."""
self.db_connection.clear_data()
def test_insert_user(self) -> None:
self.db_connection.insert("users", {"name": "Alice"})
user = self.db_connection.get("users", "Alice")
self.assertIsNotNone(user)
def test_delete_user(self) -> None:
self.db_connection.insert("users", {"name": "Bob"})
self.db_connection.delete("users", "Bob")
user = self.db_connection.get("users", "Bob")
self.assertIsNone(user)
5. Parameterized Tests
Leverage nose2.tools.params for writing data-driven tests. This significantly reduces boilerplate when you need to run the same test logic with different input data. Ensure the nose2.plugins.params plugin is enabled in pyproject.toml.
❌ BAD: Duplicating test logic for different inputs
import unittest
class CalculatorTest(unittest.TestCase):
def test_add_positive(self):
self.assertEqual(add(1, 2), 3)
def test_add_negative(self):
self.assertEqual(add(-1, -2), -3)
def test_add_zero(self):
self.assertEqual(add(0, 5), 5)
✅ GOOD: Using nose2.tools.params
import unittest
from nose2.tools import params
class CalculatorTest(unittest.TestCase):
@params(
(1, 2, 3),
(-1, -2, -3),
(0, 5, 5),
(10, -3, 7)
)
def test_add(self, a: int, b: int, expected: int) -> None:
self.assertEqual(add(a, b), expected)
6. Coverage & CI Integration
Enable the coverage plugin and set coverage-combine = true in pyproject.toml to ensure accurate coverage reporting, especially in environments using multiprocessing. Run nose2 -v in your CI pipeline and enforce a coverage threshold to prevent regressions.
Actionable Steps:
- Install
coverage:pip install coverage - Configure
pyproject.toml(as shown in section 2). - Run tests with coverage in CI:
nose2 -v - Use
coverage reportorcoverage htmlto view results. - Enforce a minimum coverage percentage in your CI/CD pipeline.
7. Mocking Strategies
Use Python's built-in unittest.mock library for isolating units under test from their dependencies. Prefer mock.patch as a decorator or context manager for clear and scoped mocking.
❌ BAD: Manually replacing dependencies or global state
# my_module.py
import external_service
def process_data(data):
return external_service.send(data)
# test_my_module.py
import unittest
import my_module
import external_service # This will be the real service
class MyModuleTest(unittest.TestCase):
def test_process_data(self):
# This is bad, it affects the actual external_service module
original_send = external_service.send
external_service.send = lambda x: "mocked_response"
result = my_module.process_data("test_data")
self.assertEqual(result, "mocked_response")
external_service.send = original_send # Must remember to clean up
✅ GOOD: Using unittest.mock.patch
import unittest
from unittest import mock
from my_module import process_data # Assuming process_data is in my_module.py
class MyModuleTest(unittest.TestCase):
@mock.patch('my_module.external_service.send') # Patch the dependency where it's looked up
def test_process_data_decorator(self, mock_send):
mock_send.return_value = "mocked_response_from_decorator"
result = process_data("test_data")
mock_send.assert_called_once_with("test_data")
self.assertEqual(result, "mocked_response_from_decorator")
def test_process_data_context_manager(self):
with mock.patch('my_module.external_service.send') as mock_send:
mock_send.return_value = "mocked_response_from_context"
result = process_data("another_data")
mock_send.assert_called_once_with("another_data")
self.assertEqual(result, "mocked_response_from_context")
8. Type Hints
Apply type hints consistently to your test code, especially for method signatures (setUp, test_*, tearDown). This improves readability, enables static analysis, and makes your tests easier to understand and maintain.
❌ BAD: Untyped test methods
class MyTest(unittest.TestCase):
def setUp(self):
self.client = create_test_client()
def test_endpoint(self):
response = self.client.get('/api/data')
self.assertEqual(response.status_code, 200)
✅ GOOD: Type-hinted test methods
from typing import Any
import unittest
class MyTest(unittest.TestCase):
client: Any # Define attributes used across methods
def setUp(self) -> None:
self.client = create_test_client()
def test_endpoint(self) -> None:
response = self.client.get('/api/data')
self.assertEqual(response.status_code, 200)
9. Common Pitfalls & Gotchas
- Not staying current:
nose2is actively maintained. Always use the latest stable release (0.15.x as of May 2024) to benefit from bug fixes, new features, and Python version compatibility. - Overly complex
setUp: KeepsetUpmethods focused on preparing the immediate test context. If setup becomes too complex, consider breaking tests into smaller, more focused classes or using factories. - Mixing
noseandnose2idioms:nose2is a distinct project. Avoid relying onnose-specific features thatnose2explicitly does not support (e.g., certain module-level functions or specific test discovery quirks). - When to consider
pytest: Ifnose2'sunittest-centric approach feels restrictive, or you find yourself fighting its conventions, consider migrating topytest.pytestoffers a more "Pythonic" test syntax (rawassert), powerful fixtures, and a vast plugin ecosystem.pytestcan runnose-style tests, making migration easier.