Skip to content

Commit c2e7f04

Browse files
fix(docs): added docs and examples for iota::test_scenario and utils … (#7815)
# Description of change This PR brings up the addition of the example for test_scenario and utils modules ## Links to any relevant issues fixes #7780. ## How the change has been tested - [x] Basic tests (linting, compilation, formatting, unit/integration tests) - [ ] Patch-specific tests (correctness, functionality coverage) - [ ] I have added tests that prove my fix is effective or that my feature works - [x] I have checked that new and existing unit tests pass locally with my changes --------- Co-authored-by: Dr-Electron <[email protected]>
1 parent ac465ec commit c2e7f04

File tree

3 files changed

+434
-338
lines changed

3 files changed

+434
-338
lines changed

docs/content/developer/getting-started/build-test.mdx

Lines changed: 111 additions & 225 deletions
Original file line numberDiff line numberDiff line change
@@ -65,22 +65,7 @@ Test result: OK. Total tests: 0; passed: 0; failed: 0
6565

6666
You can add your first unit test by copying the following public test function and adding it to the `first_package` file.
6767

68-
```move
69-
#[test]
70-
public fun test_sword() {
71-
// Create a dummy TxContext for testing.
72-
let mut ctx = tx_context::dummy();
73-
74-
// Create a sword.
75-
let sword = Sword {
76-
id: object::new(&mut ctx),
77-
magic: 42,
78-
strength: 7,
79-
};
80-
81-
// Check if accessor functions return correct values.
82-
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
83-
}
68+
```move file=<rootDir>/examples/move/first_package/sources/first_package.move#L109-L127
8469
```
8570

8671
The unit test function `test_sword()` will:
@@ -127,10 +112,7 @@ Move has many features to ensure your code is safe. In this case, the `Sword` st
127112

128113
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:
129114

130-
```move
131-
// Create a dummy address and transfer the sword.
132-
let dummy_address = @0xCAFE;
133-
transfer::transfer(sword, dummy_address);
115+
```move file=<rootDir>/examples/move/first_package/sources/first_package.move#L124-L126
134116
```
135117

136118
Run the test command again. Now the output shows a single successful test has run:
@@ -186,248 +168,152 @@ The `test_scenario` module allows you to emulate a series of IOTA transactions.
186168

187169
#### Instantiate a Scenario
188170

189-
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.
171+
You can use the `test_scenario::begin` function to create an instance of `Scenario`.
172+
173+
The `test_scenario::begin` function takes an address as an argument, which will be used as the user executing the transaction.
190174

191175
#### Add More Transactions
192176

193-
The `Scenario` instance will emulate the IOTA object storage with an object pool for every address. Once you have [instantiated the `Scenario`](#instantiate-a-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.
177+
The `Scenario` instance will emulate the IOTA object storage with an object pool for every address. Once you have [`instantiated the Scenario`](#instantiate-a-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.
194178

195179
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.
196180

197181

198-
```move
199-
public fun create_sword(magic: u64, strength: u64, recipient: address, ctx: &mut TxContext) {
200-
// Create a sword.
201-
let sword = Sword {
202-
id: object::new(ctx),
203-
magic: magic,
204-
strength: strength,
205-
};
206-
// Transfer the sword.
207-
transfer::transfer(sword, recipient);
208-
}
209-
210-
public fun sword_transfer(sword: Sword, recipient: address, _ctx: &mut TxContext) {
211-
// Transfer the sword.
212-
transfer::public_transfer(sword, recipient);
213-
}
182+
```move file=<rootDir>/examples/move/first_package/sources/first_package.move#L68-L85
214183
```
215184

216185
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:
217186

218-
```move
219-
#[test]
220-
fun test_sword_transactions() {
221-
use iota::test_scenario;
222-
223-
// Create test addresses representing users.
224-
let admin = @0xBABE;
225-
let initial_owner = @0xCAFE;
226-
let final_owner = @0xFACE;
227-
228-
// First transaction to emulate module initialization.
229-
let mut scenario_val = test_scenario::begin(admin);
230-
let scenario = &mut scenario_val;
231-
{
232-
init(test_scenario::ctx(scenario));
233-
};
234-
// Second transaction executed by admin to create a sword.
235-
test_scenario::next_tx(scenario, admin);
236-
{
237-
// Create the sword and transfer it to the initial owner.
238-
create_sword(42, 7, initial_owner, test_scenario::ctx(scenario));
239-
};
240-
// Third transaction executed by the initial sword owner.
241-
test_scenario::next_tx(scenario, initial_owner);
242-
{
243-
// Extract the sword owned by the initial owner.
244-
let sword = test_scenario::take_from_sender<Sword>(scenario);
245-
// Transfer the sword to the final owner.
246-
sword_transfer(sword, final_owner, test_scenario::ctx(scenario))
247-
};
248-
// Fourth transaction executed by the final sword owner.
249-
test_scenario::next_tx(scenario, final_owner);
250-
{
251-
// Extract the sword owned by the final owner.
252-
let sword = test_scenario::take_from_sender<Sword>(scenario);
253-
// Verify that the sword has expected properties.
254-
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
255-
// Return the sword to the object pool
256-
test_scenario::return_to_sender(scenario, sword)
257-
// or uncomment the line below to destroy the sword instead.
258-
// test_utils::destroy(sword)
259-
};
260-
test_scenario::end(scenario_val);
261-
}
187+
```move file=<rootDir>/examples/move/first_package/sources/first_package.move#L95-L199
262188
```
263189

264-
This test function is more complex than the [previous example](#add-tests), so let's break it down by steps:
190+
### Configuration Object Creation
265191

266-
1. Create test addresses that will be used to represent the users in the scenario. One for the admin, and two users:
192+
The `create_config` function allows creating reusable configuration objects that can be shared across transactions:
267193

268-
```move
269-
let admin = @0xBABE;
270-
let initial_owner = @0xCAFE;
271-
let final_owner = @0xFACE;
272-
```
194+
```move file=<rootDir>/examples/move/first_package/sources/first_package.move#L87-L93
195+
```
273196

274-
2. Create a scenario by calling `test_scenario:begin()`, and passing the `admin` address as a parameter. You can then use the `test_scenario`'s `ctx` to emulate the module's initialization:
197+
Let's break it down by steps so you understand how the `test_scenario` helpers work in a realistic multi-transaction flow:
275198

276-
```move
277-
let scenario_val = test_scenario::begin(admin);
278-
let scenario = &mut scenario_val;
279-
{
280-
init(test_scenario::ctx(scenario));
281-
};
282-
```
283-
284-
3. The `admin` creates a `sword` and sends it to the `initial_owner`. Note that the first call is to `test_scenario::next_tx`, passing the `Scenario` that was instantiated in step `2`:
285-
286-
```move
287-
test_scenario::next_tx(scenario, admin);
288-
{
289-
// create the sword and transfer it to the initial owner
290-
sword_create(42, 7, initial_owner, test_scenario::ctx(scenario));
291-
};
292-
```
293-
294-
4. After advancing the `test_scenario` with `test_scenario::next_tx`, you can emulate the `initial_owner` sending the `sword` to the `final_owner`. However, in standard Move tests, there is no object storage, so it is impossible to retrieve the `sword` created in step `3`. You should use the `test_scenario::take_from_sender` function to extract the `sword`. In this case, the test transfers the object it retrieves from storage to another address:
295-
296-
```move
297-
test_scenario::next_tx(scenario, initial_owner);
298-
{
299-
// extract the sword owned by the initial owner
300-
let sword = test_scenario::take_from_sender<Sword>(scenario);
301-
// transfer the sword to the final owner
302-
sword_transfer(sword, final_owner, test_scenario::ctx(scenario))
303-
};
304-
```
305-
306-
:::tip
307-
308-
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.
309-
310-
:::
311-
312-
5. Finally, the `final_owner` retrieves the `sword` object from storage and verifies it has the expected properties.
313-
314-
```move
315-
test_scenario::next_tx(scenario, final_owner);
316-
{
317-
// extract the sword owned by the final owner
318-
let sword = test_scenario::take_from_sender<Sword>(scenario);
319-
// verify that the sword has expected properties
320-
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
321-
// return the sword to the object pool
322-
test_scenario::return_to_sender(scenario, sword)
323-
// or uncomment the line below to destroy the sword instead
324-
// test_utils::destroy(sword)
325-
};
326-
test_scenario::end(scenario_val);
327-
```
199+
### 1. Create User Addresses
200+
201+
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.
202+
203+
```move file=<rootDir>/examples/move/first_package/sources/first_package.move#L95-L107
204+
```
205+
206+
### 2. Start the Scenario
207+
208+
Create a Scenario by calling `test_scenario::begin()`. Pass the `admin` address as the sender of the first transaction.
209+
You can then call the `init` function to simulate module initialization logic during the first transaction.
210+
211+
```move file=<rootDir>/examples/move/first_package/sources/first_package.move#L131-L137
212+
```
328213

329-
Since [the `sword` cannot simply disappear](#debugging-tests), the function transfers the `sword` object to the fake address using the `test_scenario::return_to_sender` function. If returning the object to a dummy address is not your desired behavior, you can also call the [`test_utils:destroy`](https://github.com/iotaledger/iota/blob/develop/crates/iota-framework/packages/iota-framework/sources/test/test_utils.move) function.
214+
### 3. Admin Creates a Sword and Transfers It
330215

331-
:::tip Know your toolbox
216+
After the module is initialized, the `admin` runs a transaction to create a new sword.
217+
Use `test_scenario::next_tx` to advance to the next transaction in the scenario and execute it as the `admin`.
332218

333-
This guide only covers the basics. You should check out the available functions for both [`test_scenario`](https://github.com/iotaledger/iota/blob/develop/crates/iota-framework/packages/iota-framework/sources/test/test_scenario.move) and [`test_utils`](https://github.com/iotaledger/iota/blob/develop/crates/iota-framework/packages/iota-framework/sources/test/test_utils.move) modules to see everything you can do.
219+
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`.
334220

221+
```move file=<rootDir>/examples/move/first_package/sources/first_package.move#L167-L175
222+
```
223+
224+
### 4. Initial Owner Transfers the Sword
225+
226+
Next, the `initial_owner` retrieves the sword using `take_from_sender`.
227+
They then transfer the sword to the `final_owner`.
228+
This works because `test_scenario` keeps track of objects created and transferred in earlier transactions.
229+
230+
```move file=<rootDir>/examples/move/first_package/sources/first_package.move#L177-L184
231+
```
232+
233+
:::info Transaction Effects
234+
In `test_scenario`, transaction effects (such as creating or transferring an object) are only available to retrieve in the **next** transaction.
235+
236+
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.
237+
238+
If needed, you can also use `take_from_address` to fetch an object from a specific address instead of the current sender:
239+
240+
```move
241+
let sword = test_scenario::take_from_address<Sword>(scenario, initial_owner);
242+
```
335243
:::
336244

337-
If you run the test command again, you should see two successful tests for the module:
245+
### 5. Final Owner Verifies and Cleans Up
338246

339-
```shell
340-
INCLUDING DEPENDENCY Iota
341-
INCLUDING DEPENDENCY MoveStdlib
342-
BUILDING my_first_package
343-
Running Move unit tests
344-
[ PASS ] 0x0::first_package::test_sword
345-
[ PASS ] 0x0::first_package::test_sword_transactions
346-
Test result: OK. Total tests: 2; passed: 2; failed: 0
247+
In the final transaction, the `final_owner` retrieves the sword to verify its properties.
248+
When finished, return the sword to the object pool or destroy it to keep the test state clean.
249+
250+
```move file=<rootDir>/examples/move/first_package/sources/first_package.move#L186-L197
347251
```
348252

349-
### Testing Module Initializers
253+
### Using `iota::test_utils`
350254

351-
IOTA modules can include an [initializer function](create-a-module.mdx#module-initializer) that can only be run when the module is published for the first time. The `iota move` command does not support test publishing, but you can test module initializers using the testing framework to test the `Forge` object is properly created.
255+
The `iota::test_utils` module provides useful helpers for assertions and cleanup.
256+
This makes your test checks easier to read and maintain.
352257

353-
The first thing you should do is add a `new_sword` function that takes a `Forge` as a parameter, and updates its `swords_created` attribute.
258+
#### Key Functions:
354259

355-
:::danger Redundant Code
260+
`assert_eq<T>(t1: T, t2: T)`
261+
Fails the test if t1 and t2 are not equal.
356262

357-
If this were an actual module, you should replace the `create_sword` function with `new_sword`, as they do the same thing. However, to keep the existing tests from failing this guide will keep both functions.
263+
`assert_same_elems<T>(v1: vector<T>, v2: vector<T>)`
264+
Checks that two vectors contain the same elements, regardless of order.
358265

359-
:::
266+
`destroy<T>(x: T)`
267+
Destroys an object when you no longer need it.
360268

361-
```move
362-
/// Constructor for creating swords.
363-
public fun new_sword(forge: &mut Forge, magic: u64, strength: u64, ctx: &mut TxContext): Sword {
364-
// Increment the `swords_created` counter.
365-
forge.swords_created = forge.swords_created + 1;
366-
367-
// Create a sword.
368-
Sword {
369-
id: object::new(ctx),
370-
magic: magic,
371-
strength: strength,
372-
}
373-
}
269+
#### Example:
270+
```move file=<rootDir>/examples/move/first_package/sources/first_package.move#L200-L218
271+
```
272+
273+
### Example: Complete Multi-Transaction Move Contract And Test coverage
274+
275+
Putting it all together, the full `first_package::my_module` Contract And Test coverage
276+
function looks like this:
277+
278+
Includes test_scenario and test_utils modules:-
279+
280+
```move file=<rootDir>/examples/move/first_package/sources/first_package.move#L1-L364
374281
```
375282

376-
You can now add the following code to the end of the module to test the module initializer using the `test_scenario` module by calling the module's `init` function and then asserting the number of swords is zero:
283+
### Run All Tests
377284

378-
```move
379-
#[test]
380-
fun test_sword_transactions() {
381-
use iota::test_scenario;
382-
383-
// Create test addresses representing users.
384-
let admin = @0xBABE;
385-
let initial_owner = @0xCAFE;
386-
let final_owner = @0xFACE;
387-
388-
// First transaction to emulate module initialization.
389-
let mut scenario_val = test_scenario::begin(admin);
390-
let scenario = &mut scenario_val;
391-
{
392-
init(test_scenario::ctx(scenario));
393-
};
394-
// Second transaction executed by admin to create a sword.
395-
test_scenario::next_tx(scenario, admin);
396-
{
397-
let mut forge = test_scenario::take_from_sender<Forge>(scenario);
398-
399-
// Create the sword and transfer it to the initial owner.
400-
let sword = new_sword(&mut forge, 42, 7, test_scenario::ctx(scenario));
401-
transfer::public_transfer(sword, initial_owner);
402-
403-
// Return the forge to the sender.
404-
test_scenario::return_to_sender(scenario, forge);
405-
};
406-
// Third transaction executed by the initial sword owner.
407-
test_scenario::next_tx(scenario, initial_owner);
408-
{
409-
// Extract the sword owned by the initial owner.
410-
let sword = test_scenario::take_from_sender<Sword>(scenario);
411-
// Transfer the sword to the final owner.
412-
sword_transfer(sword, final_owner, test_scenario::ctx(scenario))
413-
};
414-
// Fourth transaction executed by the final sword owner.
415-
test_scenario::next_tx(scenario, final_owner);
416-
{
417-
// Extract the sword owned by the final owner.
418-
let sword = test_scenario::take_from_sender<Sword>(scenario);
419-
// Verify that the sword has expected properties.
420-
assert!(magic(&sword) == 42 && strength(&sword) == 7, 1);
421-
// Return the sword to the object pool
422-
test_scenario::return_to_sender(scenario, sword)
423-
// or uncomment the line below to destroy the sword instead.
424-
// test_utils::destroy(sword)
425-
};
426-
test_scenario::end(scenario_val);
427-
}
285+
1.
286+
```shell
287+
iota move build
428288
```
429289

430-
You can refer to the source code for the package (with all the tests and functions properly adjusted) in the [first_package](https://github.com/iotaledger/iota/blob/develop/examples/move/first_package/sources/first_package.move) module in the `iota/examples` directory.
290+
2.
291+
```shell
292+
iota move test
293+
```
294+
295+
Output of the test command:
296+
297+
```shell
298+
INCLUDING DEPENDENCY Iota
299+
INCLUDING DEPENDENCY MoveStdlib
300+
BUILDING first_package
301+
Running Move unit tests
302+
[ PASS ] first_package::my_module::test_address_operations
303+
[ PASS ] first_package::my_module::test_assert_utils
304+
[ PASS ] first_package::my_module::test_immutable_objects
305+
[ PASS ] first_package::my_module::test_module_init
306+
[ PASS ] first_package::my_module::test_receiving_tickets
307+
[ PASS ] first_package::my_module::test_scenario_advanced
308+
[ PASS ] first_package::my_module::test_sword
309+
[ PASS ] first_package::my_module::test_sword_transactions
310+
Test result: OK. Total tests: 8; passed: 8; failed: 0
311+
```
312+
313+
## References:
314+
315+
- [`test_scenario` source](https://github.com/iotaledger/iota/blob/develop/crates/iota-framework/packages/iota-framework/sources/test/test_scenario.move)
316+
- [`test_utils` source](https://github.com/iotaledger/iota/blob/develop/crates/iota-framework/packages/iota-framework/sources/test/test_utils.move)
431317

432318

433319
<Quiz questions={questions} />

0 commit comments

Comments
 (0)