Build and Test Packages
Once you have created a package and added a module, you can build and test your package locally to ensure it's working as expected before publishing it.
Building your package
You can use the iota move build
command to build Move packages in the working directory, first_package
in this case.
iota move build
If your build fails, you can use the IOTA Client's error message to troubleshoot any errors and debug your code.
If your build is successful, the IOTA client will return the following:
UPDATING GIT DEPENDENCY https://github.com/iotaledger/iota.git
INCLUDING DEPENDENCY IOTA
INCLUDING DEPENDENCY MoveStdlib
BUILDING first_package
Test a Package
You can use the Move testing framework to write unit tests for your IOTA package. IOTA includes support for the Move testing framework.
Test Syntax
You should add unit tests in their corresponding test file. In Move, test functions are identified by the following:
- They are
public
functions. - They have no parameters.
- They have no return values.
You can use the following command in the package root to run any unit tests you have created.
iota move test
If you haven't added any tests, you should see the following output.
INCLUDING DEPENDENCY Iota
INCLUDING DEPENDENCY MoveStdlib
BUILDING first_package
Running Move unit tests
Test result: OK. Total tests: 0; passed: 0; failed: 0
Add Tests
You can add your first unit test by copying the following public test function and adding it to the first_package
file.
#[test]
public fun test_sword() {
// Create a dummy TxContext for testing.
let mut ctx = tx_context::dummy();
// Create a sword.
let sword = Sword {
id: object::new(&mut ctx),
magic: 42,
strength: 7,
};
// Check if accessor functions return correct values.
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
// Create a dummy address and transfer the sword.
let dummy_address = @0xCAFE;
transfer::transfer(sword, dummy_address);
}
The unit test function test_sword()
will:
- Create a dummy instance of the
TxContext
struct and assign it toctx
. - Create a sword object that uses
ctx
to create a unique identifier (id
), and assign42
to themagic
parameter, and7
tostrength
. - Call the
magic
andstrength
accessor functions to verify that they return correct values.
The function passes the dummy context, ctx
, to the object::new
function as a mutable reference argument (&mut
), but passes sword
to its accessor functions as a read-only reference argument, &sword
.
Now that you have a test function, run the test command again:
iota move test
Debugging Tests
If you run the iota move test
command, you might receive the following error message instead of the test results:
error[E06001]: unused value without 'drop'
┌─ sources/first_package.move:55:65
│
4 │ public struct Sword has key, store {
│ ----- To satisfy the constraint, the 'drop' ability would need to be added here
·
48 │ let sword = Sword {
│ ----- The local variable 'sword' still contains a value. The value does not have the 'drop' ability and must be consumed before the function returns
│ ╭─────────────────────'
49 │ │ id: object::new(&mut ctx),
50 │ │ magic: 42,
51 │ │ strength: 7,
52 │ │ };
│ ╰─────────' The type 'my_first_package::first_package::Sword' does not have the ability 'drop'
· │
55 │ assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
│
The compilation error provides all the necessary information to help you debug your module.
Move has many features to ensure your code is safe. In this case, the Sword
struct represents a game asset that digitally mimics a real-world item. Much like a real sword, it cannot simply disappear. Since the Sword
struct doesn't have the drop
ability, it has to be consumed before the function returns. However, since the sword
mimics a real-world item, you don't want to allow it to disappear.
Instead, you can fix the compilation error by adequately disposing of the sword
. Add the following after the function's !assert
call to transfer the sword
to a freshly created dummy address:
// Create a dummy address and transfer the sword.
let dummy_address = @0xCAFE;
transfer::transfer(sword, dummy_address);
Run the test command again. Now the output shows a single successful test has run:
INCLUDING DEPENDENCY Iota
INCLUDING DEPENDENCY MoveStdlib
BUILDING my_first_package
Running Move unit tests
[ PASS ] 0x0::first_package::test_sword
Test result: OK. Total tests: 1; passed: 1; failed: 0
Use a filter string to run only a matching subset of the unit tests. With a filter string provided, the iota move test
checks the fully qualified (<address>::<module_name>::<fn_name>
) name for a match.
Run a Subset of Tests
You can run a subset of the tests in your package that match a given string by adding said string at the end of the iota move test
command:
iota move test sword
iota move test
will check the fully qualified name (<address>::<module_name>::<fn_name>
) for matches. The previous command runs all tests whose name contains sword
.
More Options
You can use the following command to see all the available options for the test command:
iota move test -h
- Use
iota::test_scenario
to mimic multi-transaction, multi-sender test scenarios. - Use the
iota::test_utils
module for better test error messages viaassert_eq
, debug printing viaprint
, and test-only destruction viadestroy
. - Use
iota move test --coverage
to compute code coverage information for your tests, andiota move coverage source --module <name>
to see uncovered lines highlighted in red. Push coverage all the way to 100% if feasible.
IOTA-specific testing
Although you can test a great deal of your contract using the default Move testing framework, you should make sure that you also test code that is specific to IOTA.
Testing Transactions
Move calls in IOTA are encapsulated in transactions. You can use the iota::test_scenario
to test the interactions between multiple transactions within a single test. For example, you could create an object with one transaction and transfer it to another.
The test_scenario
module allows you to emulate a series of IOTA transactions. You can even assign a different user to each transaction.
Instantiate a Scenario
You can use the test_scenario::begin
function to create an instance of Scenario
.
The test_scenario::begin
function takes an address as an argument, which will be used as the user executing the transaction.
Add More Transactions
The Scenario
instance will emulate the IOTA object storage with an object pool for every address. Once you have instantiated the Scenario
with the first transaction, you can use the test_scenario::next_tx
function to execute subsequent transactions. You will need to pass the current Scenario
instance as the first argument, as well as an address for the test user sending the transaction.
You should update the first_package.move
file to include entry functions callable from IOTA that implement sword
creation and transfer. You can add these after the accessor functions.
// === Utility Functions ===
/// Creates and immediately transfers a sword to a recipient
public fun create_sword(magic: u64, strength: u64, recipient: address, ctx: &mut TxContext) {
// Create new sword
let sword = Sword {
id: object::new(ctx),
magic: magic,
strength: strength,
};
// Transfer to recipient
transfer::transfer(sword, recipient);
}
/// Transfers an existing sword to a new owner
public fun sword_transfer(sword: Sword, recipient: address, _ctx: &mut TxContext) {
transfer::public_transfer(sword, recipient);
}
With this code, you have enabled creating and transferring a sword
. Since these functions use IOTA's TxContext
and Transfer
, you should use the test_scenario
's multi-transaction capability to test these properly. You should add the following test to the first_package.move
file:
// === Tests ===
#[test_only]
use iota::test_scenario as ts; // Test scenario utilities
#[test_only]
use iota::test_utils; // Additional test helpers
// Test addresses
#[test_only]
const ADMIN: address = @0xAD;
#[test_only]
const ALICE: address = @0xA;
#[test_only]
const BOB: address = @0xB;
#[test]
public fun test_sword() {
// Create a dummy TxContext for testing.
let mut ctx = tx_context::dummy();
// Create a sword.
let sword = Sword {
id: object::new(&mut ctx),
magic: 42,
strength: 7,
};
// Check if accessor functions return correct values.
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
// Create a dummy address and transfer the sword.
let dummy_address = @0xCAFE;
transfer::transfer(sword, dummy_address);
}
#[test]
public fun test_module_init() {
let mut ts = ts::begin(ADMIN);
// first transaction to emulate module initialization.
{
ts::next_tx(&mut ts, ADMIN);
init(ts::ctx(&mut ts));
};
// second transaction to check if the forge has been created
// and has initial value of zero swords created
{
ts::next_tx(&mut ts, ADMIN);
// extract the Forge object
let forge: Forge = ts::take_from_sender(&ts);
// verify number of created swords
assert!(swords_created(&forge) == 0, 1);
// return the Forge object to the object pool
ts::return_to_sender(&ts, forge);
};
ts::end(ts);
}
#[test]
fun test_sword_transactions() {
let mut ts = ts::begin(ADMIN);
// first transaction to emulate module initialization
{
ts::next_tx(&mut ts, ADMIN);
init(ts::ctx(&mut ts));
};
// second transaction executed by admin to create the sword
{
ts::next_tx(&mut ts, ADMIN);
let mut forge: Forge = ts::take_from_sender(&ts);
// create the sword and transfer it to the initial owner
let sword = new_sword(&mut forge, 42, 7, ts::ctx(&mut ts));
transfer::public_transfer(sword, ALICE);
ts::return_to_sender(&ts, forge);
};
// third transaction executed by the initial sword owner
{
ts::next_tx(&mut ts, ALICE);
// extract the sword owned by the initial owner
let sword: Sword = ts::take_from_sender(&ts);
// transfer the sword to the final owner
transfer::public_transfer(sword, BOB);
};
// fourth transaction executed by the final sword owner
{
ts::next_tx(&mut ts, BOB);
// extract the sword owned by the final owner
let sword: Sword = ts::take_from_sender(&ts);
// verify that the sword has expected properties
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
// return the sword to the object pool (it cannot be dropped)
ts::return_to_sender(&ts, sword)
};
ts::end(ts);
}
Configuration Object Creation
The create_config
function allows creating reusable configuration objects that can be shared across transactions:
/// Creates a new configuration object
public fun create_config(value: u64, ctx: &mut TxContext): Config {
Config {
id: object::new(ctx),
value: value,
}
}
Let's break it down by steps so you understand how the test_scenario
helpers work in a realistic multi-transaction flow:
1. Create User Addresses
First, define three test addresses to represent different users in your scenario: an admin: ADMIN, an initial sword owner: ALICE, and a final sword owner: BOB.
// === Tests ===
#[test_only]
use iota::test_scenario as ts; // Test scenario utilities
#[test_only]
use iota::test_utils; // Additional test helpers
// Test addresses
#[test_only]
const ADMIN: address = @0xAD;
#[test_only]
const ALICE: address = @0xA;
#[test_only]
const BOB: address = @0xB;
2. Start the Scenario
Create a Scenario by calling test_scenario::begin()
. Pass the admin
address as the sender of the first transaction.
You can then call the init
function to simulate module initialization logic during the first transaction.
let mut ts = ts::begin(ADMIN);
// first transaction to emulate module initialization.
{
ts::next_tx(&mut ts, ADMIN);
init(ts::ctx(&mut ts));
};
3. Admin Creates a Sword and Transfers It
After the module is initialized, the admin
runs a transaction to create a new sword.
Use test_scenario::next_tx
to advance to the next transaction in the scenario and execute it as the admin
.
In this example, the admin
uses the new_sword
function to create a sword using a Forge
object and then transfers it to the initial_owner
.
// second transaction executed by admin to create the sword
{
ts::next_tx(&mut ts, ADMIN);
let mut forge: Forge = ts::take_from_sender(&ts);
// create the sword and transfer it to the initial owner
let sword = new_sword(&mut forge, 42, 7, ts::ctx(&mut ts));
transfer::public_transfer(sword, ALICE);
ts::return_to_sender(&ts, forge);
};
4. Initial Owner Transfers the Sword
Next, the initial_owner
retrieves the sword using take_from_sender
.
They then transfer the sword to the final_owner
.
This works because test_scenario
keeps track of objects created and transferred in earlier transactions.
// third transaction executed by the initial sword owner
{
ts::next_tx(&mut ts, ALICE);
// extract the sword owned by the initial owner
let sword: Sword = ts::take_from_sender(&ts);
// transfer the sword to the final owner
transfer::public_transfer(sword, BOB);
};
In test_scenario
, transaction effects (such as creating or transferring an object) are only available to retrieve in the next transaction.
For example, if the second transaction in a scenario created a sword
and transferred it to the administrator's address, it would only become available for retrieval from the administrator's address (via test_scenario
, take_from_sender
, or take_from_address
) in the third transaction.
If needed, you can also use take_from_address
to fetch an object from a specific address instead of the current sender:
let sword = test_scenario::take_from_address<Sword>(scenario, initial_owner);
5. Final Owner Verifies and Cleans Up
In the final transaction, the final_owner
retrieves the sword to verify its properties.
When finished, return the sword to the object pool or destroy it to keep the test state clean.
// fourth transaction executed by the final sword owner
{
ts::next_tx(&mut ts, BOB);
// extract the sword owned by the final owner
let sword: Sword = ts::take_from_sender(&ts);
// verify that the sword has expected properties
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
// return the sword to the object pool (it cannot be dropped)
ts::return_to_sender(&ts, sword)
};
ts::end(ts);
Using iota::test_utils
The iota::test_utils
module provides useful helpers for assertions and cleanup.
This makes your test checks easier to read and maintain.
Key Functions:
assert_eq<T>(t1: T, t2: T)
Fails the test if t1 and t2 are not equal.
assert_same_elems<T>(v1: vector<T>, v2: vector<T>)
Checks that two vectors contain the same elements, regardless of order.
destroy<T>(x: T)
Destroys an object when you no longer need it.
Example:
#[test]
fun test_assert_utils() {
// Test equality assertions
test_utils::assert_eq(10, 10);
// Test vector equality
let v1 = vector[1, 2, 3];
let v2 = vector[3, 2, 1];
test_utils::assert_same_elems(v1, v2);
// Test object destruction
let sword = Sword {
id: object::new(&mut tx_context::dummy()),
magic: 42,
strength: 7,
};
test_utils::destroy(sword);
}
Example: Complete Multi-Transaction Move Contract And Test coverage
Putting it all together, the full first_package::my_module
Contract And Test coverage
function looks like this:
Includes test_scenario and test_utils modules:-
// Copyright (c) Mysten Labs, Inc.
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
// Declare the module with package name and module name
module first_package::my_module{
// Sword struct represents a magical sword object
// - 'key' ability allows it to be stored as a top-level object
// - 'store' ability allows it to be stored inside other structs
public struct Sword has key, store {
id: UID, // Unique identifier for the sword object
magic: u64, // Magic power level of the sword
strength: u64, // strength of the sword
}
public struct Forge has key {
id: UID,
swords_created: u64,
}
/// Module initializer to be executed when this module is published
fun init(ctx: &mut TxContext) {
let admin = Forge {
id: object::new(ctx),
swords_created: 0,
};
// transfer the forge object to the module/package publisher
transfer::transfer(admin, tx_context::sender(ctx));
}
// === Accessors ===
// These provide read-only access to struct fields
// Returns the magic power of a sword
public fun magic(self: &Sword): u64 {
self.magic
}
// Returns the physical strength of a sword
public fun strength(self: &Sword): u64 {
self.strength
}
// Returns how many swords a forge has created
public fun swords_created(self: &Forge): u64 {
self.swords_created
}
/// Constructor for creating swords
/// Creates a new sword and increments the forge's creation counter
public fun new_sword(forge: &mut Forge, magic: u64, strength: u64, ctx: &mut TxContext): Sword {
forge.swords_created = forge.swords_created + 1;
Sword {
id: object::new(ctx),
magic: magic,
strength: strength,
}
}
// Config struct for storing configuration data
public struct Config has key {
id: UID,
value: u64,
}
// === Utility Functions ===
/// Creates and immediately transfers a sword to a recipient
public fun create_sword(magic: u64, strength: u64, recipient: address, ctx: &mut TxContext) {
// Create new sword
let sword = Sword {
id: object::new(ctx),
magic: magic,
strength: strength,
};
// Transfer to recipient
transfer::transfer(sword, recipient);
}
/// Transfers an existing sword to a new owner
public fun sword_transfer(sword: Sword, recipient: address, _ctx: &mut TxContext) {
transfer::public_transfer(sword, recipient);
}
/// Creates a new configuration object
public fun create_config(value: u64, ctx: &mut TxContext): Config {
Config {
id: object::new(ctx),
value: value,
}
}
// === Tests ===
#[test_only]
use iota::test_scenario as ts; // Test scenario utilities
#[test_only]
use iota::test_utils; // Additional test helpers
// Test addresses
#[test_only]
const ADMIN: address = @0xAD;
#[test_only]
const ALICE: address = @0xA;
#[test_only]
const BOB: address = @0xB;
#[test]
public fun test_sword() {
// Create a dummy TxContext for testing.
let mut ctx = tx_context::dummy();
// Create a sword.
let sword = Sword {
id: object::new(&mut ctx),
magic: 42,
strength: 7,
};
// Check if accessor functions return correct values.
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
// Create a dummy address and transfer the sword.
let dummy_address = @0xCAFE;
transfer::transfer(sword, dummy_address);
}
#[test]
public fun test_module_init() {
let mut ts = ts::begin(ADMIN);
// first transaction to emulate module initialization.
{
ts::next_tx(&mut ts, ADMIN);
init(ts::ctx(&mut ts));
};
// second transaction to check if the forge has been created
// and has initial value of zero swords created
{
ts::next_tx(&mut ts, ADMIN);
// extract the Forge object
let forge: Forge = ts::take_from_sender(&ts);
// verify number of created swords
assert!(swords_created(&forge) == 0, 1);
// return the Forge object to the object pool
ts::return_to_sender(&ts, forge);
};
ts::end(ts);
}
#[test]
fun test_sword_transactions() {
let mut ts = ts::begin(ADMIN);
// first transaction to emulate module initialization
{
ts::next_tx(&mut ts, ADMIN);
init(ts::ctx(&mut ts));
};
// second transaction executed by admin to create the sword
{
ts::next_tx(&mut ts, ADMIN);
let mut forge: Forge = ts::take_from_sender(&ts);
// create the sword and transfer it to the initial owner
let sword = new_sword(&mut forge, 42, 7, ts::ctx(&mut ts));
transfer::public_transfer(sword, ALICE);
ts::return_to_sender(&ts, forge);
};
// third transaction executed by the initial sword owner
{
ts::next_tx(&mut ts, ALICE);
// extract the sword owned by the initial owner
let sword: Sword = ts::take_from_sender(&ts);
// transfer the sword to the final owner
transfer::public_transfer(sword, BOB);
};
// fourth transaction executed by the final sword owner
{
ts::next_tx(&mut ts, BOB);
// extract the sword owned by the final owner
let sword: Sword = ts::take_from_sender(&ts);
// verify that the sword has expected properties
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
// return the sword to the object pool (it cannot be dropped)
ts::return_to_sender(&ts, sword)
};
ts::end(ts);
}
#[test]
fun test_assert_utils() {
// Test equality assertions
test_utils::assert_eq(10, 10);
// Test vector equality
let v1 = vector[1, 2, 3];
let v2 = vector[3, 2, 1];
test_utils::assert_same_elems(v1, v2);
// Test object destruction
let sword = Sword {
id: object::new(&mut tx_context::dummy()),
magic: 42,
strength: 7,
};
test_utils::destroy(sword);
}
#[test]
fun test_scenario_advanced() {
let mut ts = ts::begin(ADMIN);
// 1. Initialize module
{
init(ts::ctx(&mut ts));
let forge = Forge {
id: object::new(ts::ctx(&mut ts)),
swords_created: 0,
};
transfer::transfer(forge, ADMIN);
};
// 2. Create sword
let effects = ts::next_tx(&mut ts, ADMIN);
{
let mut forge = ts::take_from_sender<Forge>(&ts);
let sword = Sword {
id: object::new(ts::ctx(&mut ts)),
magic: 42,
strength: 7,
};
forge.swords_created = forge.swords_created + 1;
transfer::transfer(sword, ALICE);
ts::return_to_sender(&ts, forge);
// Test effects inspection
assert!(ts::created(&effects).length() > 0, 1);
};
// 3. Transfer sword
ts::next_tx(&mut ts, ALICE);
{
let sword = ts::take_from_sender<Sword>(&ts);
ts::return_to_sender(&ts, sword); // Return instead of transfer for demo
};
// 4. Test shared object
ts::next_tx(&mut ts, ADMIN);
{
let config = create_config(500, ts::ctx(&mut ts));
transfer::share_object(config);
};
// 5. Access shared object
ts::next_tx(&mut ts, ALICE);
{
let config = ts::take_shared<Config>(&ts);
assert!(config.value == 500, 1);
ts::return_shared(config);
};
// 6. Test epoch advancement
ts::later_epoch(&mut ts, 1000, BOB);
{
let ctx = ts::ctx(&mut ts);
assert!(tx_context::epoch(ctx) > 0, 1);
};
ts::end(ts);
}
#[test]
fun test_receiving_tickets() {
let mut ts = ts::begin(ADMIN);
// 1. Create sword in admin's inventory
{
let sword = Sword {
id: object::new(ts::ctx(&mut ts)),
magic: 42,
strength: 7,
};
transfer::transfer(sword, ADMIN);
};
// 2. Admin creates receiving ticket
ts::next_tx(&mut ts, ADMIN);
{
let sword_id = ts::most_recent_id_for_sender<Sword>(&ts).destroy_some();
let receiving = ts::receiving_ticket_by_id<Sword>(sword_id);
// Normally you'd transfer this to another object
ts::return_receiving_ticket(receiving);
};
ts::end(ts);
}
#[test]
fun test_immutable_objects() {
let mut ts = ts::begin(ADMIN);
// 1. Create and freeze sword
{
let sword = Sword {
id: object::new(ts::ctx(&mut ts)),
magic: 42,
strength: 7,
};
transfer::freeze_object(sword);
};
// 2. Access immutable object
ts::next_tx(&mut ts, ALICE);
{
let sword = ts::take_immutable<Sword>(&ts);
assert!(sword.magic == 42, 1);
ts::return_immutable(sword);
};
ts::end(ts);
}
#[test]
fun test_address_operations() {
let mut ts = ts::begin(ADMIN);
// 1. Create and transfer sword
{
let sword = Sword {
id: object::new(ts::ctx(&mut ts)),
magic: 42,
strength: 7,
};
transfer::transfer(sword, ALICE);
};
// 2. Access from specific address
ts::next_tx(&mut ts, BOB);
{
// Directly take from Alice's address
let sword = ts::take_from_address<Sword>(&ts, ALICE);
assert!(ts::was_taken_from_address(ALICE, object::id(&sword)), 1);
// Verify IDs list contains our sword
let ids = ts::ids_for_address<Sword>(ALICE);
assert!(vector::length(&ids) == 1, 1);
ts::return_to_address(ALICE, sword);
};
ts::end(ts);
}
}
Run All Tests
iota move build
iota move test
Output of the test command:
INCLUDING DEPENDENCY Iota
INCLUDING DEPENDENCY MoveStdlib
BUILDING first_package
Running Move unit tests
[ PASS ] first_package::my_module::test_address_operations
[ PASS ] first_package::my_module::test_assert_utils
[ PASS ] first_package::my_module::test_immutable_objects
[ PASS ] first_package::my_module::test_module_init
[ PASS ] first_package::my_module::test_receiving_tickets
[ PASS ] first_package::my_module::test_scenario_advanced
[ PASS ] first_package::my_module::test_sword
[ PASS ] first_package::my_module::test_sword_transactions
Test result: OK. Total tests: 8; passed: 8; failed: 0