Writing e2e tests with ink-wrapper
While this tutorial focuses on writing e2e (end-to-end) tests, the same approach can be utilized to call your contracts from any backend application written in Rust.
The default tooling for calling contracts provided by
subxt
and similar tools relies on runtime checks on input data and received responses. In order to provide compile-time checks as well as easy access to code completion and documentation for contracts, we've developed ink-wrapper
. Simply put, it's a tool that generates a bunch of strongly-typed wrapper code around submitting contract-related transactions. You can include this generated code in your project and use it, instead of calling primitives like subxt
directly.To get started you will need to install
ink-wrapper
itself (this tutorial uses 0.4.1
so please install the same version to follow along):cargo install ink-wrapper --locked --force --version 0.4.1
In this guide we will write e2e tests for an example PSP22 contract that we use to test
ink-wrapper
itself. You will need docker to run a chain for testing and use the tooling provided in the repo to compile contracts. With that:git clone [email protected]:Cardinal-Cryptography/ink-wrapper.git
Finally, let's create a new project that will house our tests:
cargo new --lib psp22-tests
The
ink-wrapper
takes a .json
metadata file generated while compiling the contract and produces a Rust file based on that. First, setup a node with the contract in question compiled and deployed. This will also run ink-wrapper
's own tests at the end - just ignore that.cd ink-wrapper
make all-dockerized
Then, invoke
ink-wrapper
on the produced .json
metadata file and put the results in the src
directory in the psp22-tests
project:ink-wrapper -m test-project/psp22_contract/target/ink/psp22_contract.json \
| rustfmt --edition 2021 > ../psp22-tests/src/psp22_contract.rs
Notice that we're piping the output of
ink-wrapper
through rustfmt
- the output is not guaranteed to be formatted, so in order to commit nicely formatted code into your repo, it's recommended to use this method when regenerating the wrapper files.Now, let's move to the
psp22-tests
project. We will need to add some dependencies to make the wrappers compile:Cargo.toml
# ...
[dependencies]
ink-wrapper-types = "0.4.0"
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
ink_primitives = "4.0.1"
aleph_client = "3.0.0"
async-trait = "0.1.68"
# These are a couple dependencies we will use to write our tests
tokio = { version = "1.25.0", features = ["macros"] }
rand = "0.8.5"
anyhow = "1.0.71"
As well as switch to nightly by putting the following in
rust-toolchain.toml
:rust-toolchain.toml
[toolchain]
channel = "nightly-2023-04-20"
components = ["rustfmt", "rust-src", "clippy"]
Attach the module produced by
ink-wrapper
to psp22-tests
:lib.rs
mod psp22_contract;
Now add the following test code:
lib.rs
#[cfg(test)]
mod tests {
use crate::psp22_contract;
// The PSP22-specific methods of the contract are hidden behind a trait.
// This will only happen for contract methods with names like "PSP22::transfer".
// Other contract methods will just be available on the contract instance without
// any extra trait.
use crate::psp22_contract::PSP22 as _;
use aleph_client::keypair_from_string;
use aleph_client::Connection;
use aleph_client::SignedConnection;
use anyhow::Result;
// This is just a convenience helper for converting any AsRef<[u8; 32]> to
// ink_primitives::AccountId - the datatype used by the generated code to
// represent account ids.
use ink_wrapper_types::util::ToAccountId as _;
use rand::RngCore as _;
#[tokio::test]
async fn it_works() -> Result<()> {
// Connect to the node launched earlier.
let conn = Connection::new("ws://localhost:9944").await;
let conn = SignedConnection::from_connection(conn, keypair_from_string("//Alice"));
let bob = keypair_from_string("//Bob");
// We're using a random salt here so that each test run is independent.
let mut salt = vec![0; 32];
rand::thread_rng().fill_bytes(&mut salt);
let total_supply = 1000;
// Constructors take a connection, the salt, and any arguments
// the actual constructor requires afterwards.
let contract = psp22_contract::Instance::new(&conn, salt, total_supply).await?;
// A mutating method takes a signed connection and any arguments afterwards.
contract
.transfer(&conn, bob.account_id().to_account_id(), 100, vec![])
.await?;
// A reader method takes a connection (may be unsigned) and any arguments afterwards.
let balance = contract
.balance_of(&conn, bob.account_id().to_account_id())
.await??;
assert_eq!(balance, 100);
Ok(())
}
}
With that, you should be able to run
cargo test
and have it pass with 1 test run!Last modified 3mo ago