Test a secondary module¶
DP3 includes helpers for writing focused unit tests for secondary modules without running a full worker, database, message broker, or snapshot scheduler.
Use DP3ModuleTestCase when you want to instantiate a
module with the application's real db_entities model and then call registered hooks directly.
The test registrar captures callbacks during module initialization and exposes runners for the
common hook families.
The config directory is read from the DP3_CONFIG_DIR environment variable unless a test class
sets config_dir explicitly. Module configuration is read from modules.<module_name> in that
config by default, where <module_name> is inferred from the module class' Python module name.
Basic pattern¶
from unittest.mock import patch
from dp3.testing import DP3ModuleTestCase
from modules.ip_exposure_profile import IPExposureProfile
class TestIPExposureProfile(DP3ModuleTestCase):
module_class = IPExposureProfile
def test_open_port_creates_service_and_link(self):
dp = self.make_observation_datapoint("ip", "192.0.2.1", "open_ports", 443)
tasks = self.run_on_new_attr("ip", "open_ports", "192.0.2.1", dp)
self.assertDatapoint(tasks, etype="service", eid="192.0.2.1:443", attr="guessed_type")
self.assertDatapoint(tasks, etype="ip", eid="192.0.2.1", attr="services")
def test_updater_uses_mocked_external_lookup(self):
with patch.object(self.module, "_fetch_service_intel", return_value={"risk": "high"}):
tasks = self.run_periodic_update(
"service",
"192.0.2.1:443",
{"eid": "192.0.2.1:443", "guessed_type": {"v": "https"}},
hook_id="service_intel",
)
self.assertDatapoint(tasks, attr="external_risk", v="high")
What the helper provides¶
DP3ModuleTestCase:
- loads
db_entitiesfromDP3_CONFIG_DIRorconfig_dirand builds a realModelSpec, - creates a minimal
PlatformConfig, - instantiates
module_classwith a test registrar, - creates validated
DataPointTaskand plain, observation, or timeseries datapoint objects using the loaded model, - calls registered hooks directly,
- provides partial-match assertions for emitted tasks, datapoints, and mutated records.
The helper is intended for module-level unit tests. It does not run a database, task queues, worker processes, recursive task ingestion, or full linked snapshot loading.
Datapoint helpers¶
Use the datapoint helpers to build values accepted by the loaded model specification:
plain = self.make_plain_datapoint("ip", "192.0.2.1", "hostname", "host.example")
observation = self.make_observation_datapoint("ip", "192.0.2.1", "open_ports", 443)
timeseries = self.make_timeseries_datapoint(
"ip",
"192.0.2.1",
"traffic",
{"packets": [1, 2, 3], "bytes": [100, 200, 300]},
)
For regular timeseries attributes, make_timeseries_datapoint() infers t2 from t1, the
configured time_step, and the number of samples when t2 is not supplied.
Hook runners¶
Common runners are available on the test case:
run_allow_entity_creation(entity, eid, task=None)run_on_entity_creation(entity, eid, task=None)run_on_new_attr(entity, attr, eid, dp)run_correlation_hooks(entity_type, record, master_record=None)run_periodic_update(entity_type, eid, master_record, hook_id=None)run_periodic_eid_update(entity_type, eid, hook_id=None)run_scheduler_job(job_id_or_func)
Correlation tests pass the snapshot record explicitly. The record must contain eid.
Creation and attribute hooks registered with refresh are also available through
run_correlation_hooks() and their refresh flags are unset by run_snapshot_finalize_hooks().
Periodic record update tests pass the raw persisted master_record; plain attributes are stored
under dictionaries such as {"v": ...}.
Scheduler jobs can be selected by the id returned from scheduler_register(), callable, or callable name.
Hook runners call module hooks directly and intentionally propagate hook exceptions to the test. The DP3 runtime is more resilient: worker, snapshot, and updater processing logs module errors and continues so one faulty secondary module hook does not stop processing. Unit tests use stricter behavior so failures are visible at assertion time and test authors can verify exceptional paths without searching runtime logs.
Assertions¶
Assertions use partial matching: only fields supplied in the expected values are checked.
self.assertDatapoint(tasks, etype="ip", attr="hostname", v="example.test")
self.assertTaskEmitted(tasks, etype="ip", eid="192.0.2.1")
self.assertNoTasks(tasks)
self.assertNoDatapoints(tasks)
self.assertRecordContains(record, exposure_score=10)
self.assertRecordAttr(record, "exposure_score", 10)
self.assertRecordUnchanged(before, after)
Snake-case aliases are also available: assert_datapoint, assert_task_emitted,
assert_no_tasks, assert_no_datapoints, assert_record_contains, assert_record_attr, and
assert_record_unchanged.
Registration assertions¶
Use registration assertions when a test needs to verify callback coverage or dynamic hook registration.
self.assert_registered("on_new_attr", entity="ip", attr="hostname")
self.assert_registered_once("correlation", entity_type="service")
self.assert_registered_attrs("service", expected_service_attrs)
self.assert_scheduler_registered(func="reload_ip_groups", minute="*/10")
assert_scheduler_registered() accepts scheduler fields such as minute, hour, and second,
along with func for matching the registered callable by object or function name.
Mocking external dependencies¶
Patch external constructors or functions before module instantiation when the dependency is created
in __init__ or load_config:
class TestDNSModule(DP3ModuleTestCase):
module_class = DNSModule
def setUp(self):
self.resolver_patcher = patch("modules.dns_module.Resolver", FakeResolver)
self.resolver_patcher.start()
self.addCleanup(self.resolver_patcher.stop)
super().setUp()
If patching is not convenient, use a test subclass as module_class and override the module's
initialization or dependency construction while keeping the hook methods under test unchanged.
Deprecated registrar methods (register_entity_hook and register_attr_hook) are supported by the
test registrar and emit DeprecationWarning. Prefer the modern registration methods in new module
code and tests.