| name | nose2 |
| description | [Applies to: **/*.py] This rule file provides opinionated best practices for writing, organizing, and configuring tests using nose2, emphasizing modern Python development workflows and common pitfalls. |
| source | cursor_mdc |
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]
start-dir = tests
✅ GOOD: pyproject.toml for all configuration
[tool.nose2]
start-dir = "tests"
code-directories = ["src"]
plugins = [
"nose2.plugins.coverage",
"nose2.plugins.params",
"nose2.plugins.junitxml",
]
exclude-plugins = [
"nose2.plugins.loader.functions",
]
[tool.nose2.coverage]
always-on = true
coverage-combine = true
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"
✅ 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")
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)
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):
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", )
user = .db_connection.get(, )
.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 report or coverage html to 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
import external_service
def process_data(data):
return external_service.send(data)
import unittest
import my_module
import external_service
class MyModuleTest(unittest.TestCase):
def test_process_data(self):
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
✅ GOOD: Using unittest.mock.patch
import unittest
from unittest import mock
from my_module import process_data
class MyModuleTest(unittest.TestCase):
@mock.patch('my_module.external_service.send')
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
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:
nose2 is 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: Keep setUp methods focused on preparing the immediate test context. If setup becomes too complex, consider breaking tests into smaller, more focused classes or using factories.
- Mixing
nose and nose2 idioms: nose2 is a distinct project. Avoid relying on nose-specific features that nose2 explicitly does not support (e.g., certain module-level functions or specific test discovery quirks).
- When to consider
pytest: If nose2's unittest-centric approach feels restrictive, or you find yourself fighting its conventions, consider migrating to pytest. pytest offers a more "Pythonic" test syntax (raw assert), powerful fixtures, and a vast plugin ecosystem. pytest can run nose-style tests, making migration easier.