Testing
Testing Guide¶
This guide explains how to run the test suite, write new tests, and debug failures.
Quick Start¶
Outside the devcontainer and CI, install the browser the Playwright suite drives:
python -m playwright install chromium
The devcontainer setup script and the CI browser job install it for you.
Run all tests with a single command:
make test
Run the Django suite without the separate browser suite:
make unittest
Run the Playwright browser suite:
make browser
Test Structure¶
The test suite covers all major plugin functionality. Tests are organized by the module they verify:
| Test File | What It Tests |
|---|---|
| test_librenms_api.py | LibreNMS API client—connections, device operations, locations, ports, and error handling |
| test_import_utils.py | Device import logic—filtering, validation, and data transformation |
| test_import_validation_helpers.py | Field validation for sites, roles, platforms, and device types |
| test_utils.py | General utilities—name matching, speed conversion, and data formatting |
| test_background_jobs.py | Background job execution and view decision logic |
| test_vlan_sync.py | VLAN sync—API fetching, comparison logic, CSS class utilities, and sync actions |
| test_interface_vlan_sync.py | Interface VLAN assignments—group resolution, mode detection, and per-interface VLAN assignment |
| test_librenms_id.py | Multi-server librenms_id helpers—get/set/find/migrate and boolean rejection |
| test_mixins.py | View mixins—CacheMixin key generation, LibreNMSAPIMixin lazy init |
| test_sync_devices.py | Device sync views—field updates, platform creation |
| test_sync_interfaces.py | Interface sync—port matching, attribute updates, MAC handling, librenms_id assignment |
| test_virtual_chassis.py | Virtual chassis detection—VC member naming patterns and name generation |
| test_sync_view_mismatch.py | Sync page context—device type mismatch detection and badge rendering |
| test_coverage_device_fields.py | Device field sync view—field update logic and device field mapping |
| test_coverage_list.py | Import list view—background job decision, job result loading, and GET handler |
| test_coverage_api.py | LibreNMS API client—malformed payload guards, error paths, and edge cases |
| test_coverage_api2.py | API views—device status, background job management, VM status endpoints |
| test_coverage_base_views.py | Base view coverage tests—sync table views, context data, and data pipeline |
| test_coverage_base_views2.py | Additional base view coverage—IP address sync, cable matching, edge cases |
| test_coverage_cache.py | Import cache helpers—cache key generation, active search tracking, metadata |
| test_coverage_device_operations.py | Device validation—type matching, serial handling, VC detection, role lookup |
| test_coverage_forms.py | Import forms—filter form choices, background-job option guards, field validation |
| test_coverage_mixins.py | View mixins—VLAN group scope resolution, VlanAssignmentMixin, scope priority |
| test_coverage_sync_interfaces.py | Interface sync view—port caching, attribute updates, MAC handling, VC member routing |
| test_coverage_sync_view.py | Sync view base class—context preparation and tab rendering |
| test_coverage_sync_views.py | Sync action views—cables, IP addresses, VLAN sync action handlers |
| test_coverage_sync_views2.py | Additional sync action view coverage—device fields, device name/type sync |
| test_coverage_sync_views3.py | Further sync action view coverage—location sync, VLAN assignment edge cases |
| test_coverage_actions.py | Import action views—bulk import, device role/cluster/rack update, validation details |
| test_coverage_filters.py | Import filter logic—filter form processing and device count helpers |
| test_init.py | Plugin startup—_ensure_librenms_id_custom_field creation, type migration, and multi-DB alias handling |
| test_coverage_tables.py | Sync tables—column rendering, row data, interface and cable table helpers |
| test_coverage_utils.py | Utility function coverage—name matching, speed conversion, site/platform lookup |
| test_coverage_virtual_chassis.py | Virtual chassis coverage—VC creation, position conflict handling, member naming |
| test_coverage_vlans_table.py | VLAN sync table—column rendering, group assignment, VLAN comparison rows |
| test_sync_modules.py | Module sync—inventory matching, module type resolution, and normalization rules |
| test_modules_view.py | Module sync view—context preparation, table rendering, and module bay mapping |
| test_tables_modules.py | Module tables—column rendering, row formatting, and action buttons |
| test_permissions.py | Permission enforcement—mixin contracts, object-level permissions, and write guards |
| test_vm_operations.py | VM operations—virtual machine sync, interface handling, and VM-specific views |
| test_integration_sync.py | Integration tests—API client against local mock HTTP server |
| test_integration_virtual_chassis.py | Integration tests—VC detection, negative cache, multi-server cache isolation |
| test_view_wiring.py | Smoke tests—view class MRO, mixin wiring, permission contracts, and template syntax |
| test_platform_mapping.py | PlatformMapping model—clean validation, YAML serialization, table/form/filterset, and find_matching_platform integration |
| test_module_replace.py | Module replacement—module swapping, bay reindexing, and replacement validation |
Supporting files:
| File | Purpose |
|---|---|
| conftest.py | Shared pytest fixtures |
| test_librenms_api_helpers.py | Auto-use fixture for API configuration mocking |
| mock_librenms_server.py | Minimal HTTP mock server for integration tests |
Running Tests¶
Running Specific Tests¶
# Run a specific test file
pytest netbox_librenms_plugin/tests/test_librenms_api.py -v
# Run a specific test class
pytest netbox_librenms_plugin/tests/test_librenms_api.py::TestLibreNMSAPIConnection -v
# Run a specific test method
pytest netbox_librenms_plugin/tests/test_librenms_api.py::TestLibreNMSAPIConnection::test_connection_success -v
Running Tests by Area¶
# API client tests
pytest netbox_librenms_plugin/tests/test_librenms_api.py netbox_librenms_plugin/tests/test_coverage_api.py netbox_librenms_plugin/tests/test_coverage_api2.py -v
# Import and validation tests
pytest netbox_librenms_plugin/tests/test_import_utils.py netbox_librenms_plugin/tests/test_import_validation_helpers.py netbox_librenms_plugin/tests/test_utils.py -v
# Background job tests
pytest netbox_librenms_plugin/tests/test_background_jobs.py -v
# Multi-server librenms_id tests
pytest netbox_librenms_plugin/tests/test_librenms_id.py -v
# Sync view tests (devices, interfaces, modules)
pytest netbox_librenms_plugin/tests/test_sync_devices.py netbox_librenms_plugin/tests/test_sync_interfaces.py netbox_librenms_plugin/tests/test_sync_modules.py -v
# Integration tests (API client against mock HTTP server)
pytest netbox_librenms_plugin/tests/test_integration_*.py -v
# Sync view mismatch detection and permission enforcement
pytest netbox_librenms_plugin/tests/test_sync_view_mismatch.py netbox_librenms_plugin/tests/test_permissions.py -v
# View wiring and template syntax smoke tests
pytest netbox_librenms_plugin/tests/test_view_wiring.py -v
Debugging Failed Tests¶
# Show full traceback
pytest netbox_librenms_plugin/tests/ -v --tb=long
# Show print statements during tests
pytest netbox_librenms_plugin/tests/ -v -s
# Stop on first failure
pytest netbox_librenms_plugin/tests/ -v -x
# Re-run only failed tests from last run
pytest netbox_librenms_plugin/tests/ -v --lf
# Re-run only failed browser tests from last run
pytest -c netbox_librenms_plugin/tests/browser/pytest.ini netbox_librenms_plugin/tests/browser --lf
Testing Philosophy¶
A test is worth what it actually runs. Rank the options in this order:
- End to end — a real request through the real view, the real permission gate, the real ORM, and the real response.
- Integration against real dependencies — a real test database, real NetBox models, real forms and serializers.
- Narrow unit tests — only for pure functions with no I/O.
Reserve mocks for boundaries you cannot run locally: the LibreNMS HTTP API, an error a local
database cannot produce (an IntegrityError on save, a lock DatabaseError), or a timing seam that
injects one deterministic concurrent write into an otherwise real flow.
A MagicMock answers any attribute and any method, so a test built on mocked models stays green
while the real query path is broken. It re-asserts the author's assumptions rather than the code.
- No external network access: Tests never call external services. The LibreNMS API is exercised through a local loopback HTTP server (
mock_librenms_server.py), so the real API client runs against realistic HTTP responses without a running LibreNMS instance. - Isolated: Each test is independent. Database tests run in a transaction that is rolled back.
- Coverage exclusions: Test files themselves are excluded from coverage reports (see
[tool.coverage.run]omit list inpyproject.toml).
The database-backed suite needs a test database, so the first run pays a one-off migration cost.
Pass --reuse-db on later runs to skip it.
Writing New Tests¶
Basic Test Template¶
Never set pytest_plugins in a test module. pytest registers that plugin for the whole
session, so any autouse fixture it carries also applies to every test file collected after
it. Import the fixture, or bind it as a module attribute, in the module that needs it.
New tests should follow this structure:
import pytest
@pytest.mark.django_db
class TestFeatureName:
"""Tests for [feature description]."""
def test_specific_behavior(self, settings):
"""Describe what this test verifies."""
from dcim.models import Device
from netbox_librenms_plugin.tests.conftest import make_device
from netbox_librenms_plugin.tests.view_test_helpers import (
make_request as make_view_request,
make_user_with_perms as make_view_user,
message_texts as view_message_texts,
post as post_view,
)
from netbox_librenms_plugin.views.imports.actions import SomeActionView
# Arrange - real rows, a real user, real grants
device = make_device("r01", serial="SN1")
user = make_view_user("feature-user", [("change", Device)])
request = make_view_request("post", {"action": "sync_name"}, user=user)
# Act - drive the real view with the request bound as dispatch() binds it
response = post_view(SomeActionView(), request, device_id=device.pk)
# Assert - on the response and on the persisted state
assert response.status_code == 200
assert Device.objects.get(pk=device.pk).name == "expected-name"
assert view_message_texts(request, "error") == []
Key Testing Conventions¶
Use inline imports inside test methods to avoid Django initialization at module load time:
def test_something(self):
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
Create real NetBox objects with the conftest.py factories instead of mocking models:
device = make_device("test-device", serial="SN1")
ip = make_ip("192.168.1.1/24", assigned_object=make_interface(device, "mgmt0"))
Deny a permission by withholding it, not by stubbing the gate. Build a user who genuinely lacks
the permission, then assert the response the gate actually returns (302 for a normal POST, 200 with
an HX-Redirect header for an HTMX POST) and re-read the object to prove no write landed.
Drive the LibreNMS API through the loopback stub rather than mocking the client:
from netbox_librenms_plugin.tests.mock_librenms_server import librenms_mock_server as run_librenms_server
from netbox_librenms_plugin.tests.test_modules_view import configure_servers as configure_test_servers
key = "default"
with run_librenms_server() as server:
configure_test_servers(settings, {key: {"librenms_url": server.url, "api_token": "t"}})
server.device_info_response(device_id=42, hostname="r01", serial="SN1")
view._librenms_api = LibreNMSAPI(server_key=key)
Patch at the source module, not where the function is imported:
# Correct - patch where the function is defined
@patch("netbox_librenms_plugin.import_utils.process_device_filters")
# Incorrect - patching the import location
@patch("netbox_librenms_plugin.views.imports.list.process_device_filters")
Available Helpers and Fixtures¶
Defined in conftest.py:
make_device,make_vm,make_interface,make_ip,make_cluster,make_superuser: real rows. These are plain functions, not pytest fixtures: import and call them. Naming one in a test's parameter list raises a fixture-not-found error.mock_response_factory— Factory for creating mock HTTP responsesmock_netbox_device,mock_netbox_vm— mock NetBox objects, kept for the unconverted legacy tests. Do not use them in new tests.
view_test_helpers.py supplies the request
and permission side: make_request, make_user_with_perms, grant, post, get,
message_texts, assert_locked_before_update.
mock_librenms_config is defined in
test_librenms_api_helpers.py.
Bind it at module scope in each test module that needs it. Its autouse=True setting
applies only to modules where the fixture is available.
Common Assertion Patterns¶
# Methods returning (success, data) tuples
success, data = api.get_device_info(123)
assert success is True
assert data["hostname"] == "expected-hostname"
# Methods returning dicts with error flags
result = api.test_connection()
assert "error" not in result
# Verifying exceptions are raised
with pytest.raises(ValueError, match="Invalid configuration"):
api.method_that_should_fail()
# Verifying persisted state after a view action
assert Device.objects.get(pk=device.pk).custom_field_data["librenms_id"] == {"default": 42}
# Verifying the messages a view queued
assert view_message_texts(request, "error") == ["You do not have permission to perform this action."]
Assert on persisted state and on the response, not on recorded mock calls. A assert_called_once()
passes whenever the call is made, including when the call did the wrong thing.
CI/CD Compatibility¶
The tests do not use production services:
- Database-backed tests require a test database connection. Other tests do not require one.
- No external network access needed (integration tests use local loopback only)
- Fast execution suitable for pre-commit hooks
- Clear failure messages for debugging
- Works in containerized environments
This makes the test suite suitable for GitHub Actions, pre-commit hooks, or any CI pipeline you choose to implement.