Testing Services (Rust)

Psibase extends Rust's unit and integration test system to support testing services.

A Simple Test Case

Run the following to create a project:

cargo new --lib example
cd example
cargo add psibase
cargo add -F derive serde

This creates the following files:

example
├── Cargo.toml      Project configuration
├── Cargo.lock      Versions of dependencies
└── src
    └── lib.rs      Service source file

Replace the content of lib.rs with the following. This contains both the service definition and some test cases for it:

#[psibase::service]
mod service {
    #[action]
    fn add(a: i32, b: i32) -> i32 {
        println!("Let's add {} and {}", a, b);
        println!("Hopefully the result is {}", a + b);
        a + b
    }

    #[action]
    fn multiply(a: i32, b: i32) -> i32 {
        println!("Let's multiply {} and {}", a, b);
        a * b
    }
}

#[psibase::test_case(services("example"))]
fn test_arith(chain: psibase::Chain) -> Result<(), psibase::Error> {
    // Verify the actions work as expected.
    assert_eq!(Wrapper::push(&chain).add(3, 4).get()?, 7);
    assert_eq!(Wrapper::push(&chain).multiply(3, 4).get()?, 12);

    // Start a new block; this prevents the following transaction
    // from being rejected as a duplicate.
    chain.start_block();

    // Print a trace; this allows us to see:
    // * The service call chain. Something calls our service;
    //   let's see what it is!
    // * The service's prints, which are normally invisible
    //   during testing
    println!(
        "\n\nHere is the trace:\n{}",
        Wrapper::push(&chain).add(3, 4).trace
    );

    // If we got this far, then the test has passed
    Ok(())
}

Running the Test

cargo psibase test

This command:

  • Builds the service
  • Builds the tests
  • Runs the tests using psitest, which comes with the SDK

You should see output like the following:

running 1 test
test test_arith ...

Here is the trace:
action:
     => transact::
    raw_retval: 0 bytes
    action:
        transact => auth-any::checkauthsys
        raw_retval: 0 bytes
    action:
        example => example::add
        raw_retval: 4 bytes
        console:    Let's add 3 and 4
                    Hopefully the result is 7
ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Manually Deploying A Service

#[psibase::test_case(services("example"))] automatically deployed the example service to the example account on a test chain. We could deploy it ourselves and gain some control.

// This time we don't have `services("example")` in the attribute arguments.
#[psibase::test_case]
fn manual_deploy(chain: psibase::Chain) -> Result<(), psibase::Error> {
    // Deploy the service
    chain.deploy_service(
        psibase::account!("another"), // Account to deploy on
        include_service!("example"),  // Service to deploy
    )?;

    // `push` defaulted the `service` and `sender` fields of the action to
    // `"example"`. We're using a different account now.
    assert_eq!(
        Wrapper::push_from_to(
            &chain,
            psibase::account!("another"), // sender
            psibase::account!("another")  // receiver
        )
        .add(3, 4)
        .get()?,
        7
    );

    Ok(())
}

Chain::deploy_service creates an account if it doesn't already exist and installs a service on that account. Its second argument is a &[u8], which contains the service WASM.

The include_service! macro returns the service wasm as a &[u8]. Its argument may be any of the following, assuming they define a service:

  • The name of the current package
  • The name of any package the current package depends on
  • The name of any package in the current workspace, if any

The include_service! macro only exists within a psibase::test_case.

Under The Hood

The include_service! macro uses prints to notify cargo psibase test that it needs a service (Rust package). cargo psibase test builds that package and sets an environment variable to let the macro know the final location of the built WASM. include_service! then expands into include_bytes! with the WASM's location.

The services(...) argument of the test_case macro expands to code which uses include_service!.

Next Step

Services may call other services; we show how to do this in Calling Other Services.