Skip to main content

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);
}

The unit test function test_sword() will:

  1. Create a dummy instance of the TxContext struct and assign it to ctx.
  2. Create a sword object that uses ctx to create a unique identifier (id), and assign 42 to the magic parameter, and 7 to strength.
  3. Call the magic and strength 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
TIP

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
Cheat Sheet
  • Use iota::test_scenario to mimic multi-transaction, multi-sender test scenarios.
  • Use the iota::test_utils module for better test error messages via assert_eq, debug printing via print, and test-only destruction via destroy.
  • Use iota move test --coverage to compute code coverage information for your tests, and iota 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.

public fun create_sword(magic: u64, strength: u64, recipient: address, ctx: &mut TxContext) {
// Create a sword.
let sword = Sword {
id: object::new(ctx),
magic: magic,
strength: strength,
};
// Transfer the sword.
transfer::transfer(sword, recipient);
}

public fun sword_transfer(sword: Sword, recipient: address, _ctx: &mut TxContext) {
// Transfer the sword.
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:

#[test]
fun test_sword_transactions() {
use iota::test_scenario;

// Create test addresses representing users.
let admin = @0xBABE;
let initial_owner = @0xCAFE;
let final_owner = @0xFACE;

// First transaction to emulate module initialization.
let mut scenario_val = test_scenario::begin(admin);
let scenario = &mut scenario_val;
{
init(test_scenario::ctx(scenario));
};
// Second transaction executed by admin to create a sword.
test_scenario::next_tx(scenario, admin);
{
// Create the sword and transfer it to the initial owner.
create_sword(42, 7, initial_owner, test_scenario::ctx(scenario));
};
// Third transaction executed by the initial sword owner.
test_scenario::next_tx(scenario, initial_owner);
{
// Extract the sword owned by the initial owner.
let sword = test_scenario::take_from_sender<Sword>(scenario);
// Transfer the sword to the final owner.
sword_transfer(sword, final_owner, test_scenario::ctx(scenario))
};
// Fourth transaction executed by the final sword owner.
test_scenario::next_tx(scenario, final_owner);
{
// Extract the sword owned by the final owner.
let sword = test_scenario::take_from_sender<Sword>(scenario);
// Verify that the sword has expected properties.
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
// Return the sword to the object pool
test_scenario::return_to_sender(scenario, sword)
// or uncomment the line below to destroy the sword instead.
// test_utils::destroy(sword)
};
test_scenario::end(scenario_val);
}

This test function is more complex than the previous example, so 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, an initial sword owner, and a final sword owner.

let admin = @0xBABE;
let initial_owner = @0xCAFE;
let final_owner = @0xFACE;

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 scenario_val = test_scenario::begin(admin);
let scenario = &mut scenario_val;
{
init(test_scenario::ctx(scenario));
};

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.

test_scenario::next_tx(scenario, admin);
{
// Take the Forge object from the sender’s inventory.
let mut forge = test_scenario::take_from_sender<Forge>(scenario);

// Create the sword and transfer it.
let sword = new_sword(&mut forge, 42, 7, test_scenario::ctx(scenario));
transfer::public_transfer(sword, initial_owner);

// Return the Forge object to the sender’s inventory.
test_scenario::return_to_sender(scenario, 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.

test_scenario::next_tx(scenario, initial_owner);
{
// extract the sword owned by the initial owner
let sword = test_scenario::take_from_sender<Sword>(scenario);
// transfer the sword to the final owner
sword_transfer(sword, final_owner, test_scenario::ctx(scenario))
};

Note:
In test_scenario, transaction effects (such as creating or transferring an object) are only available to retrieve in the next 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);
tip

Transaction effects, such as object creation and transfer, become visible only after a given transaction completes. For example, if the second transaction in the running example 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 functions) in the third transaction.

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.

test_scenario::next_tx(scenario, final_owner);
{
// Retrieve the sword owned by the final owner.
let sword = test_scenario::take_from_sender<Sword>(scenario);

// Verify that the sword has the correct properties.
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);

// Return the sword or destroy it to clean up.
test_scenario::return_to_sender(scenario, sword);
// Or:
// test_utils::destroy(sword);
};
test_scenario::end(scenario_val);

Example: Complete Multi-Transaction Test

Putting it all together, the full test_sword_transactions function looks like this:

#[test]
fun test_sword_transactions() {
use iota::test_scenario;

let admin = @0xBABE;
let initial_owner = @0xCAFE;
let final_owner = @0xFACE;

let mut scenario_val = test_scenario::begin(admin);
let scenario = &mut scenario_val;
{
init(test_scenario::ctx(scenario));
};
test_scenario::next_tx(scenario, admin);
{
let mut forge = test_scenario::take_from_sender<Forge>(scenario);
let sword = new_sword(&mut forge, 42, 7, test_scenario::ctx(scenario));
transfer::public_transfer(sword, initial_owner);
test_scenario::return_to_sender(scenario, forge);
};
test_scenario::next_tx(scenario, initial_owner);
{
let sword = test_scenario::take_from_sender<Sword>(scenario);
sword_transfer(sword, final_owner, test_scenario::ctx(scenario));
};
test_scenario::next_tx(scenario, final_owner);
{
let sword = test_scenario::take_from_sender<Sword>(scenario);
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
test_scenario::return_to_sender(scenario, sword);
};
test_scenario::end(scenario_val);
}

Another Example for Complete Test Scenario and other test_scenario functions

#[test]
fun test_comprehensive_scenario() {
// Import testing modules
use iota::test_scenario;
use iota::test_utils;

// ==============================================
// 1. SETUP PHASE
// ==============================================

// Define test addresses representing different users
let admin = @0xADMIN; // Administrator address
let alice = @0xALICE; // First user address
let bob = @0xBOB; // Second user address

// Define test object IDs
let forge_id = @0xFORGE; // ID for forge object
let sword_id = @0xSWORD; // ID for sword object

// ==============================================
// 2. SCENARIO INITIALIZATION
// ==============================================

// Begin test scenario with admin as first sender
// This creates a new Scenario instance and initial transaction context
let scenario_val = test_scenario::begin(admin);
let scenario = &mut scenario_val;

// ==============================================
// 3. FIRST TRANSACTION: MODULE INITIALIZATION
// ==============================================
{
// Initialize module with admin context
init(test_scenario::ctx(scenario));

// Create initial forge object that will be used to create swords
let forge = Forge {
id: object::id_from_address(forge_id),
swords_created: 0
};

// Transfer forge to admin's inventory for later use
transfer::transfer(forge, admin);
};

// ==============================================
// 4. SECOND TRANSACTION: OBJECT CREATION
// ==============================================
// Advance to next transaction with admin as sender
// This commits effects from first transaction
test_scenario::next_tx(scenario, admin);
{
// Take forge object from admin's inventory
// Must use take_from_sender since admin transferred it to themselves
let mut forge = test_scenario::take_from_sender<Forge>(scenario);

// Create new sword using forge
let sword = Sword {
id: object::id_from_address(sword_id),
magic: 42,
strength: 7,
};

// Update forge state
forge.swords_created = forge.swords_created + 1;

// Transfer sword to Alice
transfer::transfer(sword, alice);

// Return forge to admin's inventory for future transactions
test_scenario::return_to_sender(scenario, forge);
};

// ==============================================
// 5. THIRD TRANSACTION: OBJECT TRANSFER
// ==============================================
// Advance to next transaction with Alice as sender
test_scenario::next_tx(scenario, alice);
{
// Take sword from Alice's inventory
let sword = test_scenario::take_from_sender<Sword>(scenario);

// Verify sword properties using test_utils assertions
test_utils::assert_eq(sword.magic, 42);
test_utils::assert_eq(sword.strength, 7);

// Transfer sword to Bob
transfer::transfer(sword, bob);
};

// ==============================================
// 6. FOURTH TRANSACTION: OBJECT MODIFICATION
// ==============================================
// Advance to next transaction with Bob as sender
test_scenario::next_tx(scenario, bob);
{
// Take sword from Bob's inventory using alternative method
// Demonstrates take_from_address instead of take_from_sender
let sword = test_scenario::take_from_address<Sword>(scenario, bob);

// Create upgrade receipt as one-time witness
let receipt = test_utils::create_one_time_witness<UpgradeReceipt>();

// Upgrade sword using receipt
sword::upgrade(&mut sword, receipt);

// Verify upgrade was successful
test_utils::assert_eq(sword.magic, 100);

// Return sword to Bob's inventory
test_scenario::return_to_sender(scenario, sword);
};

// ==============================================
// 7. FIFTH TRANSACTION: SHARED OBJECT TESTING
// ==============================================
// Advance to next transaction with admin as sender
test_scenario::next_tx(scenario, admin);
{
// Create and share a global config object
let config = Config {
id: object::new(test_scenario::ctx(scenario)),
value: 500
};
transfer::share_object(config);
};

// ==============================================
// 8. SIXTH TRANSACTION: ACCESS SHARED OBJECT
// ==============================================
// Advance to next transaction with Alice as sender
test_scenario::next_tx(scenario, alice);
{
// Take shared config from global inventory
let config = test_scenario::take_shared<Config>(scenario);

// Modify shared config
config.value = 600;

// Return shared config
test_scenario::return_shared(config);
};

// ==============================================
// 9. SEVENTH TRANSACTION: EPOCH ADVANCEMENT
// ==============================================
// Advance epoch by 1000ms with Bob as sender
test_scenario::later_epoch(scenario, 1000, bob);
{
// Verify epoch advanced
let current_epoch = tx_context::epoch(test_scenario::ctx(scenario));
test_utils::assert(current_epoch > 0, 1);

// Take sword again from Bob's inventory
let sword = test_scenario::take_from_sender<Sword>(scenario);

// Final verification
test_utils::assert_eq(sword.magic, 100);

// Clean up by destroying sword (alternative to returning)
test_utils::destroy(sword);
};

// ==============================================
// 10. SCENARIO CLEANUP
// ==============================================

// End scenario and capture final transaction effects
let effects = test_scenario::end(scenario_val);

// Verify expected effects
test_utils::assert_eq(
test_scenario::num_user_events(&effects),
0,
"No user events expected"
);
}

Additional test_scenario API Details. The iota::test_scenario module provides extra utilities to make your test scenarios more realistic.

take_from_address

take_from_address<T>(scenario: &Scenario, addr: address): T

Use this function to retrieve an object owned by a specific address, instead of the current sender.

later_epoch Use this function to simulate time passing by advancing the epoch in your scenario.

test_scenario::later_epoch(&mut scenario, 1000, admin);

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() {
use iota::test_utils;

let a = 10;
let b = 10;
test_utils::assert_eq(a, b);

let v1 = vector[1, 2, 3];
let v2 = vector[3, 2, 1];
test_utils::assert_same_elems(v1, v2);
}

References:

Summary

These modules help you write realistic, robust multi-transaction tests for your IOTA smart contracts. Use them together to cover advanced scenarios and keep your tests clear and maintainable.