Checkers Test Suite Restructure Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Restructure the monolithic apps/checkers/tests.py (474 lines) into a proper test directory structure mirroring the module layout.
Architecture: Split tests by component into separate files within a tests/ directory. Each checker gets its own test file under tests/checkers/. Base classes, models, Django checks, and commands get dedicated test files.
Tech Stack: Django TestCase, pytest, unittest.mock, psutil mocking
Current State
Problem: All tests in single apps/checkers/tests.py file (474 lines)
Current Test Classes:
CheckResultTests- CheckResult dataclass testsBaseCheckerTests- threshold logicCPUCheckerTests- with psutil mockingMemoryCheckerTests- with psutil mockingDiskCheckerTests- with psutil mocking, multi-pathNetworkCheckerTests- with subprocess mockingProcessCheckerTests- with psutil mockingCheckerRegistryTests- registry validationSystemChecksTests- Django system checksCheckerEnablementTests- skip/disable settings
Target Structure
apps/checkers/
├── tests/
│ ├── __init__.py
│ ├── test_base.py # CheckResult, CheckStatus, BaseChecker
│ ├── checkers/
│ │ ├── __init__.py
│ │ ├── test_cpu.py # CPUChecker tests
│ │ ├── test_memory.py # MemoryChecker tests
│ │ ├── test_disk.py # DiskChecker tests
│ │ ├── test_network.py # NetworkChecker tests
│ │ └── test_process.py # ProcessChecker tests
│ ├── test_registry.py # Registry and enablement tests
│ ├── test_checks.py # Django system checks tests
│ └── test_models.py # CheckRun model tests (new)
└── tests.py # DELETE after migration
Implementation Tasks
Task 1: Create test directory structure
Files:
- Create:
apps/checkers/tests/__init__.py - Create:
apps/checkers/tests/checkers/__init__.py
Step 1: Create tests directory
mkdir -p apps/checkers/tests/checkers
Step 2: Create __init__.py files
# apps/checkers/tests/__init__.py
"""Checkers app test suite."""
# apps/checkers/tests/checkers/__init__.py
"""Checker implementation tests."""
Step 3: Verify structure
Run: ls -la apps/checkers/tests/ Expected: Shows __init__.py and checkers/ directory
Step 4: Commit
git add apps/checkers/tests/
git commit -m "chore(checkers): create test directory structure"
Task 2: Create test_base.py with base class tests
Files:
- Create:
apps/checkers/tests/test_base.py - Read:
apps/checkers/tests.py:1-100(for extraction)
Step 1: Read source tests
Read lines 1-100 of apps/checkers/tests.py to extract CheckResultTests and BaseCheckerTests.
Step 2: Write test_base.py
"""Tests for base checker classes and data structures."""
from django.test import TestCase
from apps.checkers.checkers.base import BaseChecker, CheckResult, CheckStatus
class CheckResultTests(TestCase):
"""Tests for the CheckResult dataclass."""
def test_check_result_creation(self):
"""CheckResult can be created with required fields."""
result = CheckResult(
status=CheckStatus.OK,
message="All good",
checker_name="test",
)
self.assertEqual(result.status, CheckStatus.OK)
self.assertEqual(result.message, "All good")
self.assertEqual(result.checker_name, "test")
self.assertIsNone(result.metrics)
self.assertIsNone(result.error)
def test_check_result_with_metrics(self):
"""CheckResult can include metrics dictionary."""
metrics = {"cpu_percent": 45.2, "cpu_count": 4}
result = CheckResult(
status=CheckStatus.WARNING,
message="CPU high",
checker_name="cpu",
metrics=metrics,
)
self.assertEqual(result.metrics, metrics)
def test_check_result_with_error(self):
"""CheckResult can include error string."""
result = CheckResult(
status=CheckStatus.UNKNOWN,
message="Check failed",
checker_name="test",
error="Connection timeout",
)
self.assertEqual(result.error, "Connection timeout")
class BaseCheckerTests(TestCase):
"""Tests for BaseChecker threshold logic."""
def test_determine_status_ok(self):
"""Values below warning threshold return OK."""
class TestChecker(BaseChecker):
name = "test"
warning_threshold = 70.0
critical_threshold = 90.0
def check(self):
return self._make_result(CheckStatus.OK, "ok")
checker = TestChecker()
self.assertEqual(checker._determine_status(50.0), CheckStatus.OK)
self.assertEqual(checker._determine_status(69.9), CheckStatus.OK)
def test_determine_status_warning(self):
"""Values at or above warning but below critical return WARNING."""
class TestChecker(BaseChecker):
name = "test"
warning_threshold = 70.0
critical_threshold = 90.0
def check(self):
return self._make_result(CheckStatus.OK, "ok")
checker = TestChecker()
self.assertEqual(checker._determine_status(70.0), CheckStatus.WARNING)
self.assertEqual(checker._determine_status(89.9), CheckStatus.WARNING)
def test_determine_status_critical(self):
"""Values at or above critical threshold return CRITICAL."""
class TestChecker(BaseChecker):
name = "test"
warning_threshold = 70.0
critical_threshold = 90.0
def check(self):
return self._make_result(CheckStatus.OK, "ok")
checker = TestChecker()
self.assertEqual(checker._determine_status(90.0), CheckStatus.CRITICAL)
self.assertEqual(checker._determine_status(100.0), CheckStatus.CRITICAL)
def test_threshold_override(self):
"""Thresholds can be overridden in constructor."""
class TestChecker(BaseChecker):
name = "test"
warning_threshold = 70.0
critical_threshold = 90.0
def check(self):
return self._make_result(CheckStatus.OK, "ok")
checker = TestChecker(warning_threshold=50.0, critical_threshold=80.0)
self.assertEqual(checker.warning_threshold, 50.0)
self.assertEqual(checker.critical_threshold, 80.0)
# 60% is now WARNING with new thresholds
self.assertEqual(checker._determine_status(60.0), CheckStatus.WARNING)
def test_make_result(self):
"""_make_result creates CheckResult with checker name."""
class TestChecker(BaseChecker):
name = "mytest"
warning_threshold = 70.0
critical_threshold = 90.0
def check(self):
return self._make_result(
CheckStatus.OK, "test message", {"metric": 1}
)
checker = TestChecker()
result = checker.check()
self.assertEqual(result.checker_name, "mytest")
self.assertEqual(result.message, "test message")
self.assertEqual(result.metrics, {"metric": 1})
def test_error_result(self):
"""_error_result creates UNKNOWN status with error."""
class TestChecker(BaseChecker):
name = "test"
def check(self):
return self._error_result("Something broke")
checker = TestChecker()
result = checker.check()
self.assertEqual(result.status, CheckStatus.UNKNOWN)
self.assertEqual(result.error, "Something broke")
Step 3: Run test to verify it works
Run: uv run pytest apps/checkers/tests/test_base.py -v Expected: All tests PASS
Step 4: Commit
git add apps/checkers/tests/test_base.py
git commit -m "test(checkers): add test_base.py for base classes"
Task 3: Create test_cpu.py with CPU checker tests
Files:
- Create:
apps/checkers/tests/checkers/test_cpu.py - Read:
apps/checkers/tests.py(for CPUCheckerTests extraction)
Step 1: Write test_cpu.py
"""Tests for the CPU checker."""
from unittest.mock import patch
from django.test import TestCase
from apps.checkers.checkers.base import CheckStatus
from apps.checkers.checkers.cpu import CPUChecker
class CPUCheckerTests(TestCase):
"""Tests for CPUChecker implementation."""
@patch("apps.checkers.checkers.cpu.psutil")
def test_cpu_check_ok(self, mock_psutil):
"""CPU below warning threshold returns OK."""
mock_psutil.cpu_percent.return_value = 25.0
mock_psutil.cpu_count.return_value = 4
checker = CPUChecker()
result = checker.check()
self.assertEqual(result.status, CheckStatus.OK)
self.assertIn("25.0%", result.message)
self.assertEqual(result.metrics["cpu_percent"], 25.0)
self.assertEqual(result.metrics["cpu_count"], 4)
@patch("apps.checkers.checkers.cpu.psutil")
def test_cpu_check_warning(self, mock_psutil):
"""CPU at warning threshold returns WARNING."""
mock_psutil.cpu_percent.return_value = 75.0
mock_psutil.cpu_count.return_value = 4
checker = CPUChecker()
result = checker.check()
self.assertEqual(result.status, CheckStatus.WARNING)
@patch("apps.checkers.checkers.cpu.psutil")
def test_cpu_check_critical(self, mock_psutil):
"""CPU at critical threshold returns CRITICAL."""
mock_psutil.cpu_percent.return_value = 95.0
mock_psutil.cpu_count.return_value = 4
checker = CPUChecker()
result = checker.check()
self.assertEqual(result.status, CheckStatus.CRITICAL)
@patch("apps.checkers.checkers.cpu.psutil")
def test_cpu_check_per_cpu(self, mock_psutil):
"""Per-CPU metrics included when requested."""
mock_psutil.cpu_percent.side_effect = [
50.0, # Overall
[40.0, 60.0, 45.0, 55.0], # Per-CPU
]
mock_psutil.cpu_count.return_value = 4
checker = CPUChecker(per_cpu=True)
result = checker.check()
self.assertIn("per_cpu", result.metrics)
self.assertEqual(len(result.metrics["per_cpu"]), 4)
@patch("apps.checkers.checkers.cpu.psutil")
def test_cpu_check_custom_thresholds(self, mock_psutil):
"""Custom thresholds override defaults."""
mock_psutil.cpu_percent.return_value = 55.0
mock_psutil.cpu_count.return_value = 4
checker = CPUChecker(warning_threshold=50.0, critical_threshold=80.0)
result = checker.check()
# 55% is WARNING with 50/80 thresholds
self.assertEqual(result.status, CheckStatus.WARNING)
@patch("apps.checkers.checkers.cpu.psutil")
def test_cpu_check_error_handling(self, mock_psutil):
"""Errors during check return UNKNOWN status."""
mock_psutil.cpu_percent.side_effect = Exception("psutil error")
checker = CPUChecker()
result = checker.check()
self.assertEqual(result.status, CheckStatus.UNKNOWN)
self.assertIn("psutil error", result.error)
Step 2: Run test to verify it works
Run: uv run pytest apps/checkers/tests/checkers/test_cpu.py -v Expected: All tests PASS
Step 3: Commit
git add apps/checkers/tests/checkers/test_cpu.py
git commit -m "test(checkers): add test_cpu.py for CPU checker"
Task 4: Create test_memory.py with memory checker tests
Files:
- Create:
apps/checkers/tests/checkers/test_memory.py
Step 1: Write test_memory.py
"""Tests for the memory checker."""
from unittest.mock import MagicMock, patch
from django.test import TestCase
from apps.checkers.checkers.base import CheckStatus
from apps.checkers.checkers.memory import MemoryChecker
class MemoryCheckerTests(TestCase):
"""Tests for MemoryChecker implementation."""
@patch("apps.checkers.checkers.memory.psutil")
def test_memory_check_ok(self, mock_psutil):
"""Memory below warning threshold returns OK."""
mock_memory = MagicMock()
mock_memory.percent = 45.0
mock_memory.total = 16 * 1024**3 # 16 GB
mock_memory.available = 8 * 1024**3 # 8 GB
mock_memory.used = 8 * 1024**3
mock_psutil.virtual_memory.return_value = mock_memory
checker = MemoryChecker()
result = checker.check()
self.assertEqual(result.status, CheckStatus.OK)
self.assertIn("45.0%", result.message)
self.assertEqual(result.metrics["memory_percent"], 45.0)
@patch("apps.checkers.checkers.memory.psutil")
def test_memory_check_warning(self, mock_psutil):
"""Memory at warning threshold returns WARNING."""
mock_memory = MagicMock()
mock_memory.percent = 75.0
mock_memory.total = 16 * 1024**3
mock_memory.available = 4 * 1024**3
mock_memory.used = 12 * 1024**3
mock_psutil.virtual_memory.return_value = mock_memory
checker = MemoryChecker()
result = checker.check()
self.assertEqual(result.status, CheckStatus.WARNING)
@patch("apps.checkers.checkers.memory.psutil")
def test_memory_check_critical(self, mock_psutil):
"""Memory at critical threshold returns CRITICAL."""
mock_memory = MagicMock()
mock_memory.percent = 95.0
mock_memory.total = 16 * 1024**3
mock_memory.available = 1 * 1024**3
mock_memory.used = 15 * 1024**3
mock_psutil.virtual_memory.return_value = mock_memory
checker = MemoryChecker()
result = checker.check()
self.assertEqual(result.status, CheckStatus.CRITICAL)
@patch("apps.checkers.checkers.memory.psutil")
def test_memory_check_with_swap(self, mock_psutil):
"""Swap memory included when requested."""
mock_memory = MagicMock()
mock_memory.percent = 50.0
mock_memory.total = 16 * 1024**3
mock_memory.available = 8 * 1024**3
mock_memory.used = 8 * 1024**3
mock_psutil.virtual_memory.return_value = mock_memory
mock_swap = MagicMock()
mock_swap.percent = 10.0
mock_swap.total = 4 * 1024**3
mock_swap.used = 400 * 1024**2
mock_psutil.swap_memory.return_value = mock_swap
checker = MemoryChecker(include_swap=True)
result = checker.check()
self.assertIn("swap_percent", result.metrics)
self.assertEqual(result.metrics["swap_percent"], 10.0)
@patch("apps.checkers.checkers.memory.psutil")
def test_memory_check_error_handling(self, mock_psutil):
"""Errors during check return UNKNOWN status."""
mock_psutil.virtual_memory.side_effect = Exception("psutil error")
checker = MemoryChecker()
result = checker.check()
self.assertEqual(result.status, CheckStatus.UNKNOWN)
self.assertIn("psutil error", result.error)
Step 2: Run test to verify it works
Run: uv run pytest apps/checkers/tests/checkers/test_memory.py -v Expected: All tests PASS
Step 3: Commit
git add apps/checkers/tests/checkers/test_memory.py
git commit -m "test(checkers): add test_memory.py for memory checker"
Task 5: Create test_disk.py with disk checker tests
Files:
- Create:
apps/checkers/tests/checkers/test_disk.py
Step 1: Write test_disk.py
"""Tests for the disk checker."""
from unittest.mock import MagicMock, patch
from django.test import TestCase
from apps.checkers.checkers.base import CheckStatus
from apps.checkers.checkers.disk import DiskChecker
class DiskCheckerTests(TestCase):
"""Tests for DiskChecker implementation."""
@patch("apps.checkers.checkers.disk.psutil")
def test_disk_check_ok(self, mock_psutil):
"""Disk below warning threshold returns OK."""
mock_usage = MagicMock()
mock_usage.percent = 50.0
mock_usage.total = 500 * 1024**3 # 500 GB
mock_usage.used = 250 * 1024**3
mock_usage.free = 250 * 1024**3
mock_psutil.disk_usage.return_value = mock_usage
checker = DiskChecker(paths=["/"])
result = checker.check()
self.assertEqual(result.status, CheckStatus.OK)
self.assertIn("50.0%", result.message)
@patch("apps.checkers.checkers.disk.psutil")
def test_disk_check_warning(self, mock_psutil):
"""Disk at warning threshold returns WARNING."""
mock_usage = MagicMock()
mock_usage.percent = 75.0
mock_usage.total = 500 * 1024**3
mock_usage.used = 375 * 1024**3
mock_usage.free = 125 * 1024**3
mock_psutil.disk_usage.return_value = mock_usage
checker = DiskChecker(paths=["/"])
result = checker.check()
self.assertEqual(result.status, CheckStatus.WARNING)
@patch("apps.checkers.checkers.disk.psutil")
def test_disk_check_critical(self, mock_psutil):
"""Disk at critical threshold returns CRITICAL."""
mock_usage = MagicMock()
mock_usage.percent = 95.0
mock_usage.total = 500 * 1024**3
mock_usage.used = 475 * 1024**3
mock_usage.free = 25 * 1024**3
mock_psutil.disk_usage.return_value = mock_usage
checker = DiskChecker(paths=["/"])
result = checker.check()
self.assertEqual(result.status, CheckStatus.CRITICAL)
@patch("apps.checkers.checkers.disk.psutil")
def test_disk_check_multiple_paths(self, mock_psutil):
"""Multiple paths checked, worst status returned."""
mock_root = MagicMock()
mock_root.percent = 50.0
mock_root.total = 500 * 1024**3
mock_root.used = 250 * 1024**3
mock_root.free = 250 * 1024**3
mock_data = MagicMock()
mock_data.percent = 85.0 # WARNING level
mock_data.total = 1000 * 1024**3
mock_data.used = 850 * 1024**3
mock_data.free = 150 * 1024**3
mock_psutil.disk_usage.side_effect = [mock_root, mock_data]
checker = DiskChecker(paths=["/", "/data"])
result = checker.check()
# Should return WARNING because /data is at 85%
self.assertEqual(result.status, CheckStatus.WARNING)
self.assertIn("/data", result.message)
@patch("apps.checkers.checkers.disk.psutil")
def test_disk_check_default_path(self, mock_psutil):
"""Default path is / when none specified."""
mock_usage = MagicMock()
mock_usage.percent = 50.0
mock_usage.total = 500 * 1024**3
mock_usage.used = 250 * 1024**3
mock_usage.free = 250 * 1024**3
mock_psutil.disk_usage.return_value = mock_usage
checker = DiskChecker()
result = checker.check()
mock_psutil.disk_usage.assert_called_with("/")
@patch("apps.checkers.checkers.disk.psutil")
def test_disk_check_error_handling(self, mock_psutil):
"""Errors during check return UNKNOWN status."""
mock_psutil.disk_usage.side_effect = Exception("No such path")
checker = DiskChecker(paths=["/nonexistent"])
result = checker.check()
self.assertEqual(result.status, CheckStatus.UNKNOWN)
self.assertIn("No such path", result.error)
Step 2: Run test to verify it works
Run: uv run pytest apps/checkers/tests/checkers/test_disk.py -v Expected: All tests PASS
Step 3: Commit
git add apps/checkers/tests/checkers/test_disk.py
git commit -m "test(checkers): add test_disk.py for disk checker"
Task 6: Create test_network.py with network checker tests
Files:
- Create:
apps/checkers/tests/checkers/test_network.py
Step 1: Write test_network.py
"""Tests for the network checker."""
from unittest.mock import MagicMock, patch
from django.test import TestCase
from apps.checkers.checkers.base import CheckStatus
from apps.checkers.checkers.network import NetworkChecker
class NetworkCheckerTests(TestCase):
"""Tests for NetworkChecker implementation."""
@patch("apps.checkers.checkers.network.subprocess")
def test_network_check_ok(self, mock_subprocess):
"""Successful ping returns OK."""
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=15.2 ms"
mock_subprocess.run.return_value = mock_result
checker = NetworkChecker(hosts=["8.8.8.8"])
result = checker.check()
self.assertEqual(result.status, CheckStatus.OK)
self.assertIn("reachable", result.message.lower())
@patch("apps.checkers.checkers.network.subprocess")
def test_network_check_host_unreachable(self, mock_subprocess):
"""Unreachable host returns CRITICAL."""
mock_result = MagicMock()
mock_result.returncode = 1
mock_result.stdout = ""
mock_result.stderr = "Request timeout"
mock_subprocess.run.return_value = mock_result
checker = NetworkChecker(hosts=["192.0.2.1"]) # TEST-NET, unreachable
result = checker.check()
self.assertEqual(result.status, CheckStatus.CRITICAL)
@patch("apps.checkers.checkers.network.subprocess")
def test_network_check_multiple_hosts(self, mock_subprocess):
"""Multiple hosts checked, any failure is CRITICAL."""
mock_success = MagicMock()
mock_success.returncode = 0
mock_success.stdout = "time=10.0 ms"
mock_failure = MagicMock()
mock_failure.returncode = 1
mock_failure.stdout = ""
mock_failure.stderr = "Request timeout"
mock_subprocess.run.side_effect = [mock_success, mock_failure]
checker = NetworkChecker(hosts=["8.8.8.8", "192.0.2.1"])
result = checker.check()
# One host failed, should be CRITICAL
self.assertEqual(result.status, CheckStatus.CRITICAL)
@patch("apps.checkers.checkers.network.subprocess")
def test_network_check_latency_parsing(self, mock_subprocess):
"""Latency extracted from ping output."""
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=25.5 ms"
mock_subprocess.run.return_value = mock_result
checker = NetworkChecker(hosts=["8.8.8.8"])
result = checker.check()
self.assertIn("latency_ms", result.metrics)
# Latency should be parsed from output
@patch("apps.checkers.checkers.network.subprocess")
def test_network_check_default_hosts(self, mock_subprocess):
"""Default hosts used when none specified."""
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "time=10.0 ms"
mock_subprocess.run.return_value = mock_result
checker = NetworkChecker()
result = checker.check()
# Should have checked default hosts (8.8.8.8, 1.1.1.1)
self.assertTrue(mock_subprocess.run.called)
@patch("apps.checkers.checkers.network.subprocess")
def test_network_check_error_handling(self, mock_subprocess):
"""Subprocess errors return UNKNOWN status."""
mock_subprocess.run.side_effect = Exception("Command failed")
checker = NetworkChecker(hosts=["8.8.8.8"])
result = checker.check()
self.assertEqual(result.status, CheckStatus.UNKNOWN)
self.assertIn("Command failed", result.error)
Step 2: Run test to verify it works
Run: uv run pytest apps/checkers/tests/checkers/test_network.py -v Expected: All tests PASS
Step 3: Commit
git add apps/checkers/tests/checkers/test_network.py
git commit -m "test(checkers): add test_network.py for network checker"
Task 7: Create test_process.py with process checker tests
Files:
- Create:
apps/checkers/tests/checkers/test_process.py
Step 1: Write test_process.py
"""Tests for the process checker."""
from unittest.mock import MagicMock, patch
from django.test import TestCase
from apps.checkers.checkers.base import CheckStatus
from apps.checkers.checkers.process import ProcessChecker
class ProcessCheckerTests(TestCase):
"""Tests for ProcessChecker implementation."""
@patch("apps.checkers.checkers.process.psutil")
def test_process_check_ok(self, mock_psutil):
"""Process running returns OK."""
mock_proc = MagicMock()
mock_proc.info = {"name": "nginx", "pid": 1234, "status": "running"}
mock_psutil.process_iter.return_value = [mock_proc]
checker = ProcessChecker(processes=["nginx"])
result = checker.check()
self.assertEqual(result.status, CheckStatus.OK)
self.assertIn("nginx", result.message)
@patch("apps.checkers.checkers.process.psutil")
def test_process_check_not_running(self, mock_psutil):
"""Process not running returns CRITICAL."""
mock_psutil.process_iter.return_value = []
checker = ProcessChecker(processes=["nginx"])
result = checker.check()
self.assertEqual(result.status, CheckStatus.CRITICAL)
self.assertIn("nginx", result.message)
@patch("apps.checkers.checkers.process.psutil")
def test_process_check_multiple_processes(self, mock_psutil):
"""Multiple processes, any missing is CRITICAL."""
mock_nginx = MagicMock()
mock_nginx.info = {"name": "nginx", "pid": 1234, "status": "running"}
# postgres not in list
mock_psutil.process_iter.return_value = [mock_nginx]
checker = ProcessChecker(processes=["nginx", "postgres"])
result = checker.check()
self.assertEqual(result.status, CheckStatus.CRITICAL)
self.assertIn("postgres", result.message)
@patch("apps.checkers.checkers.process.psutil")
def test_process_check_all_running(self, mock_psutil):
"""All processes running returns OK."""
mock_nginx = MagicMock()
mock_nginx.info = {"name": "nginx", "pid": 1234, "status": "running"}
mock_postgres = MagicMock()
mock_postgres.info = {"name": "postgres", "pid": 5678, "status": "running"}
mock_psutil.process_iter.return_value = [mock_nginx, mock_postgres]
checker = ProcessChecker(processes=["nginx", "postgres"])
result = checker.check()
self.assertEqual(result.status, CheckStatus.OK)
@patch("apps.checkers.checkers.process.psutil")
def test_process_check_no_processes_specified(self, mock_psutil):
"""No processes specified returns OK (nothing to check)."""
checker = ProcessChecker(processes=[])
result = checker.check()
# Empty process list means nothing to check, should be OK or UNKNOWN
self.assertIn(result.status, [CheckStatus.OK, CheckStatus.UNKNOWN])
@patch("apps.checkers.checkers.process.psutil")
def test_process_check_error_handling(self, mock_psutil):
"""Errors during check return UNKNOWN status."""
mock_psutil.process_iter.side_effect = Exception("Access denied")
checker = ProcessChecker(processes=["nginx"])
result = checker.check()
self.assertEqual(result.status, CheckStatus.UNKNOWN)
self.assertIn("Access denied", result.error)
Step 2: Run test to verify it works
Run: uv run pytest apps/checkers/tests/checkers/test_process.py -v Expected: All tests PASS
Step 3: Commit
git add apps/checkers/tests/checkers/test_process.py
git commit -m "test(checkers): add test_process.py for process checker"
Task 8: Create test_registry.py with registry and enablement tests
Files:
- Create:
apps/checkers/tests/test_registry.py
Step 1: Write test_registry.py
"""Tests for the checker registry and enablement system."""
from django.test import TestCase, override_settings
from apps.checkers.checkers import (
CHECKER_REGISTRY,
get_enabled_checkers,
is_checker_enabled,
)
from apps.checkers.checkers.base import BaseChecker
from apps.checkers.checkers.cpu import CPUChecker
from apps.checkers.checkers.disk import DiskChecker
from apps.checkers.checkers.memory import MemoryChecker
from apps.checkers.checkers.network import NetworkChecker
from apps.checkers.checkers.process import ProcessChecker
class CheckerRegistryTests(TestCase):
"""Tests for the CHECKER_REGISTRY."""
def test_all_checkers_registered(self):
"""All expected checkers are in registry."""
expected = {"cpu", "memory", "disk", "network", "process"}
self.assertEqual(set(CHECKER_REGISTRY.keys()), expected)
def test_registry_values_are_checker_classes(self):
"""All registry values are BaseChecker subclasses."""
for name, checker_class in CHECKER_REGISTRY.items():
self.assertTrue(
issubclass(checker_class, BaseChecker),
f"{name} is not a BaseChecker subclass",
)
def test_checker_classes_match_expected(self):
"""Registry maps to correct checker classes."""
self.assertIs(CHECKER_REGISTRY["cpu"], CPUChecker)
self.assertIs(CHECKER_REGISTRY["memory"], MemoryChecker)
self.assertIs(CHECKER_REGISTRY["disk"], DiskChecker)
self.assertIs(CHECKER_REGISTRY["network"], NetworkChecker)
self.assertIs(CHECKER_REGISTRY["process"], ProcessChecker)
def test_checker_names_match_keys(self):
"""Checker class names match their registry keys."""
for name, checker_class in CHECKER_REGISTRY.items():
self.assertEqual(
checker_class.name,
name,
f"Checker {checker_class.__name__} has name '{checker_class.name}' but is registered as '{name}'",
)
class CheckerEnablementTests(TestCase):
"""Tests for checker enable/disable functionality."""
def test_is_checker_enabled_default(self):
"""Checkers enabled by default."""
self.assertTrue(is_checker_enabled("cpu"))
self.assertTrue(is_checker_enabled("memory"))
@override_settings(CHECKERS_SKIP_ALL=True)
def test_skip_all_disables_all(self):
"""CHECKERS_SKIP_ALL disables all checkers."""
self.assertFalse(is_checker_enabled("cpu"))
self.assertFalse(is_checker_enabled("memory"))
@override_settings(CHECKERS_SKIP=["cpu", "disk"])
def test_skip_list_disables_specific(self):
"""CHECKERS_SKIP disables specific checkers."""
self.assertFalse(is_checker_enabled("cpu"))
self.assertFalse(is_checker_enabled("disk"))
self.assertTrue(is_checker_enabled("memory"))
self.assertTrue(is_checker_enabled("network"))
def test_get_enabled_checkers_default(self):
"""All checkers returned when none disabled."""
enabled = get_enabled_checkers()
self.assertEqual(set(enabled.keys()), set(CHECKER_REGISTRY.keys()))
@override_settings(CHECKERS_SKIP=["cpu"])
def test_get_enabled_checkers_with_skip(self):
"""Disabled checkers excluded from get_enabled_checkers."""
enabled = get_enabled_checkers()
self.assertNotIn("cpu", enabled)
self.assertIn("memory", enabled)
self.assertIn("disk", enabled)
@override_settings(CHECKERS_SKIP_ALL=True)
def test_get_enabled_checkers_all_skipped(self):
"""Empty dict when all checkers disabled."""
enabled = get_enabled_checkers()
self.assertEqual(enabled, {})
def test_is_checker_enabled_unknown_checker(self):
"""Unknown checker names return False or raise."""
# Depending on implementation, might return False or raise
result = is_checker_enabled("nonexistent")
self.assertFalse(result)
Step 2: Run test to verify it works
Run: uv run pytest apps/checkers/tests/test_registry.py -v Expected: All tests PASS
Step 3: Commit
git add apps/checkers/tests/test_registry.py
git commit -m "test(checkers): add test_registry.py for registry and enablement"
Task 9: Create test_checks.py with Django system checks tests
Files:
- Create:
apps/checkers/tests/test_checks.py
Step 1: Write test_checks.py
"""Tests for Django system checks in the checkers app."""
from unittest.mock import MagicMock, patch
from django.core.checks import Error, Warning
from django.test import TestCase, override_settings
from apps.checkers.checks import (
check_crontab_configuration,
check_database_connection,
check_database_tables_exist,
check_pending_migrations,
)
class DatabaseConnectionCheckTests(TestCase):
"""Tests for database connection system check."""
def test_database_connection_ok(self):
"""No errors when database is connected."""
# Default test database should be connected
errors = check_database_connection(None)
self.assertEqual(errors, [])
@patch("apps.checkers.checks.connection")
def test_database_connection_failed(self, mock_connection):
"""Error returned when database connection fails."""
mock_connection.ensure_connection.side_effect = Exception("Connection refused")
errors = check_database_connection(None)
self.assertEqual(len(errors), 1)
self.assertIsInstance(errors[0], Error)
self.assertIn("database", errors[0].msg.lower())
class PendingMigrationsCheckTests(TestCase):
"""Tests for pending migrations system check."""
def test_no_pending_migrations(self):
"""No warnings when migrations are applied."""
# Test database should have all migrations
warnings = check_pending_migrations(None)
# May or may not have warnings depending on test setup
for w in warnings:
self.assertIsInstance(w, Warning)
@patch("apps.checkers.checks.MigrationExecutor")
def test_pending_migrations_detected(self, mock_executor_class):
"""Warning returned when migrations pending."""
mock_executor = MagicMock()
mock_executor.migration_plan.return_value = [
(MagicMock(), False) # One pending migration
]
mock_executor_class.return_value = mock_executor
warnings = check_pending_migrations(None)
self.assertEqual(len(warnings), 1)
self.assertIsInstance(warnings[0], Warning)
self.assertIn("migration", warnings[0].msg.lower())
class CrontabConfigurationCheckTests(TestCase):
"""Tests for crontab configuration system check."""
@override_settings(CHECKERS_CRONTAB_ENABLED=True)
@patch("apps.checkers.checks.os.path.exists")
def test_crontab_configured(self, mock_exists):
"""No errors when crontab is properly configured."""
mock_exists.return_value = True
errors = check_crontab_configuration(None)
# Should pass or return warnings (not errors)
for e in errors:
self.assertNotIsInstance(e, Error)
@override_settings(CHECKERS_CRONTAB_ENABLED=False)
def test_crontab_disabled(self):
"""No checks when crontab is disabled."""
errors = check_crontab_configuration(None)
self.assertEqual(errors, [])
class DatabaseTablesExistCheckTests(TestCase):
"""Tests for database tables existence check."""
def test_tables_exist(self):
"""No errors when required tables exist."""
# Test database should have tables
errors = check_database_tables_exist(None)
self.assertEqual(errors, [])
@patch("apps.checkers.checks.connection")
def test_tables_missing(self, mock_connection):
"""Error returned when tables missing."""
mock_cursor = MagicMock()
mock_cursor.fetchone.return_value = None
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
errors = check_database_tables_exist(None)
# Should detect missing tables
# Implementation dependent
Step 2: Run test to verify it works
Run: uv run pytest apps/checkers/tests/test_checks.py -v Expected: All tests PASS (may need adjustment based on actual implementation)
Step 3: Commit
git add apps/checkers/tests/test_checks.py
git commit -m "test(checkers): add test_checks.py for Django system checks"
Task 10: Create test_models.py with CheckRun model tests
Files:
- Create:
apps/checkers/tests/test_models.py
Step 1: Write test_models.py
"""Tests for the CheckRun model."""
from django.test import TestCase
from django.utils import timezone
from apps.checkers.checkers.base import CheckStatus
from apps.checkers.models import CheckRun
class CheckRunModelTests(TestCase):
"""Tests for CheckRun ORM model."""
def test_create_check_run(self):
"""CheckRun can be created with required fields."""
check_run = CheckRun.objects.create(
checker_name="cpu",
status=CheckStatus.OK.value,
message="CPU usage at 25%",
)
self.assertIsNotNone(check_run.pk)
self.assertEqual(check_run.checker_name, "cpu")
self.assertEqual(check_run.status, CheckStatus.OK.value)
def test_check_run_with_metrics(self):
"""CheckRun can store metrics JSON."""
metrics = {"cpu_percent": 45.2, "cpu_count": 4}
check_run = CheckRun.objects.create(
checker_name="cpu",
status=CheckStatus.OK.value,
message="CPU check passed",
metrics=metrics,
)
check_run.refresh_from_db()
self.assertEqual(check_run.metrics, metrics)
def test_check_run_with_error(self):
"""CheckRun can store error message."""
check_run = CheckRun.objects.create(
checker_name="cpu",
status=CheckStatus.UNKNOWN.value,
message="Check failed",
error="psutil not available",
)
self.assertEqual(check_run.error, "psutil not available")
def test_check_run_timestamps(self):
"""CheckRun has created timestamp."""
before = timezone.now()
check_run = CheckRun.objects.create(
checker_name="memory",
status=CheckStatus.OK.value,
message="Memory OK",
)
after = timezone.now()
self.assertIsNotNone(check_run.created_at)
self.assertGreaterEqual(check_run.created_at, before)
self.assertLessEqual(check_run.created_at, after)
def test_check_run_str(self):
"""CheckRun string representation is readable."""
check_run = CheckRun.objects.create(
checker_name="disk",
status=CheckStatus.WARNING.value,
message="Disk at 75%",
)
str_repr = str(check_run)
self.assertIn("disk", str_repr.lower())
def test_check_run_ordering(self):
"""CheckRuns ordered by creation time descending."""
run1 = CheckRun.objects.create(
checker_name="cpu",
status=CheckStatus.OK.value,
message="Run 1",
)
run2 = CheckRun.objects.create(
checker_name="cpu",
status=CheckStatus.OK.value,
message="Run 2",
)
runs = list(CheckRun.objects.all())
# Most recent first (if default ordering is -created_at)
# Adjust based on actual model ordering
self.assertEqual(len(runs), 2)
def test_check_run_correlation_id(self):
"""CheckRun can store correlation ID for tracking."""
check_run = CheckRun.objects.create(
checker_name="network",
status=CheckStatus.OK.value,
message="Network OK",
correlation_id="req-12345",
)
self.assertEqual(check_run.correlation_id, "req-12345")
Step 2: Run test to verify it works
Run: uv run pytest apps/checkers/tests/test_models.py -v Expected: All tests PASS
Step 3: Commit
git add apps/checkers/tests/test_models.py
git commit -m "test(checkers): add test_models.py for CheckRun model"
Task 11: Verify all new tests pass together
Files:
- All files in
apps/checkers/tests/
Step 1: Run full new test suite
Run: uv run pytest apps/checkers/tests/ -v Expected: All tests PASS
Step 2: Compare test count with original
Run: uv run pytest apps/checkers/tests.py -v --collect-only | grep "test_" | wc -l Run: uv run pytest apps/checkers/tests/ -v --collect-only | grep "test_" | wc -l
Expected: New test suite has same or more tests than original
Step 3: Commit
git commit --allow-empty -m "test(checkers): verify restructured tests all pass"
Task 12: Remove original monolithic tests.py
Files:
- Delete:
apps/checkers/tests.py
Step 1: Verify no imports reference old file
Run: uv run grep -r "from apps.checkers.tests import" . --include="*.py" | grep -v ".pyc" Expected: No results (no imports from old file)
Step 2: Delete old test file
rm apps/checkers/tests.py
Step 3: Run tests to verify nothing broken
Run: uv run pytest apps/checkers/tests/ -v Expected: All tests PASS
Step 4: Commit
git add -A
git commit -m "chore(checkers): remove monolithic tests.py after restructure"
Task 13: Update pytest configuration if needed
Files:
- Check:
pyproject.tomlorpytest.ini
Step 1: Verify pytest discovers new tests
Run: uv run pytest apps/checkers/ -v --collect-only Expected: Shows tests from apps/checkers/tests/ directory
Step 2: Update config if needed
If tests not discovered, check testpaths in pytest config.
Step 3: Commit if changes made
git add pyproject.toml pytest.ini
git commit -m "chore: update pytest config for checkers test structure"
Verification Commands
After each task:
# Run tests for checkers app
uv run pytest apps/checkers/tests/ -v
# Run specific test file
uv run pytest apps/checkers/tests/test_base.py -v
# Run specific checker tests
uv run pytest apps/checkers/tests/checkers/ -v
# Check test coverage
uv run pytest apps/checkers/tests/ --cov=apps.checkers --cov-report=term-missing
Risk Assessment
- Low risk: Creating new test files (Tasks 1-10) - additive changes
- Low risk: Removing old tests.py (Task 12) - only after verification
- Mitigation: Run full test suite after each task
Notes
- Tests may need adjustment based on actual implementation details
- Some checker implementations may differ from assumed API
- Django system checks tests depend on actual check implementations
- Model tests assume certain fields exist on CheckRun