Writing Tests
Linera applications can be tested using unit tests or integration tests. Both
are a bit different than usual Rust tests. Unit tests are executed inside a
WebAssembly virtual machine in an environment that simulates a single microchain
and a single application. System APIs are only available if they are mocked
using helper functions from linera_sdk::test
.
Integration tests run outside a WebAssembly virtual machine, and use a simulated validator for testing. This allows creating chains and adding blocks to them in order to test interactions between multiple microchains and multiple applications.
Applications should consider having both types of tests. Unit tests should be used to focus on the application's internals and core functionality. Integration tests should be used to test how the application behaves on a more complex environment that's closer to the real network.
In most cases, the simplest way to run both unit tests and integration tests is to call
linera project test
from the project's directory.
Unit tests
Unit tests are written beside the application's source code (i.e., inside the
src
directory of the project). There are several differences to normal Rust
unit tests:
-
the target
wasm32-unknown-unknown
must be selected; -
the custom test runner
linera-wasm-test-runner
must be used; -
the
#[webassembly_test]
attribute must be used instead of the usual#[test]
attribute.
The first two items are done automatically by linera project test
.
Alternatively, one may set up the environment and run cargo test
directly as
described below.
Example
A simple unit test is shown below, which tests if the application's
do_something
method changes the application state.
#[cfg(test)]
mod tests {
use crate::state::ApplicationState;
use webassembly_test::webassembly_test;
#[webassembly_test]
fn test_do_something() {
let mut application = ApplicationState {
// Configure the application's initial state
..ApplicationState::default()
};
let result = application.do_something();
assert!(result.is_ok());
assert_eq!(application, ApplicationState {
// Define the application's expected final state
..ApplicationState::default()
});
}
}
Mocking System APIs
Unit tests run in a constrained environment, so things like access to the
key-value store, cross-chain messages and cross-application calls can't be
executed. However, they can be simulated using mock APIs. The linera-sdk::test
module provides some helper functions to mock the system APIs.
Here's an example mocking the key-value store.
#[cfg(test)]
mod tests {
use crate::state::ApplicationState;
use linera_sdk::test::mock_key_value_store;
use webassembly_test::webassembly_test;
#[webassembly_test]
fn test_state_is_not_persisted() {
let mut storage = mock_key_value_store();
// Assuming the application uses views
let mut application = ApplicationState::load(storage.clone())
.now_or_never()
.expect("Mock key-value store returns immediately")
.expect("Failed to load view from mock key-value store");
// Assuming `do_something` changes the view, but does not persist it
let result = application.do_something();
assert!(result.is_ok());
// Check that the state in memory is different from the state in storage
assert_ne!(application, ApplicatonState::load(storage));
}
}
Running Unit Tests with cargo test
Running linera project test
is easier, but if there's a need to run
cargo test
explicitly to run the unit tests, Cargo must be configured to use
the custom test runner linera-wasm-test-runner
. This binary can be built from
the repository or installed with cargo install --locked linera-sdk
.
cd linera-protocol
cargo build -p linera-sdk --bin linera-wasm-test-runner --release
The steps above build the linera-wasm-test-runner
and places the resulting
binary at linera-protocol/target/release/linera-wasm-test-runner
.
With the binary available, the last step is to configure Cargo. There are a few
ways to do this. A quick way is to set the
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER
environment variable to the path of
the binary.
A more persistent way is to change one of
Cargo's configuration files.
As an example, the following file can be placed inside the project's directory
at PROJECT_DIR/.cargo/config.toml
:
[target.wasm32-unknown-unknown]
runner = "PATH_TO/linera-wasm-test-runner"
After configuring the test runner, unit tests can be executed with
cargo test --target wasm32-unknown-unknown
Optionally, wasm32-unknown-unknown
can be made the default build target with
the following lines in PROJECT_DIR/.cargo/config.toml
:
[build]
target = "wasm32-unknown-unknown"
Integration Tests
Integration tests are usually written separately from the application's source
code (i.e., inside a tests
directory that's beside the src
directory).
Integration tests are normal Rust integration tests, and they are compiled to
the host target instead of the wasm32-unknown-unknown
target used for unit
tests. This is because unit tests run inside a WebAssembly virtual machine and
integration tests run outside a virtual machine, starting isolated virtual
machines to run each operation of each block added to each chain.
Integration tests can be run with
linera project test
or simplycargo test
.
If you wish to use cargo test
and have overridden your default target to be in
wasm32-unknown-unknown
in .cargo/config.toml
, you will have to pass a native
target to cargo
, for instance cargo test --target aarch64-apple-darwin
.
Integration tests use the helper types from linera_sdk::test
to set up a
simulated Linera network, and publish blocks to microchains in order to execute
the application.
Example
A simple test that sends a message between application instances on different chains is shown below.
#[tokio::test]
async fn test_cross_chain_message() {
let (validator, application_id) = TestValidator::with_current_application(vec![], vec![]).await;
let mut sender_chain = validator.get_chain(application_id.creation.chain_id).await;
let mut receiver_chain = validator.new_chain().await;
sender_chain
.add_block(|block| {
block.with_operation(
application_id,
Operation::SendMessageTo(receiver_chain.id()),
)
})
.await;
receiver_chain.handle_received_messages().await;
assert_eq!(
receiver_chain
.query::<ChainId>(application_id, Query::LastSender)
.await,
sender_chain.id(),
);
}