Based on patterns from Flow EVM Bridge tests and other Flow projects.
cadence/tests/
├── test_helpers.cdc # Shared test utilities
├── setup_tests.cdc # Common setup functions
├── unit/ # Unit tests for individual components
│ ├── ComponentA_test.cdc
│ └── ComponentB_test.cdc
├── integration/ # Integration tests
│ └── flow_integration_test.cdc
└── fixtures/ # Test data and mock contracts
└── mock_contracts.cdc
import Test
import BlockchainHelpers
import "ContractName"
// Test helper functions at the top
access(all) fun setupTest(): @TestResources {
// Setup code
}
// Main setup function
access(all) fun setup() {
let err = Test.deployContract(
name: "ContractName",
path: "../contracts/ContractName.cdc",
arguments: []
)
Test.expect(err, Test.beNil())
}
// Individual test functions
access(all) fun testFeatureName() {
// Test implementation
}
// Cleanup if needed
access(all) fun tearDown() {
// Cleanup code
}// Deploy with arguments
let err = Test.deployContract(
name: "MyContract",
path: "../contracts/MyContract.cdc",
arguments: [arg1, arg2]
)
Test.expect(err, Test.beNil())
// Deploy multiple contracts
let contracts = ["ContractA", "ContractB", "ContractC"]
for contract in contracts {
let err = Test.deployContract(
name: contract,
path: "../contracts/".concat(contract).concat(".cdc"),
arguments: []
)
Test.expect(err, Test.beNil())
}// Create test accounts
let alice = Test.createAccount()
let bob = Test.createAccount()
// Fund accounts
let fundingResult = Test.mintFlow(to: alice, amount: 1000.0)
Test.expect(fundingResult, Test.beSucceeded())
// Get account addresses
let aliceAddress = alice.address
let bobAddress = bob.address// Execute transaction
let txResult = Test.executeTransaction(
"../transactions/my_transaction.cdc",
[arg1, arg2],
alice
)
Test.expect(txResult, Test.beSucceeded())
// Test transaction failure
let failingTx = Test.executeTransaction(
"../transactions/failing_transaction.cdc",
[invalidArg],
bob
)
Test.expect(failingTx, Test.beFailed())
Test.assertError(failingTx, errorMessage: "Expected error message")// Execute script and check result
let scriptResult = Test.executeScript(
"../scripts/get_balance.cdc",
[accountAddress]
)
Test.expect(scriptResult, Test.beSucceeded())
let balance = scriptResult.returnValue! as! UFix64
Test.assertEqual(100.0, balance)// Test event emission
let events = Test.eventsOfType(Type<MyContract.MyEvent>())
Test.expect(events.length, Test.beGreaterThan(0))
let event = events[0] as! MyContract.MyEvent
Test.assertEqual(expectedValue, event.field)// Advance blockchain time
Test.moveTime(by: secondsInDay)
// Set specific block height
Test.moveToBlockHeight(1000)// Pattern 1: Using Test.executeTransaction with error checking
let result = Test.executeTransaction(
"../transactions/will_fail.cdc",
[args],
signer
)
Test.expect(result, Test.beFailed())
Test.assertError(result, errorMessage: "Expected error substring")
// Pattern 2: Using scripts to check state
let canWithdraw = Test.executeScript(
"pub fun main(amount: UFix64): Bool {
// Check if withdrawal would succeed
return amount <= availableBalance
}",
[withdrawAmount]
).returnValue! as! Bool
Test.assertEqual(false, canWithdraw)// Create and manage resources in tests
access(all) fun testResourceCreation() {
let testAccount = Test.createAccount()
// Create resource via transaction
let createResult = Test.executeTransaction(
"../transactions/create_resource.cdc",
[],
testAccount
)
Test.expect(createResult, Test.beSucceeded())
// Verify resource exists
let hasResource = Test.executeScript(
"../scripts/check_resource.cdc",
[testAccount.address]
).returnValue! as! Bool
Test.assertEqual(true, hasResource)
}// Test complex state changes
access(all) fun testComplexStateChange() {
// Setup initial state
let setupResult = executeSetupTransactions()
Test.expect(setupResult, Test.beSucceeded())
// Perform action
let actionResult = Test.executeTransaction(
"../transactions/complex_action.cdc",
[param1, param2],
signer
)
Test.expect(actionResult, Test.beSucceeded())
// Verify multiple state changes
let finalState = Test.executeScript(
"../scripts/get_state.cdc",
[]
).returnValue! as! {String: AnyStruct}
Test.assertEqual(expectedValue1, finalState["field1"])
Test.assertEqual(expectedValue2, finalState["field2"])
}// Reusable setup function
access(all) fun setupTestEnvironment(): TestEnvironment {
let admin = Test.createAccount()
let user1 = Test.createAccount()
let user2 = Test.createAccount()
// Deploy contracts
deployAllContracts()
// Initialize
initializeContracts(admin: admin)
return TestEnvironment(
admin: admin,
user1: user1,
user2: user2
)
}
// Batch operations
access(all) fun mintTokensToMultipleAccounts(
accounts: [Test.Account],
amount: UFix64
) {
for account in accounts {
let mintResult = Test.executeTransaction(
"../transactions/mint_tokens.cdc",
[account.address, amount],
admin
)
Test.expect(mintResult, Test.beSucceeded())
}
}// Custom assertion functions
access(all) fun assertBalanceEquals(
account: Test.Account,
expectedBalance: UFix64,
message: String
) {
let balance = getBalance(account: account)
Test.assertEqual(
expectedBalance,
balance,
message: message.concat(" - Expected: ")
.concat(expectedBalance.toString())
.concat(", Got: ")
.concat(balance.toString())
)
}
// Range assertions
access(all) fun assertInRange(
value: UFix64,
min: UFix64,
max: UFix64,
message: String
) {
Test.assert(
value >= min && value <= max,
message: message
)
}- Use Test.executeTransaction instead of Test.expectFailure when possible
- Create helper functions for common operations
- Use descriptive test names that explain what is being tested
- Group related tests in the same file
- Clean up resources after tests when necessary
- Use scripts to verify state instead of relying on transaction success
- Test both success and failure cases
- Use meaningful assertion messages that help debug failures
- Avoid hardcoded values - use constants or helper functions
- Test edge cases and boundary conditions
- Don't use Test.expectFailure - it has known issues in the current framework
- Don't assume transaction order - each test should be independent
- Don't forget to check events when they're part of the contract behavior
- Don't ignore precision issues with UFix64 calculations
- Don't test implementation details - focus on behavior
import Test
import BlockchainHelpers
import "MyContract"
// Constants
let INITIAL_SUPPLY: UFix64 = 1000000.0
let DECIMALS: UInt8 = 8
// Helper structures
access(all) struct TestAccounts {
access(all) let admin: Test.Account
access(all) let alice: Test.Account
access(all) let bob: Test.Account
init() {
self.admin = Test.createAccount()
self.alice = Test.createAccount()
self.bob = Test.createAccount()
}
}
// Setup
access(all) fun setup() {
// Deploy contract
let err = Test.deployContract(
name: "MyContract",
path: "../contracts/MyContract.cdc",
arguments: [INITIAL_SUPPLY, DECIMALS]
)
Test.expect(err, Test.beNil())
}
// Helper functions
access(all) fun setupAccounts(): TestAccounts {
return TestAccounts()
}
// Tests
access(all) fun testInitialization() {
let supply = Test.executeScript(
"../scripts/get_total_supply.cdc",
[]
).returnValue! as! UFix64
Test.assertEqual(INITIAL_SUPPLY, supply)
}
access(all) fun testTransfer() {
let accounts = setupAccounts()
// Setup: Give Alice some tokens
let mintResult = Test.executeTransaction(
"../transactions/mint.cdc",
[accounts.alice.address, 100.0],
accounts.admin
)
Test.expect(mintResult, Test.beSucceeded())
// Action: Transfer from Alice to Bob
let transferResult = Test.executeTransaction(
"../transactions/transfer.cdc",
[accounts.bob.address, 50.0],
accounts.alice
)
Test.expect(transferResult, Test.beSucceeded())
// Verify: Check balances
let aliceBalance = getBalance(accounts.alice)
let bobBalance = getBalance(accounts.bob)
Test.assertEqual(50.0, aliceBalance)
Test.assertEqual(50.0, bobBalance)
}
// Utility functions
access(all) fun getBalance(account: Test.Account): UFix64 {
return Test.executeScript(
"../scripts/get_balance.cdc",
[account.address]
).returnValue! as! UFix64
}This guide provides a comprehensive foundation for writing robust Cadence tests based on patterns from successful Flow projects.