Test Configuration
Platform tests need to run fast. A production node verifies block signatures, checks instant lock proofs, persists platform state to disk, and creates database checkpoints. All of that is essential for security -- and all of it makes tests slow.
This chapter covers the configuration system that lets tests disable expensive checks selectively, the builder that wires everything together, and the mock RPC layer that eliminates the need for a real Dash Core node.
PlatformTestConfig
The heart of test configuration is PlatformTestConfig, defined in
packages/rs-drive-abci/src/config.rs:
#![allow(unused)] fn main() { #[cfg(feature = "testing-config")] pub struct PlatformTestConfig { /// Whether to perform block signing. pub block_signing: bool, /// Whether to store platform state to disk. pub store_platform_state: bool, /// Whether to verify block commit signatures. pub block_commit_signature_verification: bool, /// Whether to disable instant lock signature verification. pub disable_instant_lock_signature_verification: bool, /// Whether to disable contested documents validation. pub disable_contested_documents_is_allowed_validation: bool, /// Whether to disable checkpoint creation during tests. pub disable_checkpoints: bool, } }
Notice the #[cfg(feature = "testing-config")] gate. This struct does not exist in
production builds. You cannot accidentally ship code that disables signature verification.
Two Default Profiles
PlatformTestConfig provides two defaults, and choosing the right one matters:
Full defaults (Default::default()): Everything enabled. Tests run like a production
node, just with a mock RPC backend:
#![allow(unused)] fn main() { #[cfg(feature = "testing-config")] impl Default for PlatformTestConfig { fn default() -> Self { Self { block_signing: true, store_platform_state: true, block_commit_signature_verification: true, disable_instant_lock_signature_verification: false, disable_contested_documents_is_allowed_validation: true, disable_checkpoints: true, } } } }
Minimal verifications (default_minimal_verifications()): Disables everything that
is not needed to test application logic:
#![allow(unused)] fn main() { impl PlatformTestConfig { pub fn default_minimal_verifications() -> Self { Self { block_signing: false, store_platform_state: false, block_commit_signature_verification: false, disable_instant_lock_signature_verification: true, disable_contested_documents_is_allowed_validation: true, disable_checkpoints: true, } } } }
Use default() when testing consensus-critical behavior (block signing, quorum
verification). Use default_minimal_verifications() for everything else -- it is
significantly faster because it skips cryptographic operations.
When to Override Individual Fields
Sometimes you need a custom combination. For example, testing withdrawal transitions requires disabling instant lock verification but keeping everything else:
#![allow(unused)] fn main() { let platform_config = PlatformConfig { testing_configs: PlatformTestConfig { disable_instant_lock_signature_verification: true, ..Default::default() }, ..Default::default() }; }
The ..Default::default() spread syntax fills in the remaining fields with their
defaults. This pattern lets you express "default with one override" clearly.
TestPlatformBuilder
TestPlatformBuilder is the fluent API for constructing a test platform. It lives in
packages/rs-drive-abci/src/test/helpers/setup.rs:
#![allow(unused)] fn main() { pub struct TestPlatformBuilder { config: Option<PlatformConfig>, initial_protocol_version: Option<ProtocolVersion>, tempdir: TempDir, } }
The Builder Chain
The builder supports three configuration methods:
#![allow(unused)] fn main() { impl TestPlatformBuilder { /// Create a new builder with a fresh temporary directory. pub fn new() -> Self { Self::default() } /// Override the platform configuration. pub fn with_config(mut self, config: PlatformConfig) -> Self { self.config = Some(config); self } /// Pin a specific protocol version. pub fn with_initial_protocol_version( mut self, initial_protocol_version: ProtocolVersion, ) -> Self { self.initial_protocol_version = Some(initial_protocol_version); self } /// Use the latest protocol version. pub fn with_latest_protocol_version(mut self) -> Self { self.initial_protocol_version = Some(PlatformVersion::latest().protocol_version); self } } }
Building
The builder has two build methods:
build_with_mock_rpc() creates a TempPlatform<MockCoreRPCLike> -- no real
Dash Core node needed:
#![allow(unused)] fn main() { pub fn build_with_mock_rpc(self) -> TempPlatform<MockCoreRPCLike> { let config = self.config.map(|mut c| { c.db_path = self.tempdir.path().to_path_buf(); c }); let platform = Platform::<MockCoreRPCLike>::open( self.tempdir.path(), config, self.initial_protocol_version .or(Some(PlatformVersion::latest().protocol_version)), ) .expect("should open Platform successfully"); TempPlatform { platform, tempdir: self.tempdir, } } }
Notice how the builder automatically sets db_path to the temp directory -- you
cannot accidentally write to a real database.
build_with_default_rpc() creates a TempPlatform<DefaultCoreRPC> for
integration tests that need a real Dash Core connection.
Initializing State
After building, you choose what initial state to install:
#![allow(unused)] fn main() { // Minimal: just the GroveDB tree structure let platform = TestPlatformBuilder::new() .build_with_mock_rpc() .set_initial_state_structure(); // Full: genesis state with system data contracts let platform = TestPlatformBuilder::new() .with_latest_protocol_version() .build_with_mock_rpc() .set_genesis_state(); // Genesis with specific activation info let platform = TestPlatformBuilder::new() .build_with_mock_rpc() .set_genesis_state_with_activation_info( genesis_time, start_core_block_height, ); }
Most tests want set_genesis_state(). Use set_initial_state_structure() only when
testing the state structure itself.
Loading Test Data Contracts
TempPlatform provides convenience methods for loading test contracts:
#![allow(unused)] fn main() { let (platform, card_game_contract) = TestPlatformBuilder::new() .build_with_mock_rpc() .set_initial_state_structure() .with_crypto_card_game_transfer_only(Transferable::Always); }
This loads a predefined "crypto card game" data contract from
tests/supporting_files/contract/ and applies it to the platform. The returned
DataContract can be used to create documents in subsequent test steps.
Mock RPC: Simulating Dash Core
The MockCoreRPCLike type (from the mockall crate) replaces the real Dash Core
RPC client. It lets tests control exactly what Core "reports" -- which transactions
are confirmed, what the current block height is, which asset locks exist, etc.
In strategy tests, the mock is configured automatically by run_chain_for_strategy.
In unit tests, you typically let the default mock behavior handle things:
#![allow(unused)] fn main() { let platform = TestPlatformBuilder::new() .with_config(platform_config) .build_with_mock_rpc() // <-- MockCoreRPCLike .set_genesis_state(); }
The mock RPC means unit tests require zero external services. They run in CI, on developer laptops, and in sandboxed environments with no network access.
PlatformConfig for Tests vs Production
PlatformConfig is a large struct with many subsections. Here is how tests typically
configure it:
#![allow(unused)] fn main() { let config = PlatformConfig { // Validator set: 100 nodes, 67% threshold validator_set: ValidatorSetConfig::default_100_67(), // Chain lock quorum config chain_lock: ChainLockConfig::default_100_67(), // Instant lock quorum config instant_lock: InstantLockConfig::default_100_67(), // Execution settings execution: ExecutionConfig { verify_sum_trees: true, ..ExecutionConfig::default() }, // Block timing block_spacing_ms: 3000, // Test-specific overrides testing_configs: PlatformTestConfig::default_minimal_verifications(), // Fill the rest with defaults ..Default::default() }; }
The default_100_67() methods create configs for a 100-node network with a 67%
signing threshold -- the standard test network size.
Platform Restart Testing
TempPlatform supports simulating a platform restart by reopening from the same
temporary directory:
#![allow(unused)] fn main() { pub fn open_with_tempdir( tempdir: TempDir, mut config: PlatformConfig, ) -> Self { config.db_path = tempdir.path().to_path_buf(); let platform = Platform::<MockCoreRPCLike>::open( tempdir.path(), Some(config), None, ) .expect("should open Platform successfully"); Self { platform, tempdir } } }
The pattern for restart testing:
#![allow(unused)] fn main() { // Run first phase let outcome = run_chain_for_strategy( &mut platform, 50, strategy, config.clone(), seed, &mut None, &mut None, ); // Extract tempdir (ownership transfer) let tempdir = platform.tempdir; // Reopen -- simulates restart let platform = TempPlatform::open_with_tempdir(tempdir, config); // Verify state survived the restart }
Rules
Do:
- Use
default_minimal_verifications()for tests that do not need signature verification. - Use
Default::default()forPlatformTestConfigwhen testing block signing or quorum logic. - Always build with
build_with_mock_rpc()unless you specifically need Dash Core. - Let the builder manage
db_path-- never set it manually in test configs. - Use
set_genesis_state()for most tests; useset_initial_state_structure()only for low-level storage tests.
Don't:
- Disable verifications in production code --
PlatformTestConfigis#[cfg(feature)]guarded. - Create
Platforminstances directly -- always useTestPlatformBuilder. - Share temporary directories between tests -- each test gets its own
TempDir. - Forget
with_latest_protocol_version()-- without it, the builder still defaults to latest, but being explicit prevents surprises during protocol upgrades. - Use
build_with_default_rpc()in CI -- it requires a running Dash Core node.