This is unreleased documentation for Flow Developer Portal Next version.
For up-to-date documentation, see the latest version (Current).
Version: Next
Cadence Testing Framework
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.
info
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 always runs before the tests,
a tearDown function that always runs at the end of all test cases,
a beforeEach function that runs before each test case,
and an afterEach function that runs after each test case.
All the above four functions are optional.
// A `setup` function that always runs before the rest of the test cases.
// Can be used to initialize things that would be used across the test cases.
// e.g: initialling a blockchain backend, initializing a contract, etc.
access(all) fun setup() {
}
// The `beforeEach` function runs before each test case. Can be used to perform
// some state cleanup before each test case, among other things.
access(all) fun beforeEach() {
}
// The `afterEach` function runs after each test case. Can be used to perform
// some state cleanup after each test case, among other things.
access(all) fun afterEach() {
}
// Valid test functions start with the 'test' prefix.
access(all) fun testSomething() {
}
access(all) fun testAnotherThing() {
}
access(all) fun testMoreThings() {
}
// Test functions cannot have any arguments or return values.
access(all) fun testInvalidSignature(message: String): Bool {
}
// A `tearDown` function that always runs at the end of all test cases.
// e.g: Can be used to stop the blockchain back-end used for tests, etc. or any cleanup.
fun expectFailure(_ functionWrapper: ((): Void), errorMessageSubstring: String)
The expectFailure function wraps a function call in a closure, and expects it to fail with
an error message that contains the given error message portion.
import Test
access(all) struct Foo {
access(self) let answer: UInt8
init(answer: UInt8) {
self.answer = answer
}
access(all) fun correctAnswer(_ input: UInt8): Bool {
A matcher is an object that consists of a test function and associated utility functionality.
access(all) struct Matcher {
access(all) let test: fun(AnyStruct): Bool
access(all) init(test: fun(AnyStruct): Bool) {
self.test = test
}
/// Combine this matcher with the given matcher.
/// Returns a new matcher that succeeds if this and the given matcher succeed.
///
access(all) fun and(_ other: Matcher): Matcher {
return Matcher(test: fun (value: AnyStruct): Bool {
return self.test(value) && other.test(value)
})
}
/// Combine this matcher with the given matcher.
/// Returns a new matcher that succeeds if this or the given matcher succeeds.
///
access(all) fun or(_ other: Matcher): Matcher {
return Matcher(test: fun (value: AnyStruct): Bool {
return self.test(value) || other.test(value)
})
}
}
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.
fun newMatcher<T: AnyStruct>(_ test: fun(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:
import Test
access(all) fun testExample() {
let isNegative = Test.newMatcher(fun (_ value: Int): Bool {
return value < 0
})
Test.expect(-15, isNegative)
// Alternatively, we can use `Test.assert` and the matcher's `test` function.
Test.assert(isNegative.test(-15), message: "number is not negative")
}
access(all) fun testCustomMatcherUntyped() {
let matcher = Test.newMatcher(fun (_ value: AnyStruct): Bool {
if !value.getType().isSubtype(of: Type<Int>()) {
return false
}
return (value as! Int) > 5
})
Test.expect(8, matcher)
}
access(all) fun testCustomMatcherTyped() {
let matcher = Test.newMatcher<Int>(fun (_ value: Int): Bool {
return value == 7
})
Test.expect(7, matcher)
}
The Test contract provides some built-in matcher functions for convenience.
The contain function returns a matcher that succeeds if the tested value is an array that contains
a value that is equal to the given value, or the tested value is a dictionary
that contains an entry where the key is equal to the given value.
The beSucceeded function returns a new matcher that checks if the given test value is either
a ScriptResult or TransactionResult and the ResultStatus is succeeded.
Returns false in any other case.
The beFailed function returns a new matcher that checks if the given test value is either
a ScriptResult or TransactionResult and the ResultStatus is failed.
Returns false in any other case.
The Matcher.or function combines this matcher with the given matcher.
Returns a new matcher that succeeds if this or the given matcher succeed.
If this matcher succeeds, then the other matcher would not be tested.
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.
/// Blockchain emulates a real network.
///
access(all) struct Blockchain {
access(all) let backend: AnyStruct{BlockchainBackend}
init(backend: AnyStruct{BlockchainBackend}) {
self.backend = backend
}
/// Executes a script and returns the script return value and the status.
/// `returnValue` field of the result will be `nil` if the script failed.
///
access(all) fun executeScript(_ script: String, _ arguments: [AnyStruct]): ScriptResult {
A new blockchain instance can be created using the Test.newEmulatorBlockchain method.
It returns a Blockchain which is backed by a new Flow Emulator instance.
import Test
access(all) let 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.
import Test
access(all) let blockchain = Test.newEmulatorBlockchain()
access(all) let account = blockchain.createAccount()
access(all) fun testExample() {
log(account.address)
}
Running the above command, from the command-line, we would get:
flow test tests/test_sample_usage.cdc
3:31PM DBG LOG: 0x01cf0e2f2f715450
Test results: "tests/test_sample_usage.cdc"
- PASS: testExample
The returned account consists of the address of the account, and a publicKey associated with it.
/// Account represents info about the account created on the blockchain.
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.
import Test
access(all) let blockchain = Test.newEmulatorBlockchain()
access(all) fun testExample() {
let code = "access(all) fun main(name: String): String { return \"Hello, \".concat(name) }"
let args = ["Peter"]
let scriptResult = blockchain.executeScript(code, args)
// Assert that the script was successfully executed.
Test.expect(scriptResult, Test.beSucceeded())
// returnValue has always the type `AnyStruct`,
// so we need to type-cast accordingly.
let returnValue = scriptResult.returnValue! as! String
Test.assertEqual("Hello, Peter", returnValue)
}
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).
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.
/// Transaction that can be submitted and executed on the blockchain.
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 such a 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.
The configurations can be specified during the test setup as a best-practice.
e.g: Assume running a script that imports the above Foo.cdc contract.
The import location for the contract can be specified using the placeholder "Foo".
This placeholder can be any unique string.
Suppose this script is saved in say_hello.cdc.
import "Foo"
access(all) fun main(): String {
return Foo.sayHello()
}
Then, before executing the script, the address mapping can be specified as follows:
import Test
access(all) let blockchain = Test.newEmulatorBlockchain()
access(all) let account = blockchain.createAccount()
access(all) fun setup() {
blockchain.useConfiguration(Test.Configuration({
"Foo": account.address
}))
let contractCode = Test.readFile("Foo.cdc")
let err = blockchain.deployContract(
name: "Foo",
code: contractCode,
account: account,
arguments: ["hello from args"],
)
Test.expect(err, Test.beNil())
}
access(all) fun testExample() {
let script = Test.readFile("say_hello.cdc")
let scriptResult = blockchain.executeScript(script, [])
Test.expect(scriptResult, Test.beSucceeded())
let returnValue = scriptResult.returnValue! as! String
Test.assertEqual("hello from args", returnValue)
}
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.) has failed.
It contains a message indicating why the operation failed.
// Error is returned if something has gone wrong.
//
access(all) struct Error {
access(all) let message: String
init(_ message: String) {
self.message = message
}
}
An Error can be asserted against its presence or absence.
import Test
access(all) let blockchain = Test.newEmulatorBlockchain()
access(all) let account = blockchain.createAccount()
access(all) fun testExample() {
let script = Test.readFile("say_hello.cdc")
let scriptResult = blockchain.executeScript(script, [])
// If we expect a script to fail, we can use Test.beFailed() instead
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.
let contractCode = Test.readFile("./sample/contracts/FooContract.cdc")
readFile returns the content of the file as a string.
This repository contains some functional examples
that demonstrate most of the above features, both for contrived and real-world smart contracts.
It also contains a detailed explanation on using code coverage from within the testing framework.