Unit Testing Functions in Sui Move: SuiChallenge Day 2 Guide with Code Examples
In the fast-paced world of Sui Move development, where smart contract security can make or break a DeFi protocol or NFT project, unit testing stands as your first line of defense. As part of the SuiChallenge Day 2, we’re diving into functions and Sui Move unit testing, proving that your safety checks aren’t just theoretical. This guide arms you with practical code examples to test functions effectively, drawing from real-world patterns in the Sui ecosystem.
Structuring Your Project for Seamless Sui Move Unit Testing
Before writing a single test, get your project layout right. A clean structure separates concerns, making test Sui smart contracts intuitive. Place core logic in sources/ and tests in a dedicated tests/ folder. This convention, straight from Sui docs, boosts maintainability as your codebase grows.
Think of it as building a fortress: main modules handle business logic, while tests simulate battles without risking the kingdom. I’ve seen devs skip this, leading to tangled codebases that slow iteration. Don’t repeat that mistake.
Standard Project Structure for Sui Move Package
A standard Sui Move package follows a clear directory structure to separate production code from tests. This organization facilitates easy navigation and execution of unit tests using the `sui move test` command.
```
SuiChallengeDay2/
├── Move.toml
├── sources/
│ └── challenge.move
└── tests/
└── challenge_tests.move
```
The `sources/` directory holds your main Move modules with business logic, while `tests/` contains dedicated test modules. The `Move.toml` manifest specifies package details, dependencies, and the test-only address for safe testing.
Your Move. toml is the blueprint. Specify Sui framework dependencies pointing to testnet for reliable testing. This setup ensures your package compiles cleanly and pulls in essentials like std: : unit_test.
Unit tests in Sui Move leverage the Move Testing Framework, annotated with Sui Move supports straightforward unit testing with the #[test] attribute on functions within a #[test_only] module. This allows importing and exercising functions from your main module, such as the simple adder from SuiChallenge Day 1. Use sui::test_assert::assert_eq! for precise comparisons. The example below demonstrates tests for standard cases and edges like zero values and u64 boundaries, confirming correct behavior without triggering arithmetic overflows or aborts. These tests validate core functionality reliably. They focus on non-aborting scenarios to build confidence in the adder's logic. To handle expected failures (e.g., overflow), annotate with #[expected_failure]. Run all tests via `sui move test` in your package directory for quick feedback during development. for helpers unavailable in production, keeping modules lean. This nuance separates amateurs from pros, as it enforces resource safety even in tests. Fire up tests via Pro tip: Pair with Debugging shines here too. Drop Expected failures take this further, letting you verify that aborts trigger as intended. Slap on To thoroughly validate error handling in Sui Move functions, employ the #[expected_failure(abort_code = ECode)] attribute within test functions. This confirms that invalid inputs, such as minting zero coins or attempting division by zero, trigger aborts with the precise error code. The following self-contained example illustrates this for SuiChallenge Day 2 scenarios. By integrating these assertions, your unit tests gain precision, ensuring functions fail safely and predictably. Run `sui move test` to execute and verify these tests pass as expected. for abort on invalid input, like minting zero coins or division by zero in a math function from SuiChallenge Day 2] Layer in This complete unit test module for the `safe_multiply` function uses `#[test_only]` to isolate testing dependencies. It covers happy path scenarios with `assert_eq!`-style checks via `assert!`, an edge case with `u64::MAX * 1`, and an expected failure for overflow (`u64::MAX * 2`), showcasing Move's safe arithmetic that aborts on overflow. Running these tests confirms the function's correctness: happy paths and edges pass, while overflows correctly abort, enforcing runtime safety without unsafe operations. This pattern is essential for reliable Sui Move smart contracts. Coverage isn't vanity; it's your audit trail. Common traps: Ignoring gas timeouts (bump with Prioritize: One assertion per test for pinpoint failures. Name tests descriptively - For SuiChallenge Day 2, this arsenal proves your functions bulletproof. Functions aren't just code; they're the gears of secure Sui apps. Nail unit testing, and you're set for DeFi mints, NFT drops, or whatever blockchain beast you unleash next. Keep iterating - safety scales. #Example Unit Tests for the Day 1 Adder Function
```move
#[test_only]
module suichallenge::adder_tests {
use suichallenge::adder;
use sui::test_assert::assert_eq;
#[test]
/// Tests the happy path for addition.
fun test_add_happy_path() {
let result = adder::add(5, 3);
assert_eq!(result, 8);
}
#[test]
/// Tests addition with zero inputs, an important edge case.
fun test_add_zero() {
let result = adder::add(0, 0);
assert_eq!(result, 0);
}
#[test]
/// Tests addition near u64 maximum to verify no unexpected aborts.
fun test_add_max_edge() {
let max_u64 = 18446744073709551615u64;
let result = adder::add(max_u64, 0);
assert_eq!(result, max_u64);
}
}
```Executing Tests with Sui CLI Precision
sui move test – it’s that straightforward. Watch outputs: PASS, FAIL, or TIMEOUT reveal truths instantly. Filter with patterns for focused runs, ideal during iteration. --coverage to quantify tested lines. Anything under 80%? Prioritize those gaps. In my experience mentoring Sui devs, coverage reports unearth dead code faster than manual reviews. std: : debug: : print inside tests to inspect values mid-execution. No more blind assertions; see exactly why a test flakes. #Complete Unit Test with Expected Failure Assertions
```move
#[test_only]
module examples::math_tests {
const E_ZERO_DIVIDE: u64 = 100;
const E_INVALID_AMOUNT: u64 = 101;
/// Simulate a mint function that aborts on zero amount.
public fun mint(amount: u64): u64 {
assert!(amount > 0, E_INVALID_AMOUNT);
amount // Represents minted coins
}
/// Simulate division that aborts on divide by zero.
public fun divide(a: u64, b: u64): u64 {
assert!(b != 0, E_ZERO_DIVIDE);
a / b
}
#[test(expected_failure(abort_code = E_INVALID_AMOUNT))]
/// Test that minting zero coins aborts with the correct code.
fun test_mint_zero() {
mint(0);
}
#[test(expected_failure(abort_code = E_ZERO_DIVIDE))]
/// Test that division by zero aborts with the correct code.
fun test_divide_by_zero() {
divide(42, 0);
}
}
```#Sui Move Unit Test Module for safe_multiply
```move
#[test_only]
module sui_challenge::day2_tests {
use sui_challenge::day2;
use std::u64;
#[test]
fun test_happy_path_small() {
let result = day2::safe_multiply(2u64, 3u64);
assert!(result == 6u64, 0);
}
#[test]
fun test_happy_path_medium() {
let result = day2::safe_multiply(100u64, 10u64);
assert!(result == 1000u64, 0);
}
#[test]
fun test_max_times_one() {
let result = day2::safe_multiply(u64::MAX, 1u64);
assert!(result == u64::MAX, 0);
}
#[test(expected_failure)]
fun test_overflow_abort() {
day2::safe_multiply(u64::MAX, 2u64);
}
}
```sui move test --coverage followed by sui move coverage summary spotlights blind spots. Aim for 90% and on critical paths - functions handling funds demand it. Low coverage? Refactor or risk exploits. #[test(allow_unstable))] for heavy tests) or testing isolated without scenarios. Bridge to integration via Sui's scenario framework later, but master units first. I've mentored devs who unit-tested meticulously, shipping contracts that weathered mainnet storms unscathed. Best Practices and Pitfalls to Sidestep
test_vault_drain_unauthorized_aborts beats test_2. Mock minimally; leverage Sui's object model for realistic sims. And always, always test in testnet deps to mirror prod.
std: : debug: : print sparingly; prefer asserts.