- Always verify actual behavior before writing tests - Don't assume how the system should work
- Test what the system actually does, not what you think it should do
- Example: Basic permission queries don't automatically filter expired roles
- Always use established fixtures - Don't invent your own role names
- Use
CustomerRole.SUPPORTnotCustomerRole.MANAGER(which doesn't exist) - Use
fixtures.ProjectFixture()for consistent test setup with proper relationships - Use
factories.UserFactory()for creating test users with proper defaults
- Test for actual exceptions, not ideal ones
- If the system raises
AttributeErrorfor missing attributes, test forAttributeError - Only test for
PermissionDeniedwhen the system actually catches and converts errors
- Use Mock objects effectively for nested permission paths
- Create realistic mock structures:
mock_resource.project.customer = self.customer - Test permission factory with multiple source paths:
["direct_customer", "project.customer"] - Mock objects help test complex scenarios without database overhead
- Understand explicit vs implicit time checking
- Basic
has_permission()doesn't check expiration times automatically - Test boundary conditions: exact expiration time, microseconds past expiration
- Create roles with
timezone.now() ± timedelta()for realistic time testing
Choose the right test base class for each test:
- Default:
test.APITestCase— uses transaction rollback, much faster - Use
test.APITransactionTestCaseonly when:transaction.on_commit()callbacks must fire (e.g., Celery task dispatch)IntegrityErroris deliberately triggered (breaks TestCase's wrapping transaction)- Threading or multi-process database access is needed
responses.start()insetUpfor class-wide HTTP mocking (leaks across TestCase classes)
# GOOD: Default to APITestCase
class MyTest(test.APITestCase):
def test_something(self):
...
# GOOD: Use APITransactionTestCase when on_commit is needed
class OrderProcessingTest(test.APITransactionTestCase):
def test_order_triggers_task(self):
# on_commit callback fires Celery task
...A CI lint job (scripts/analyze_transaction_test_cases.py --ci --baseline N) enforces this — adding new unjustified APITransactionTestCase classes will fail the pipeline. The baseline is lowered as classes are migrated.
- Include query optimization tests where appropriate
- Use
override_settings(DEBUG=True)to count database queries - Test with multiple users/roles to ensure performance doesn't degrade
- Test that system roles work correctly even when modified
- System roles like
CustomerRole.OWNERshould maintain functionality - Test that role modifications don't break core functionality
- Verify that predefined roles have expected permissions
- Test None values, missing attributes, and circular references
- Handle
AttributeErrorwhen accessing missing nested attributes - Test with inactive users, deleted roles, removed permissions
- Verify behavior with complex nested object hierarchies
Preferred: @responses.activate per method — fully isolated, no cleanup needed:
class MyTest(test.APITestCase):
@responses.activate
def test_external_call(self):
responses.add(responses.GET, "https://api.example.com/data", json={"ok": True})
result = my_function()
self.assertEqual(result, {"ok": True})Class-wide mocking with responses.start() — requires APITransactionTestCase:
class ExternalAPITest(test.APITransactionTestCase):
"""responses.start() in setUp leaks state across TestCase classes."""
def setUp(self):
super().setUp()
responses.start()
responses.add(responses.GET, "https://api.example.com/data", json={"ok": True})
def tearDown(self):
responses.stop()
responses.reset()
super().tearDown()Using responses.start() in setUp with APITestCase causes leaked mock state across test classes because TestCase doesn't fully reset process-level state between classes.
When combining APITransactionTestCase with a mixin that extends APITestCase, Python's MRO can silently break TransactionTestCase behavior:
# BAD: MRO puts TestCase._fixture_teardown first
class MyTest(test.APITransactionTestCase, SomeTestMixin):
... # SomeTestMixin extends APITestCase — TransactionTestCase teardown is skipped
# GOOD: Ensure all parents use TransactionTestCase, or use standalone setup
class MyTest(test.APITransactionTestCase):
def setUp(self):
super().setUp()
# Set up mocks directly instead of inheriting from a TestCase mixinWhen writing standalone backend tests that don't inherit from BaseBackendTestCase:
class StandaloneBackendTest(test.APITransactionTestCase):
def setUp(self):
super().setUp()
self.fixture = openstack_fixtures.OpenStackFixture()
# Mock all 5 OpenStack clients
self.mock_admin = mock.patch("waldur_openstack.openstack_base.backend.AdminSession").start()
self.mock_session = mock.patch("waldur_openstack.openstack_base.backend.SessionManager").start()
self.mock_nova = mock.patch("waldur_openstack.openstack_base.backend.NovaClient").start()
self.mock_neutron = mock.patch("waldur_openstack.openstack_base.backend.NeutronClient").start()
self.mock_cinder = mock.patch("waldur_openstack.openstack_base.backend.CinderClient").start()
def tearDown(self):
mock.patch.stopall()
super().tearDown()- Test behavior, not implementation
- One assertion per test when possible
- Clear test names describing scenario
- Use existing test utilities/helpers
- Tests should be deterministic
When fixing performance or accuracy issues:
- Isolate the problem:
- Run individual failing tests to understand specific issues
- Use
pytest -v -sfor verbose output with print statements - Check if multiple tests fail for the same underlying reason
- Understand test expectations:
- Read test comments carefully - they often explain intended behavior
- Check if tests expect specific error types
- Look for conflicting expectations between test suites
- Fix systematically:
- Fix one root cause at a time
- After each fix, run full test suite to check for regressions
- Update related tests for consistency when changing behavior
- API changes require test updates:
- When changing function signatures or default parameters, expect test failures
- Update tests for consistency rather than reverting functional improvements
- Document parameter behavior changes clearly