The Cadence testing framework provides a convenient way to write tests for Cadence programs in Cadence.
This functionality is provided by the built-in Test
contract.
The testing framework can only be used off-chain, e.g. by using the Flow CLI.
Tests must be written in the form of a Cadence script.
A test script may contain testing functions that starts with the test
prefix,
a setup
function that will always run before the tests,
and a tearDown
function that will always run at the end of all test cases.
Both setup
and tearDown
functions are optional.
1// A `setup` function that will always run before the rest of the methods.2// Can be used to initialize things that would be used across the test cases.3// e.g: initialling a blockchain backend, initializing a contract, etc.4pub fun setup() {5}67// Test functions start with the 'test' prefix.8pub fun testSomething() {9}1011pub fun testAnotherThing() {12}1314pub fun testMoreThings() {15}1617// A `tearDown` function that will always run at the end of all test cases.18// e.g: Can be used to stop the blockchain back-end used for tests, etc. or any cleanup.19pub fun tearDown() {20}
The testing framework can be used by importing the built-in Test
contract:
1import Test
1fun assert(_ condition: Bool, message: String)
Fails a test-case if the given condition is false, and reports a message which explains how the condition is false.
The message argument is optional.
1fun fail(message: String)
Immediately fails a test-case, with a message explaining the reason to fail the test.
The message argument is optional.
The expect
function tests a value against a matcher (see matchers section), and fails the test if it's not a match.
1fun expect(_ value: AnyStruct, _ matcher: Matcher)
A matcher is an object that consists of a test function and associated utility functionality.
1pub struct Matcher {23pub let test: ((AnyStruct): Bool)45pub init(test: ((AnyStruct): Bool)) {6self.test = test7}89/// Combine this matcher with the given matcher.10/// Returns a new matcher that succeeds if this and the given matcher succeed.11///12pub fun and(_ other: Matcher): Matcher {13return Matcher(test: fun (value: AnyStruct): Bool {14return self.test(value) && other.test(value)15})16}1718/// Combine this matcher with the given matcher.19/// Returns a new matcher that succeeds if this or the given matcher succeeds.20///21pub fun or(_ other: Matcher): Matcher {22return Matcher(test: fun (value: AnyStruct): Bool {23return self.test(value) || other.test(value)24})25}26}
The test
function defines the evaluation criteria for a value, and returns a boolean indicating whether the value
conforms to the test criteria defined in the function.
The and
and or
functions can be used to combine this matcher with another matcher to produce a new matcher with
multiple testing criteria.
The and
method returns a new matcher that succeeds if both this and the given matcher are succeeded.
The or
method returns a new matcher that succeeds if at-least this or the given matcher is succeeded.
A matcher that accepts a generic-typed test function can be constructed using the newMatcher
function.
1fun newMatcher<T: AnyStruct>(_ test: ((T): Bool)): Test.Matcher
The type parameter T
is bound to AnyStruct
type. It is also optional.
For example, a matcher that checks whether a given integer value is negative can be defined as follows:
1let isNegative = Test.newMatcher(fun (_ value: Int): Bool {2return value < 03})45// Use `expect` function to test a value against the matcher.6Test.expect(-15, isNegative)
The Test
contract provides some built-in matcher functions for convenience.
-
fun equal(_ value: AnyStruct): Matcher
Returns a matcher that succeeds if the tested value is equal to the given value. Accepts an
AnyStruct
value.
A blockchain is an environment to which transactions can be submitted to, and against which scripts can be run. It imitates the behavior of a real network, for testing.
1/// Blockchain emulates a real network.2///3pub struct Blockchain {45pub let backend: AnyStruct{BlockchainBackend}67init(backend: AnyStruct{BlockchainBackend}) {8self.backend = backend9}1011/// Executes a script and returns the script return value and the status.12/// `returnValue` field of the result will be `nil` if the script failed.13///14pub fun executeScript(_ script: String, _ arguments: [AnyStruct]): ScriptResult {15return self.backend.executeScript(script, arguments)16}1718/// Creates a signer account by submitting an account creation transaction.19/// The transaction is paid by the service account.20/// The returned account can be used to sign and authorize transactions.21///22pub fun createAccount(): Account {23return self.backend.createAccount()24}2526/// Add a transaction to the current block.27///28pub fun addTransaction(_ tx: Transaction) {29self.backend.addTransaction(tx)30}3132/// Executes the next transaction in the block, if any.33/// Returns the result of the transaction, or nil if no transaction was scheduled.34///35pub fun executeNextTransaction(): TransactionResult? {36return self.backend.executeNextTransaction()37}3839/// Commit the current block.40/// Committing will fail if there are un-executed transactions in the block.41///42pub fun commitBlock() {43self.backend.commitBlock()44}4546/// Executes a given transaction and commit the current block.47///48pub fun executeTransaction(_ tx: Transaction): TransactionResult {49self.addTransaction(tx)50let txResult = self.executeNextTransaction()!51self.commitBlock()52return txResult53}5455/// Executes a given set of transactions and commit the current block.56///57pub fun executeTransactions(_ transactions: [Transaction]): [TransactionResult] {58for tx in transactions {59self.addTransaction(tx)60}6162let results: [TransactionResult] = []63for tx in transactions {64let txResult = self.executeNextTransaction()!65results.append(txResult)66}6768self.commitBlock()69return results70}7172/// Deploys a given contract, and initilizes it with the arguments.73///74pub fun deployContract(75name: String,76code: String,77account: Account,78arguments: [AnyStruct]79): Error? {80return self.backend.deployContract(81name: name,82code: code,83account: account,84arguments: arguments85)86}87}
The BlockchainBackend
provides the actual functionality of the blockchain.
1/// BlockchainBackend is the interface to be implemented by the backend providers.2///3pub struct interface BlockchainBackend {45pub fun executeScript(_ script: String, _ arguments: [AnyStruct]): ScriptResult67pub fun createAccount(): Account89pub fun addTransaction(_ tx: Transaction)1011pub fun executeNextTransaction(): TransactionResult?1213pub fun commitBlock()1415pub fun deployContract(16name: String,17code: String,18account: Account,19arguments: [AnyStruct]20): Error?21}
A new blockchain instance can be created using the newEmulatorBlockchain
method.
It returns a Blockchain
which is backed by a new Flow Emulator instance.
1let blockchain = Test.newEmulatorBlockchain()
It may be necessary to create accounts during tests for various reasons, such as for deploying contracts, signing transactions, etc.
An account can be created using the createAccount
function.
1let acct = blockchain.createAccount()
The returned account consist of the address
of the account, and a publicKey
associated with it.
1/// Account represents info about the account created on the blockchain.2///3pub struct Account {4pub let address: Address5pub let publicKey: PublicKey67init(address: Address, publicKey: PublicKey) {8self.address = address9self.publicKey = publicKey10}11}
Scripts can be run with the executeScript
function, which returns a ScriptResult
.
The function takes script-code as the first argument, and the script-arguments as an array as the second argument.
1let result = blockchain.executeScript("pub fun main(a: String) {}", ["hello"])
The script result consists of the status
of the script execution, and a returnValue
if the script execution was
successful, or an error
otherwise (see errors section for more details on errors).
1/// The result of a script execution.2///3pub struct ScriptResult {4pub let status: ResultStatus5pub let returnValue: AnyStruct?6pub let error: Error?78init(status: ResultStatus, returnValue: AnyStruct?, error: Error?) {9self.status = status10self.returnValue = returnValue11self.error = error12}13}
A transaction must be created with the transaction code, a list of authorizes, a list of signers that would sign the transaction, and the transaction arguments.
1/// Transaction that can be submitted and executed on the blockchain.2///3pub struct Transaction {4pub let code: String5pub let authorizers: [Address]6pub let signers: [Account]7pub let arguments: [AnyStruct]89init(code: String, authorizers: [Address], signers: [Account], arguments: [AnyStruct]) {10self.code = code11self.authorizers = authorizers12self.signers = signers13self.arguments = arguments14}15}
The number of authorizers must match the number of AuthAccount
arguments in the prepare
block of the transaction.
1let tx = Test.Transaction(2code: "transaction { prepare(acct: AuthAccount) {} execute{} }",3authorizers: [account.address],4signers: [account],5arguments: [],6)
There are two ways to execute the created transaction.
-
Executing the transaction immediately
1let result = blockchain.executeTransaction(tx)This may fail if the current block contains transactions that have not being executed yet.
-
Adding the transaction to the current block, and executing it later.
1// Add to the current block2blockchain.addTransaction(tx)34// Execute the next transaction in the block5let result = blockchain.executeNextTransaction()
The result of a transaction consists of the status of the execution, and an Error
if the transaction failed.
1/// The result of a transaction execution.2///3pub struct TransactionResult {4pub let status: ResultStatus5pub let error: Error?67init(status: ResultStatus, error: Error) {8self.status = status9self.error = error10}11}
commitBlock
block will commit the current block, and will fail if there are any un-executed transactions in the block.
1blockchain.commitBlock()
A contract can be deployed using the deployContract
function of the Blockchain
.
1let contractCode = "pub contract Foo{ pub let msg: String; init(_ msg: String){ self.msg = msg } pub fun sayHello(): String { return self.msg } }"23let err = blockchain.deployContract(4name: "Foo",5code: contractCode,6account: account,7arguments: ["hello from args"],8)
An Error
is returned if the contract deployment fails. Otherwise, a nil
is returned.
A common pattern in Cadence projects is to define the imports as file locations and specify the addresses corresponding to each network in the Flow CLI configuration file. When writing tests for a such project, it may also require to specify the addresses to be used during the tests as well. However, during tests, since accounts are created dynamically and the addresses are also generated dynamically, specifying the addresses statically in a configuration file is not an option.
Hence, the test framework provides a way to specify the addresses using the
useConfiguration(_ configuration: Test.Configuration)
function in Blockchain
.
The Configuration
struct consists of a mapping of import locations to their addresses.
1/// Configuration to be used by the blockchain.2/// Can be used to set the address mapping.3///4pub struct Configuration {5pub let addresses: {String: Address}67init(addresses: {String: Address}) {8self.addresses = addresses9}10}
The Blockchain.useConfiguration
is a run-time alternative for
statically defining contract addresses in the flow.json config file.
The configurations can be specified during the test setup as a best-practice.
e.g: Assume running a script that imports FooContract
and BarContract
.
The import locations for the two contracts can be specified using the two placeholders "FooContract"
and
"BarContract"
. These placeholders can be any unique strings.
1import FooContract from "FooContract"2import BarContract from "BarContract"34pub fun main() {5// do something6}
Then, before executing the script, the address mapping can be specified as follows:
1pub var blockchain = Test.newEmulatorBlockchain()2pub var accounts: [Test.Account] = []34pub fun setup() {5// Create accounts in the blockchain.67let acct1 = blockchain.createAccount()8accounts.append(acct1)910let acct2 = blockchain.createAccount()11accounts.append(acct2)1213// Set the configuration with the addresses.14// They keys of the mapping should be the placeholders used in the imports.1516blockchain.useConfiguration(Test.Configuration({17"FooContract": acct1.address,18"BarContract": acct2.address19}))20}
The subsequent operations on the blockchain (e.g: contract deployment, script/transaction execution) will resolve the import locations to the provided addresses.
An Error
maybe returned when an operation (such as executing a script, executing a transaction, etc.) is failed.
Contains a message indicating why the operation failed.
1// Error is returned if something has gone wrong.2//3pub struct Error {4pub let message: String56init(_ message: String) {7self.message = message8}9}
An Error
may typically be handled by failing the test case or by panicking (which will result in failing the test).
1let err: Error? = ...23if let err = err {4panic(err.message)5}
Writing tests often require constructing source-code of contracts/transactions/scripts in the test script. Testing framework provides a convenient way to load programs from a local file, without having to manually construct them within the test script.
1let contractCode = Test.readFile("./sample/contracts/FooContract.cdc")
readFile
returns the content of the file as a string.