β—ˆ Loading Article

21-Day Sui Challenge Day 2: Functions & Unit Testing in Move

PREPARING CONTENT

[tutorial]

21-Day Sui Challenge Day 2: Functions & Unit Testing in Move

Learn Move functions, visibility modifiers, and unit testing. Write your first test with assert_eq and expected_failure.

Harry Phan Writer: Harry Phan
#sui-challenge #learn-move #move-functions #unit-testing #sui-tutorial #day-2 #beginner
21-Day Sui Challenge Day 2: Functions & Unit Testing in Move

Day 2 of the 21-Day Sui Challenge.

Yesterday you defined constants and explored Move’s type system. Today, we’re making things do something. Functions are the heart of any program, and Move has some unique twists you need to understand.


The Anatomy of a Function

Let’s start with the basics. Here’s what a Move function looks like:

public fun sum(a: u64, b: u64): u64 {
    a + b
}

Five parts to understand:

  1. public β€” Visibility modifier (who can call this)
  2. fun β€” Keyword that declares a function
  3. sum β€” The function name
  4. (a: u64, b: u64) β€” Parameters with explicit types
  5. : u64 β€” Return type

No Return Keyword Needed

Here’s the thing that trips up developers from other languages: Move functions automatically return the last expression.

// These are identical:
public fun add_v1(a: u64, b: u64): u64 {
    return a + b  // explicit return
}

public fun add_v2(a: u64, b: u64): u64 {
    a + b  // implicit return (preferred)
}

Notice the missing semicolon in add_v2. That’s intentional. In Move:

  • Expression (no semicolon) β†’ returns a value
  • Statement (with semicolon) β†’ returns nothing
// This WON'T compile:
public fun broken(a: u64, b: u64): u64 {
    a + b;  // semicolon makes it a statement, returns ()
}
// Error: expected 'u64', found '()'

This is borrowed from Rust, and once you get it, it feels natural.


Visibility and Access

Move has four visibility levels, but for Day 2, you need to know two:

public fun β€” Callable from Anywhere

public fun greet(): vector<u8> {
    b"Hello, Sui!"
}

Anyone can call this: other modules, other packages, even external transactions.

fun (no modifier) β€” Private

fun internal_helper(x: u64): u64 {
    x * 2
}

Only functions in the same module can call this. It’s hidden from the outside world.

Why This Matters

Good Move code uses private functions for implementation details and only exposes what’s necessary:

module challenge::calculator {
    // Public API
    public fun calculate(a: u64, b: u64, op: u8): u64 {
        if (op == 0) add(a, b)
        else if (op == 1) subtract(a, b)
        else multiply(a, b)
    }

    // Private helpers
    fun add(a: u64, b: u64): u64 { a + b }
    fun subtract(a: u64, b: u64): u64 { a - b }
    fun multiply(a: u64, b: u64): u64 { a * b }
}

External callers only see calculate. The internal logic is hidden and can be changed without breaking the public API.

A Note on Other Visibility Levels

Move actually has two more visibility modifiers you’ll encounter later:

  • public(package) β€” Only callable within the same package (not from other packages)
  • entry β€” Callable from transactions, but not from other Move code

The entry modifier is particularly useful for security. When you want users to call a function directly but prevent other smart contracts from wrapping it (to avoid composition attacks), use entry. You’ll see this pattern when working with sensitive operations like token transfers.

For now, public and private (fun) are all you need.


Return Patterns

Let’s go deeper than the basic sum function.

Multiple Return Values

Move functions can return tuples:

public fun divide_with_remainder(a: u64, b: u64): (u64, u64) {
    let quotient = a / b;
    let remainder = a % b;
    (quotient, remainder)
}

// Usage:
fun use_division() {
    let (q, r) = divide_with_remainder(17, 5);
    // q = 3, r = 2
}

This is cleaner than returning a struct for simple cases.

Early Returns with Conditions

Sometimes you need to bail out early:

public fun safe_divide(a: u64, b: u64): u64 {
    if (b == 0) {
        return 0  // Early return to avoid division by zero
    };
    a / b
}

Note the semicolon after the if block β€” it’s a statement here. The final a / b is the expression that gets returned for non-zero b.

Functions That Return Nothing

Some functions are just for side effects:

public fun log_value(value: u64) {
    // In real code, this might emit an event
    // For now, it does nothing visible
    let _ = value;
}

The return type is () (unit type), which is implicit when you don’t specify one.


Testing Deep Dive

This is where Day 2 really shines. Move’s testing system is built into the language, not bolted on as an afterthought.

Basic Test Structure

#[test]
fun test_sum() {
    let result = sum(1, 2);
    assert!(result == 3, 0);
}

The #[test] attribute tells the Move compiler this is a test function. It won’t be included in production builds.

Assert vs Assert_eq

Move gives you two main assertion tools:

#[test_only]
use std::unit_test::assert_eq;

#[test]
fun test_with_assert() {
    let result = sum(10, 20);
    assert!(result == 30, 0);  // Basic assertion
}

#[test]
fun test_with_assert_eq() {
    let result = sum(10, 20);
    assert_eq!(result, 30);  // More descriptive on failure
}

assert_eq! is a macro (note the !) that gives better error messages when tests fail. Use it.

The #[test_only] Pattern

You don’t want test utilities in production code:

module challenge::day_02 {
    // This import only exists during testing
    #[test_only]
    use std::unit_test::assert_eq;

    public fun sum(a: u64, b: u64): u64 {
        a + b
    }

    // This function only exists during testing
    #[test_only]
    fun create_test_value(): u64 {
        42
    }

    #[test]
    fun test_sum() {
        let expected = create_test_value();
        // ...
    }
}

The #[test_only] attribute on the import and helper function means they’re stripped from the final bytecode. Your production module stays lean.

Testing Edge Cases

Good tests cover more than the happy path:

#[test]
fun test_sum_zero() {
    assert_eq!(sum(0, 0), 0);
    assert_eq!(sum(0, 100), 100);
    assert_eq!(sum(100, 0), 100);
}

#[test]
fun test_sum_large_numbers() {
    let large = 1_000_000_000_000;
    assert_eq!(sum(large, large), 2_000_000_000_000);
}

Expected Failures

Sometimes you want code to fail. Move lets you test for that:

#[test]
#[expected_failure(arithmetic_error, location = Self)]
fun test_overflow_aborts() {
    let max: u64 = 18_446_744_073_709_551_615;
    let _result = sum(max, 1);  // This overflows!
}

If the code doesn’t abort, the test fails. This is crucial for security β€” you need to verify that dangerous operations actually get blocked.

Abort Codes

You can also test for specific abort codes:

public fun divide(a: u64, b: u64): u64 {
    assert!(b != 0, 1001);  // Abort code 1001 for division by zero
    a / b
}

#[test]
#[expected_failure(abort_code = 1001)]
fun test_divide_by_zero() {
    let _result = divide(10, 0);  // Should abort with code 1001
}

This is how professional Move code handles errors β€” each failure has a unique code that can be caught and tested.


Complete Solution

Here’s my complete Day 2 implementation. It goes well beyond the minimum requirements:

/// DAY 2: Primitive Types & Simple Functions - Complete Solution
///
/// This demonstrates:
/// - Basic functions with parameters and return values
/// - Multiple function patterns (helpers, wrappers, mathematical)
/// - Comprehensive testing strategies
/// - Edge case handling

module challenge::day_02 {
    // ═══════════════════════════════════════════════════════════════
    // IMPORTS
    // ═══════════════════════════════════════════════════════════════

    #[test_only]
    use std::unit_test::assert_eq;

    // ═══════════════════════════════════════════════════════════════
    // CONSTANTS
    // ═══════════════════════════════════════════════════════════════

    /// Error code for division by zero
    const E_DIVISION_BY_ZERO: u64 = 1001;

    /// Error code for invalid input
    const E_INVALID_INPUT: u64 = 1002;

    // ═══════════════════════════════════════════════════════════════
    // CORE FUNCTIONS
    // ═══════════════════════════════════════════════════════════════

    /// Adds two numbers
    /// The basic function required by Day 2
    public fun sum(a: u64, b: u64): u64 {
        a + b
    }

    /// Subtracts b from a
    /// Note: Will abort if b > a (no negative numbers in Move!)
    public fun subtract(a: u64, b: u64): u64 {
        a - b
    }

    /// Multiplies two numbers
    public fun multiply(a: u64, b: u64): u64 {
        a * b
    }

    /// Divides a by b with explicit zero-check
    public fun divide(a: u64, b: u64): u64 {
        assert!(b != 0, E_DIVISION_BY_ZERO);
        a / b
    }

    /// Returns quotient and remainder
    public fun divide_with_remainder(a: u64, b: u64): (u64, u64) {
        assert!(b != 0, E_DIVISION_BY_ZERO);
        (a / b, a % b)
    }

    // ═══════════════════════════════════════════════════════════════
    // HELPER FUNCTIONS (Private)
    // ═══════════════════════════════════════════════════════════════

    /// Checks if a number is even
    fun is_even(n: u64): bool {
        n % 2 == 0
    }

    /// Checks if a number is odd
    fun is_odd(n: u64): bool {
        !is_even(n)
    }

    /// Returns the larger of two numbers
    fun max(a: u64, b: u64): u64 {
        if (a > b) a else b
    }

    /// Returns the smaller of two numbers
    fun min(a: u64, b: u64): u64 {
        if (a < b) a else b
    }

    // ═══════════════════════════════════════════════════════════════
    // ADVANCED FUNCTIONS
    // ═══════════════════════════════════════════════════════════════

    /// Calculates power: base^exponent
    /// Uses iterative approach to avoid stack overflow
    public fun power(base: u64, exponent: u64): u64 {
        if (exponent == 0) {
            return 1
        };

        let mut result = 1;
        let mut i = 0;
        while (i < exponent) {
            result = result * base;
            i = i + 1;
        };
        result
    }

    /// Calculates factorial: n!
    /// n! = n * (n-1) * (n-2) * ... * 1
    public fun factorial(n: u64): u64 {
        if (n <= 1) {
            return 1
        };

        let mut result = 1;
        let mut i = 2;
        while (i <= n) {
            result = result * i;
            i = i + 1;
        };
        result
    }

    /// Calculates the average of two numbers (integer division)
    public fun average(a: u64, b: u64): u64 {
        // Safe average that doesn't overflow
        // Instead of (a + b) / 2, use a/2 + b/2 + (a%2 + b%2)/2
        let half_a = a / 2;
        let half_b = b / 2;
        let remainder = ((a % 2) + (b % 2)) / 2;
        half_a + half_b + remainder
    }

    /// Calculates absolute difference between two numbers
    public fun abs_diff(a: u64, b: u64): u64 {
        if (a > b) a - b else b - a
    }

    /// Clamps a value between min and max bounds
    public fun clamp(value: u64, min_val: u64, max_val: u64): u64 {
        assert!(min_val <= max_val, E_INVALID_INPUT);
        if (value < min_val) {
            min_val
        } else if (value > max_val) {
            max_val
        } else {
            value
        }
    }

    // ═══════════════════════════════════════════════════════════════
    // TESTS - Basic
    // ═══════════════════════════════════════════════════════════════

    #[test]
    fun test_sum_basic() {
        assert_eq!(sum(1, 2), 3);
    }

    #[test]
    fun test_sum_zero() {
        assert_eq!(sum(0, 0), 0);
        assert_eq!(sum(0, 100), 100);
        assert_eq!(sum(100, 0), 100);
    }

    #[test]
    fun test_sum_large() {
        let billion = 1_000_000_000;
        assert_eq!(sum(billion, billion), 2_000_000_000);
    }

    #[test]
    fun test_subtract() {
        assert_eq!(subtract(10, 3), 7);
        assert_eq!(subtract(100, 100), 0);
        assert_eq!(subtract(5, 0), 5);
    }

    #[test]
    fun test_multiply() {
        assert_eq!(multiply(6, 7), 42);
        assert_eq!(multiply(0, 1000), 0);
        assert_eq!(multiply(1, 999), 999);
    }

    #[test]
    fun test_divide() {
        assert_eq!(divide(20, 4), 5);
        assert_eq!(divide(17, 5), 3);  // Integer division
        assert_eq!(divide(0, 10), 0);
    }

    // ═══════════════════════════════════════════════════════════════
    // TESTS - Advanced
    // ═══════════════════════════════════════════════════════════════

    #[test]
    fun test_divide_with_remainder() {
        let (q, r) = divide_with_remainder(17, 5);
        assert_eq!(q, 3);
        assert_eq!(r, 2);

        let (q2, r2) = divide_with_remainder(20, 4);
        assert_eq!(q2, 5);
        assert_eq!(r2, 0);
    }

    #[test]
    fun test_power() {
        assert_eq!(power(2, 0), 1);
        assert_eq!(power(2, 1), 2);
        assert_eq!(power(2, 10), 1024);
        assert_eq!(power(3, 4), 81);
        assert_eq!(power(10, 6), 1_000_000);
    }

    #[test]
    fun test_factorial() {
        assert_eq!(factorial(0), 1);
        assert_eq!(factorial(1), 1);
        assert_eq!(factorial(5), 120);
        assert_eq!(factorial(10), 3_628_800);
    }

    #[test]
    fun test_average() {
        assert_eq!(average(10, 20), 15);
        assert_eq!(average(0, 100), 50);
        assert_eq!(average(7, 8), 7);  // Integer division: (7+8)/2 = 7
    }

    #[test]
    fun test_average_no_overflow() {
        // This would overflow with naive (a+b)/2 approach
        let max = 18_446_744_073_709_551_615u64;
        let result = average(max, max);
        assert_eq!(result, max);
    }

    #[test]
    fun test_abs_diff() {
        assert_eq!(abs_diff(10, 7), 3);
        assert_eq!(abs_diff(7, 10), 3);
        assert_eq!(abs_diff(5, 5), 0);
    }

    #[test]
    fun test_clamp() {
        assert_eq!(clamp(5, 0, 10), 5);   // Within range
        assert_eq!(clamp(0, 5, 10), 5);   // Below min
        assert_eq!(clamp(15, 0, 10), 10); // Above max
    }

    // ═══════════════════════════════════════════════════════════════
    // TESTS - Private Functions (test_only access)
    // ═══════════════════════════════════════════════════════════════

    #[test]
    fun test_is_even() {
        assert!(is_even(0), 0);
        assert!(is_even(2), 1);
        assert!(is_even(100), 2);
        assert!(!is_even(1), 3);
        assert!(!is_even(99), 4);
    }

    #[test]
    fun test_is_odd() {
        assert!(is_odd(1), 0);
        assert!(is_odd(99), 1);
        assert!(!is_odd(0), 2);
        assert!(!is_odd(100), 3);
    }

    #[test]
    fun test_max_min() {
        assert_eq!(max(10, 5), 10);
        assert_eq!(max(5, 10), 10);
        assert_eq!(max(7, 7), 7);

        assert_eq!(min(10, 5), 5);
        assert_eq!(min(5, 10), 5);
        assert_eq!(min(7, 7), 7);
    }

    // ═══════════════════════════════════════════════════════════════
    // TESTS - Expected Failures
    // ═══════════════════════════════════════════════════════════════

    #[test]
    #[expected_failure(abort_code = E_DIVISION_BY_ZERO)]
    fun test_divide_by_zero() {
        let _result = divide(10, 0);
    }

    #[test]
    #[expected_failure(abort_code = E_DIVISION_BY_ZERO)]
    fun test_divide_with_remainder_by_zero() {
        let (_q, _r) = divide_with_remainder(10, 0);
    }

    #[test]
    #[expected_failure(arithmetic_error, location = Self)]
    fun test_subtract_underflow() {
        let _result = subtract(5, 10);  // 5 - 10 underflows
    }

    #[test]
    #[expected_failure(arithmetic_error, location = Self)]
    fun test_sum_overflow() {
        let max = 18_446_744_073_709_551_615u64;
        let _result = sum(max, 1);
    }

    #[test]
    #[expected_failure(abort_code = E_INVALID_INPUT)]
    fun test_clamp_invalid_bounds() {
        let _result = clamp(5, 10, 5);  // min > max is invalid
    }
}

Running the Tests

Navigate to your Day 2 folder and run:

cd day_02
sui move test

You should see all tests passing:

Running Move unit tests
[ PASS    ] challenge::day_02::test_abs_diff
[ PASS    ] challenge::day_02::test_average
[ PASS    ] challenge::day_02::test_average_no_overflow
[ PASS    ] challenge::day_02::test_clamp
[ PASS    ] challenge::day_02::test_clamp_invalid_bounds
[ PASS    ] challenge::day_02::test_divide
[ PASS    ] challenge::day_02::test_divide_by_zero
[ PASS    ] challenge::day_02::test_divide_with_remainder
[ PASS    ] challenge::day_02::test_divide_with_remainder_by_zero
[ PASS    ] challenge::day_02::test_factorial
[ PASS    ] challenge::day_02::test_is_even
[ PASS    ] challenge::day_02::test_is_odd
[ PASS    ] challenge::day_02::test_max_min
[ PASS    ] challenge::day_02::test_multiply
[ PASS    ] challenge::day_02::test_power
[ PASS    ] challenge::day_02::test_subtract
[ PASS    ] challenge::day_02::test_subtract_underflow
[ PASS    ] challenge::day_02::test_sum_basic
[ PASS    ] challenge::day_02::test_sum_large
[ PASS    ] challenge::day_02::test_sum_overflow
[ PASS    ] challenge::day_02::test_sum_zero
Test result: OK. 21 passed; 0 failed
Day 2 Move tests passing - 21 tests for functions and testing
cli.firstmovers.io

Run your tests with Sui CLI Web β€” all tests passing with detailed output


Key Takeaways

1. Functions Return Last Expression

No return keyword needed. Just leave off the semicolon:

public fun add(a: u64, b: u64): u64 {
    a + b  // ← This is returned
}

2. Visibility Controls Access

  • public fun β€” Anyone can call
  • fun β€” Only this module can call

3. Tests Are First-Class

The #[test] attribute integrates testing into the language:

#[test]
fun test_something() {
    assert_eq!(1 + 1, 2);
}

4. Expected Failures Are Powerful

Test that your code fails correctly:

#[test]
#[expected_failure(abort_code = 1001)]
fun test_should_fail() {
    // This must abort with code 1001
}

5. Error Codes Matter

Define constants for error codes:

const E_DIVISION_BY_ZERO: u64 = 1001;

This makes debugging easier and tests more precise.


What’s Next

Tomorrow in Day 3, we move from functions to data structures. You’ll learn:

  • How to define structs in Move
  • The difference between struct and Sui objects
  • How to create, modify, and destroy data

Functions let you do things. Structs let you hold things. Together, they’re the foundation for everything you’ll build on Sui.


Commit Your Progress

cd day_02
sui move test
git add day_02/
git commit -m "Day 2: practice functions and testing"

Day 2 complete. You’ve learned how Move functions work, written tests that verify behavior, and tested for expected failures. That’s real Move development.

See you on Day 3.


Follow: @ercandotsui | @harry_phan06


πŸ‡»πŸ‡³ Vietnamese Builders: Level Up Your Journey

First Movers Sprint 2026 is here!

Following the success of CommandOSS Hacker House HCMC, First Movers Sprint 2026 is designed as a stepping-stone Hacker House β€” a space for builders in the Sui ecosystem to sharpen their skills, accelerate product development, and get fully prepared for the next CommandOSS Hacker House.

Whether you’re a developer or a non-technical business/growth builder, you’re welcome to join!

First Movers Sprint 2026 Hackathon
firstmovers.io

Why join First Movers Sprint?

  • πŸ’Ό Paid internship at top Sui ecosystem projects (full-time)
  • πŸ† Exciting prizes and rewards
  • 🌟 Learn directly with top mentors from the Sui ecosystem

Co-organized by @firstmoversvn, @0xCommandOSS, and ITviec β€” the leading IT recruitment platform.