Test Doubles and In-Memory Repositories#
Dify's workflow and graph-engine unit tests avoid mocks by using protocol-based concrete test doubles β real implementations that satisfy typing.Protocol interfaces with in-memory state. This pattern appears throughout api/tests/unit_tests/core/workflow/ and enables deterministic, end-to-end exercising of node behavior, pause/resume cycles, and multi-branch join logic without a database or external services.
Why Protocols Enable This#
Core repository and entity contracts are defined as typing.Protocol rather than ABCs . Because Python structural typing accepts any object that satisfies the method signatures, test doubles can be plain dataclasses or classes without subclassing any framework base. The same pattern applies to FileReferenceFactoryProtocol (imported from graphon.nodes.protocols), which test doubles implement by returning File objects directly from raw mappings without touching Dify's file-persistence layer .
Catalog of In-Memory Test Doubles#
| Double | Defined In | Replaces |
|---|---|---|
InMemoryPauseStore | test_parallel_human_input_join_resume.py | Serialized graph state persistence |
StaticRepo (HumanInputFormRepository) | test_parallel_human_input_join_resume.py | DB-backed form repository |
StaticForm (HumanInputFormEntity) | test_parallel_human_input_join_resume.py | ORM-backed form entity |
InMemoryHumanInputFormRepository | test_entities.py | DB-backed form repository |
_InMemoryFormEntity (HumanInputFormEntity) | test_entities.py | ORM-backed form entity |
_FakeFormRepository | test_human_input_form_filled_event.py | DB-backed form repository |
_TestFileReferenceFactory | test_entities.py, test_human_input_form_filled_event.py | DifyFileReferenceFactory (tenant-aware) |
Protocol Interfaces Being Doubled#
HumanInputFormRepositoryβ two methods:get_form(node_id, *, form_id?)andcreate_form(params)HumanInputFormEntityβ read-only property protocol coveringid,submitted,submitted_data,selected_action_id,status,expiration_time, etc.PauseStateStore(local protocol in the test file) βsave(runtime_state)/load()wrappingGraphRuntimeState.dumps()/GraphRuntimeState.from_snapshot()
How Each Double Works#
InMemoryPauseStore serializes a GraphRuntimeState to a string snapshot on save() and deserializes it on load(), simulating the object-storage round-trip used in production . This lets tests verify that pause β resume cycles correctly restore graph state.
InMemoryHumanInputFormRepository creates auto-incrementing _InMemoryFormEntity instances, stores them keyed by node_id, and exposes set_submission() to mutate the last created form β simulating a user filling and submitting the form between two node._run() calls .
StaticRepo / StaticForm take pre-built form state at construction time (immutable initial data) and support a set_forms() method to swap form state between graph runs. The create_form() method deliberately raises AssertionError β enforcing that resume scenarios must never attempt to re-create existing forms .
_FakeFormRepository is the simplest variant: a one-liner that always returns a SimpleNamespace fake form, used for tests that only need to verify post-submission output mapping .
_TestFileReferenceFactory implements FileReferenceFactoryProtocol.build_from_mapping() by constructing File objects directly from raw dict payloads, bypassing Dify's tenant-scoped file factory .
Parallel Human-Input Join Test Pattern#
The most complex use of these doubles is test_parallel_human_input_join_resume.py, which exercises a fork-join graph with two simultaneous HumanInputNode branches. The test runs the graph three times (initial β first resume β second resume), using InMemoryPauseStore to carry state between runs and StaticRepo.set_forms() to advance form submission state between each run . The GraphEngine uses InMemoryChannel (from the external graphon package) as its command channel, keeping the engine fully synchronous within the test process .
Shared Test Fixtures#
api/tests/workflow_test_utils.py provides shared builders β build_test_run_context(), build_test_graph_init_params(), and build_test_variable_pool() β that assemble the context objects (GraphInitParams, VariablePool) all node and graph tests need . These are lightweight wrappers around production constructors, not mocks.
Where to Find the Production Counterparts#
| Concept | Production Implementation |
|---|---|
| Form repository | HumanInputFormRepositoryImpl in api/core/repositories/human_input_repository.py |
| File reference factory | DifyFileReferenceFactory in api/core/workflow/node_runtime.py |
| Pause state persistence | PauseStatePersistenceLayer in api/core/app/layers/pause_state_persist_layer.py |