vike
DeveloperTesting

Test doubles

One owned double per seam, behind a `test-support` feature a default build never compiles — the scripted WS streams, the in-memory history store, the recording broker, and the sanitized capture writer.

The rule is one sentence: one owned double per seam, behind a test-support feature. It exists because the alternative was measured. Before crates/vike-bridge-core/src/scripted.rs, every consumer hand-rolled the same two doubles — the user-data pump tests each carried a private scripted queue, and the market and depth drivers plus a pump tap and a re-parse harness each carried a private canned-text stream. One drift-prone shape, roughly eight copies. The shared RecordingSink in vike_data::test_support tells the same story on the live-data seam: one capturing sink instead of the roughly thirteen inline copies the venue bridges carried.

Every double is gated #[cfg(any(test, feature = "test-support"))], so in-crate unit tests see it always, and a downstream crate reaches it by enabling the feature as a dev-dependency. A normal build never compiles any of it, and no dev-dependency edge can reach a shipped binary.

The four homes

vike_bridge_core::scripted — the scripted stream doubles for the three WS seams: UserStream, MarketStream and DepthStream. Its behaviour pins are carried verbatim from the copies it replaced, and they are the interesting part:

  • exhausting the script is a stream-closed error, the same shape a real disconnect takes, so a driver returning an error at the end of a script is expected rather than a failure;
  • send_text records what the driver sent — subscribe frames, keepalive pings — into a log the test reads back, shareable across sessions for the reconnect-test shape;
  • an optional shared clock advances before every read, so consuming the script ages an ack or freshness watchdog with zero real sleeps;
  • a timeout step can escalate the reported stall the moment it is popped, because a synchronous session gives a test no re-entry point to mutate the stream mid-call.

vike_data::test_support — a DataFusion-free in-memory HistStore, plus the shared RecordingSink and NoopSink live-data doubles. Venue bridges build a properties recorder over it to assert their opt-in recording wiring without pulling the Arrow and DataFusion tree into a test build.

vike_model::strategy::MockBroker — a minimal recording Broker for the strategy layer. It captures every market and limit submit and lets a test script the clock, the mark and the position; the rest of the surface is inert. It lives in the trait's own home, consolidated from near-identical per-crate copies, so a new strategy-layer test inherits it instead of declaring another broker.

vike_bridge_core::capture — a separate feature, and a different kind of tool: it writes fixtures rather than standing in for something. See below.

A double's job is fidelity, not convenience

The in-memory store is the worked example, and its header argues each choice. Bars are erased on append of exactly the fields the real bar schema never persists, so the round trip loses precisely what the real store's does. An empty batch returns zero without consuming the commit key, matching the real store — a double that burned the key would turn a later real append under that key into a no-op that happens only in tests. The catalog verbs answer with a real fold over the held seams rather than inheriting a default, because the empty default they replaced had a seeded double answering "no series" while holding rows. And the parts that hold nothing say so honestly: quotes, trades and book read empty, and the resample verbs return zero rather than fabricating, because resampling zero ticks derives zero bars on the real store too.

The capture feature: real wire as a fixture

vike_bridge_core::capture takes a verbatim venue frame and returns it with wire structure preserved exactly — field names, value types, nesting, casing — because the structure is the drift-sensitive part a hand-authored fixture can never witness. Only a known-sensitive leaf key set is redacted, and the placeholder depends on the key, never on the secret value, so two captures taken with different session keys sanitize to identical bytes and a re-capture diff shows real wire drift rather than secret churn.

Venue order ids, trade ids, prices, quantities and timestamps are deliberately kept — they are the fixture's fidelity, and a demo-account order id identifies nothing. Key order inside a frame is sorted on write, since JSON key order is not wire semantics and sorting keeps diffs minimal.

Fixtures land under crates/bridges/<venue>/tests/fixtures/captured/ with a provenance stamp, written by that venue's opt-in capture smoke and then consumed twice: by the venue's normal, CI-run captured_wire_replay test, and as frame templates by the exec contract harness. The replay test is not #[ignore]d, needs no network and no credentials — and a missing committed fixture is a failure, because a deleted gate must not pass silently.

Which features imply what

vike-bridge-core's test-support implies its full feature, for a structural reason: the doubles are depth, market-pump and user-data streams, so without full there is nothing for them to double. capture implies nothing — standard library and serde_json only — and is gated so the shipped order-signing binary never compiles the fixture writer. vike-model and vike-data both declare an empty, additive test-support.

The crate also carries a self-referential dev-dependency on itself with test-support enabled, which is the standard cargo idiom for making a feature-gated module visible to the crate's own integration tests: an integration test compiles the library without cfg(test), so the feature is the only way in. It never affects a normal build.

Related: live smokes for where captures come from, and the live data seam for what the sink doubles stand in for.

On this page