$npx -y skills add aptos-labs/aptos-agent-skills --skill generate-testsCreates comprehensive test suites for Move contracts with 100% coverage requirement. Triggers on: 'generate tests', 'create tests', 'write test suite', 'test this contract', 'how to test', 'add test coverage', 'write unit tests'.
| 1 | # Generate Tests Skill |
| 2 | |
| 3 | ## Overview |
| 4 | |
| 5 | This skill generates comprehensive test suites for Move contracts with **100% line coverage** requirement. Tests verify: |
| 6 | |
| 7 | - ✅ Happy paths (functionality works) |
| 8 | - ✅ Access control (unauthorized users blocked) |
| 9 | - ✅ Input validation (invalid inputs rejected) |
| 10 | - ✅ Edge cases (boundaries, limits, empty states) |
| 11 | |
| 12 | **Critical Rule:** NEVER deploy without 100% test coverage. |
| 13 | |
| 14 | ## Core Workflow |
| 15 | |
| 16 | ### Step 1: Create Test Module |
| 17 | |
| 18 | ```move |
| 19 | #[test_only] |
| 20 | module my_addr::my_module_tests { |
| 21 | use my_addr::my_module::{Self, MyObject}; |
| 22 | use aptos_framework::object::{Self, Object}; |
| 23 | use std::string; |
| 24 | use std::signer; |
| 25 | |
| 26 | // Test constants |
| 27 | const ADMIN_ADDR: address = @0x100; |
| 28 | const USER_ADDR: address = @0x200; |
| 29 | const ATTACKER_ADDR: address = @0x300; |
| 30 | |
| 31 | // ========== Setup Helpers ========== |
| 32 | // (Reusable setup functions) |
| 33 | |
| 34 | // ========== Happy Path Tests ========== |
| 35 | // (Basic functionality) |
| 36 | |
| 37 | // ========== Access Control Tests ========== |
| 38 | // (Unauthorized access blocked) |
| 39 | |
| 40 | // ========== Input Validation Tests ========== |
| 41 | // (Invalid inputs rejected) |
| 42 | |
| 43 | // ========== Edge Case Tests ========== |
| 44 | // (Boundaries and limits) |
| 45 | } |
| 46 | ``` |
| 47 | |
| 48 | ### Step 2: Write Happy Path Tests |
| 49 | |
| 50 | **Test basic functionality works correctly:** |
| 51 | |
| 52 | ```move |
| 53 | #[test(creator = @0x1)] |
| 54 | public fun test_create_object_succeeds(creator: &signer) { |
| 55 | // Execute |
| 56 | let obj = my_module::create_my_object( |
| 57 | creator, |
| 58 | string::utf8(b"Test Object") |
| 59 | ); |
| 60 | |
| 61 | // Verify |
| 62 | assert!(object::owner(obj) == signer::address_of(creator), 0); |
| 63 | } |
| 64 | |
| 65 | #[test(owner = @0x1)] |
| 66 | public fun test_update_object_succeeds(owner: &signer) { |
| 67 | // Setup |
| 68 | let obj = my_module::create_my_object(owner, string::utf8(b"Old Name")); |
| 69 | |
| 70 | // Execute |
| 71 | let new_name = string::utf8(b"New Name"); |
| 72 | my_module::update_object(owner, obj, new_name); |
| 73 | |
| 74 | // Verify (if you have view functions) |
| 75 | // assert!(my_module::get_object_name(obj) == new_name, 0); |
| 76 | } |
| 77 | |
| 78 | #[test(owner = @0x1, recipient = @0x2)] |
| 79 | public fun test_transfer_object_succeeds( |
| 80 | owner: &signer, |
| 81 | recipient: &signer |
| 82 | ) { |
| 83 | let recipient_addr = signer::address_of(recipient); |
| 84 | |
| 85 | // Setup |
| 86 | let obj = my_module::create_my_object(owner, string::utf8(b"Object")); |
| 87 | assert!(object::owner(obj) == signer::address_of(owner), 0); |
| 88 | |
| 89 | // Execute |
| 90 | my_module::transfer_object(owner, obj, recipient_addr); |
| 91 | |
| 92 | // Verify |
| 93 | assert!(object::owner(obj) == recipient_addr, 1); |
| 94 | } |
| 95 | ``` |
| 96 | |
| 97 | ### Step 3: Write Access Control Tests |
| 98 | |
| 99 | **Test unauthorized access is blocked:** |
| 100 | |
| 101 | ```move |
| 102 | #[test(owner = @0x1, attacker = @0x2)] |
| 103 | #[expected_failure(abort_code = my_module::E_NOT_OWNER)] |
| 104 | public fun test_non_owner_cannot_update( |
| 105 | owner: &signer, |
| 106 | attacker: &signer |
| 107 | ) { |
| 108 | let obj = my_module::create_my_object(owner, string::utf8(b"Object")); |
| 109 | |
| 110 | // Attacker tries to update (should abort) |
| 111 | my_module::update_object(attacker, obj, string::utf8(b"Hacked")); |
| 112 | } |
| 113 | |
| 114 | #[test(owner = @0x1, attacker = @0x2)] |
| 115 | #[expected_failure(abort_code = my_module::E_NOT_OWNER)] |
| 116 | public fun test_non_owner_cannot_transfer( |
| 117 | owner: &signer, |
| 118 | attacker: &signer |
| 119 | ) { |
| 120 | let obj = my_module::create_my_object(owner, string::utf8(b"Object")); |
| 121 | |
| 122 | // Attacker tries to transfer (should abort) |
| 123 | my_module::transfer_object(attacker, obj, @0x3); |
| 124 | } |
| 125 | |
| 126 | #[test(admin = @0x1, user = @0x2)] |
| 127 | #[expected_failure(abort_code = my_module::E_NOT_ADMIN)] |
| 128 | public fun test_non_admin_cannot_configure( |
| 129 | admin: &signer, |
| 130 | user: &signer |
| 131 | ) { |
| 132 | my_module::init_module(admin); |
| 133 | |
| 134 | // Regular user tries admin function (should abort) |
| 135 | my_module::update_config(user, 100); |
| 136 | } |
| 137 | ``` |
| 138 | |
| 139 | ### Step 4: Write Input Validation Tests |
| 140 | |
| 141 | **Test invalid inputs are rejected:** |
| 142 | |
| 143 | ```move |
| 144 | #[test(user = @0x1)] |
| 145 | #[expected_failure(abort_code = my_module::E_ZERO_AMOUNT)] |
| 146 | public fun test_zero_amount_rejected(user: &signer) { |
| 147 | my_module::deposit(user, 0); // Should abort |
| 148 | } |
| 149 | |
| 150 | #[test(user = @0x1)] |
| 151 | #[expected_failure(abort_code = my_module::E_AMOUNT_TOO_HIGH)] |
| 152 | public fun test_excessive_amount_rejected(user: &signer) { |
| 153 | my_module::deposit(user, my_module::MAX_DEPOSIT_AMOUNT + 1); // Should abort |
| 154 | } |
| 155 | |
| 156 | #[test(owner = @0x1)] |
| 157 | #[expected_failure(abort_code = my_module::E_EMPTY_NAME)] |
| 158 | public fun test_empty_string_rejected(owner: &signer) { |
| 159 | let obj = my_module::create_my_object(owner, string::utf8(b"Initial")); |
| 160 | my_module::update_object(owner, obj, string::utf8(b"")); // Empty - should abort |
| 161 | } |
| 162 | |
| 163 | #[test(owner = @0x1)] |
| 164 | #[expected_failure(abort_code = my_module::E_NAME_TOO_LONG)] |
| 165 | public fun test_string_too_long_rejected(owner: &signer) { |