Python Testing with nose: The Complete Guide
Mastering test discovery, execution, and extensibility with nose
Table of Contents
1. Test Discovery
Basic Discovery Rules
nose automatically discovers tests following these conventions:
Pattern | Description |
---|---|
test_*.py |
Files starting with "test_" |
*_test.py |
Files ending with "_test" |
Test* classes |
Classes starting with "Test" |
test_* functions |
Functions starting with "test" |
project/
├── tests/
│ ├── test_math.py
│ ├── string_utils_test.py
│ └── integration/
│ └── test_api.py
├── tests/
│ ├── test_math.py
│ ├── string_utils_test.py
│ └── integration/
│ └── test_api.py
Advanced Discovery
# Customize discovery with nose.cfg
[nosetests]
match=^(check|validate)_
exclude=^skip_
where=tests/integration
exclude-dir=legacy_tests
nosetests --match="^integration_" |
Run tests matching pattern |
nosetests --exclude="^slow_" |
Exclude matching tests |
nosetests --where=tests/unit |
Search specific directory |
Performance Optimization
# Run tests in parallel (4 processes)
nosetests --processes=4
# Cache discovered tests between runs
nosetests --with-cache
# Limit discovery depth
nosetests --no-recursive
2. Plugins System
Built-in Plugins
Plugin | Command | Purpose |
---|---|---|
xunit | --with-xunit |
JUnit XML output |
cover | --with-coverage |
Code coverage reporting |
prof | --with-profile |
Performance profiling |
doctests | --with-doctest |
Run doctests |
Custom Plugin Development
import nose.plugins.Plugin
class TimingPlugin(nose.plugins.Plugin):
name = 'timing'
def options(self, parser, env):
super().options(parser, env)
parser.add_option('--min-time', type=float, default=1.0)
def configure(self, options, conf):
self.min_time = options.min_time
def beforeTest(self, test):
self._start = time.time()
def afterTest(self, test):
duration = time.time() - self._start
if duration > self.min_time:
print(f"Slow test: {test.id()} ({duration:.2f}s)")
Popular Third-party Plugins
- nose-parameterized: Data-driven testing with decorators
- nose-progressive: Cleaner output with progress bars
- nose-timer: Measure test execution time
- nose-exclude: Exclude specific tests from runs
3. Execution Control
Test Selection
# Run specific test module
nosetests tests/test_math.py
# Run tests matching pattern
nosetests -m "test_addition"
# Run tests with custom attribute
nosetests -a '!slow'
Execution Options
--stop |
Stop after first failure |
--failed |
Run only previously failed tests |
--randomize |
Run tests in random order |
--process-timeout=300 |
Set timeout for parallel tests |
4. Advanced Features
Fixtures
def setup_module():
"""Module-level setup"""
print("Setting up module")
def teardown_module():
"""Module-level teardown"""
print("Tearing down module")
class TestExample:
@classmethod
def setup_class(cls):
"""Class-level setup"""
cls.shared = Resource()
def setup(self):
"""Test-level setup"""
self.value = 42
def test_example(self):
assert self.value == 42
def teardown(self):
"""Test-level teardown"""
del self.value
Test Generators
def test_multiple_cases():
"""Generator creates multiple test cases"""
for input, expected in [
(1+1, 2),
(2*3, 6),
(9-5, 4)
]:
yield check_result, input, expected
def check_result(input, expected):
assert input == expected
Multi-process Testing
# Run tests in parallel (4 processes)
nosetests --processes=4 --process-timeout=60
# With coverage (requires special handling)
nosetests --processes=4 --with-coverage --cover-process-start
Warning: Parallel testing requires tests to be properly isolated. Avoid shared state between tests when using --processes
.
5. Integration & Extensibility
CI/CD Integration
# .travis.yml example for nose
language: python
python:
- "3.8"
- "3.9"
install:
- pip install nose coverage
script:
- nosetests --with-xunit --with-coverage --cover-package=myapp
Debugging Support
# Run with PDB on failure
nosetests --pdb
# Post-mortem debugging
nosetests --pdb-failures
# Drop to debugger on specific test
from nose.tools import set_trace
def test_debugging():
set_trace() # Execution pauses here
assert 1 + 1 == 2
6. Migration & Alternatives
Transitioning to nose2/pytest
Feature | nose | pytest |
---|---|---|
Test discovery | test_*.py |
test_*.py + more |
Fixtures | setup/teardown | Flexible fixture system |
Plugins | Rich ecosystem | Larger ecosystem |
When to Choose nose
- Maintaining legacy test suites
- Dependency on specific nose plugins
- Team familiarity with nose idioms
7. Complete Example
# test_math_operations.py
from nose.tools import assert_equal, assert_raises
def test_addition():
"""Test basic addition"""
assert_equal(1 + 1, 2)
def test_division():
"""Test division with exception"""
with assert_raises(ZeroDivisionError):
1 / 0
class TestMathOperations:
"""Group of math operation tests"""
@classmethod
def setup_class(cls):
cls.precision = 2
def test_multiplication(self):
assert_equal(2 * 3, 6)
def test_floating_point(self):
result = 0.1 + 0.2
assert abs(result - 0.3) < 10**-self.precision
def test_generator():
"""Data-driven test using generator"""
test_data = [
(2, 2, 4),
(3, 3, 9),
(4, 4, 16)
]
for a, b, expected in test_data:
yield check_multiplication, a, b, expected
def check_multiplication(a, b, expected):
assert_equal(a * b, expected)
Pro Tip: Use nosetests --with-doctest
to combine nose tests with doctests in the same codebase for comprehensive testing.