nose2 — Cursor rule: what it does, how to install
Cursor rule setting up nose2 testing standards, pyproject.toml configuration, and coverage reporting for Python projects.
.cursor/rules/nose2.mdcWhat nose2 does
This rule guides Cursor to write Python tests organized around unittest.TestCase and nose2. It enforces placing test modules matching the test_*.py pattern inside a root tests/ directory and replaces bare assert statements with self.assert* diagnostic methods.
Test configuration belongs inside pyproject.toml under the [tool.nose2] table, avoiding legacy unittest.cfg files. It specifies enabling plugins such as nose2.plugins.params for data-driven testing with the @params decorator, alongside junitxml and coverage.
Coverage reporting in multiprocessing CI environments fails unless coverage-combine = true is enabled in pyproject.toml. The coverage plugin also requires installing coverage via pip before execution.
What it helps with
- configure nose2 plugins and paths via pyproject.toml
- replace bare assert statements with unittest assertion methods
- generate combined test coverage reports in multiprocessing CI builds
- set up parameterized test cases using nose2.tools.params
Example prompts for the agent
- Create a parameterized test using nose2.tools.params for the shipping fee function.
- Add a [tool.nose2] configuration block to pyproject.toml with coverage and junitxml plugins.
- Refactor test_auth.py to inherit from unittest.TestCase with setUpClass and assertEqual.
How to install nose2
Manually: put the files in place
.cursor/rules/nose2.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/nose2.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/nose2.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/nose2.mdManually: put the files in place
.clinerules/nose2.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about nose2
How to install nose2 in Cursor?
Save the rule file to .cursor/rules/nose2.mdc in your project root.
What does the nose2 rule do?
It provides conventions for test discovery, pyproject.toml settings, parameterized runs, and unittest.mock usage.
Where should nose2 settings be configured?
All options must go under the [tool.nose2] table in pyproject.toml, avoiding legacy .cfg files.
Source text
nose2.mdc · CC0-1.0
---
description: This rule file provides opinionated best practices for writing, organizing, and configuring tests using nose2, emphasizing modern Python development workflows and common pitfalls.
globs: **/*.py
---
# 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**
```ini
# unittest.cfg
[unittest]
start-dir = tests
```
**✅ GOOD: `pyproject.toml` for all configuration**
```toml
# 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**
```python
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**
```python
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)**
```python
# 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**
```python
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**
```python
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`**
```python
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:**
1. Install `coverage`: `pip install coverage`
2. Configure `pyproject.toml` (as shown in section 2).
3. Run tests with coverage in CI: `nose2 -v`
4. Use `coverage report` or `coverage html` to view results.
5. 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**
```python
# 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`**
```python
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**
```python
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**
```python
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:** `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.
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc