Mocking communications
Data acquisition (DAQ) and processing software is often tightly coupled to the hardware it talks to: sensors, network links, message brokers. This makes such software hard to test, because a "real" test needs the actual hardware, or at least a live network, to be available. Access to that infrastructure is frequently limited, expensive, or shared between several teams, which slows down development and makes automated testing (CI) difficult.
To solve this, Phoenix2 uses a mock mechanism for its communication libraries (sockets). The idea is simple: any piece of code that sends or receives data through a socket can be switched, without any code change, between talking to a real communication channel and a mock one that simply reads and writes to a file.
The idea: one API, two backends
Application code never talks directly to the network. Instead, it always goes through the same generic socket API (send, recv, ...). Behind that single API, two different implementations can answer the calls:
- a real backend, which uses an actual network technology (Phoenix2 uses ZeroMQ by default) to talk to another process,
- a mock backend, which reads and writes the exact same data, but from/to a binary file on disk instead of the network.
Because both backends expose the same API, the application code that uses sockets does not know, and does not need to know, which one is currently active.
flowchart LR
App["Application code<br/>(sendData / recvData)"] --> GS["Generic Socket"]
GS -->|production| RB["Real backend<br/>(e.g. ZeroMQ, network)"]
GS -->|testing| MB["Mock backend<br/>(binary file on disk)"]
Three modes, one code path
Switching between real and mock behavior is a simple configuration choice, called a mode:
| Mode | What happens | Typical use |
|---|---|---|
NO_MOCK |
Only the real backend is used. | Production / normal operation. |
MOCK |
Only the mock backend is used: data is read from / written to a file, no network needed. | Fast unit and integration tests, offline development. |
MOCK_RECORD |
Both backends run together: real traffic is exchanged as usual, and every message is also saved to a mock file. | Capturing a real session to reuse later as a test. |
The same application binary, with the same code, can run in any of these three modes. Only the mode changes.
Why this is useful
No hardware needed to test. Since MOCK mode never touches the network, unit and integration tests run in milliseconds, in a single process, without needing the real hardware, a live network, or any external infrastructure. This makes it realistic to test communication-heavy code continuously in CI.
Record once, replay forever. MOCK_RECORD lets you capture a real communication session, on real hardware, exactly as it happened. That recording becomes a mock file that can be replayed later, offline, as many times as needed. This is especially valuable in DAQ contexts where access to the hardware is only available occasionally: capture the session while you have access, then debug or write regression tests against the recording afterward, without needing the hardware again.
Maintainability. Because application code only ever talks to the generic socket, replacing the underlying technology (upgrading ZeroMQ, switching to Unix sockets, or something else entirely) only requires a new backend implementation. The application code, and all the existing tests, are unaffected.
Extensibility. Adding a new backend (real or mock) does not require modifying any existing file: it is a purely additive operation.
Reproducibility. Mock files are just binary files. They can be committed alongside test code, shared between developers, or archived alongside published results, so that a test scenario or an experimental condition can be reproduced exactly, even by someone else, even later.
Multi-language. Phoenix2 libraries are available in C++, Rust and Python, and all share the same underlying binary message format. This means a mock file recorded from a C++ process can be replayed by a Python or Rust test, and vice versa.
A simple example
The pattern always follows two steps: first record, then replay.
// --- Sender side: record a real exchange to a mock file ---
SocketManager sender(PSocketMode::MOCK_RECORD);
sender.addServerSocket("Alice", {"localhost", 3390});
sender.sendData("Alice", 42u);
// --- Receiver side: replay the recorded data, no network involved ---
SocketManager receiver(PSocketMode::MOCK);
receiver.addClientSocket("Bob", {"localhost", 3390});
size_t value;
receiver.recvData("Bob", value); // value == 42
The mock files are named after the socket they belong to (hostname and port), so a whole test suite can ship a set of pre-recorded files alongside its test code, and reproduce complex, multi-message scenarios deterministically.
Good to know
Mock playback faithfully reproduces the content of a communication, but not its timing: two messages exchanged a few milliseconds apart in real life will simply be replayed one after the other, without the actual delay. This makes mocks excellent for testing what your code does with the data, but they are not a substitute for real, live tests when it comes to timing-sensitive behavior, high-frequency bursts, or concurrency.
Going further
The mock mechanism is provided by dedicated libraries:
- PhoenixSocket implements the generic socket API together with its real (ZeroMQ) and mock backends, in C++.
- PhoenixGenericMock provides the generic record/replay file mechanism used by the mock backends.
- PhoenixCheckStream helps validate mock data during test replay, with readable error messages when something does not match.