SUI move programming model: Choosing between return value (boolean, option ) VS assert!

I am a bit unclear about the SUI programming model on how to handle smart contract execution status. Imagine, I have a module to issue an NFTCard with a max supply, once the max supply is reached, the minting should fail. I used assert! to assert that the number of minting NFTCard must be less than or equal to the max supply, however, I could not find any way to test the assert! using the SUI test framework. Currently, I have to convert the mint fun to return Option instead to be able to test.

My questions are:

  • Are there any better ways to use assertions and still be able to test?
  • What could be the consequences, if I convert from assertions to a return value for the sake of writing unit testing? I always put maintainable and testable code as a priority but security is paramount.
  public fun mint_card(
    card_tier_name: String,
    card_type_name: String,
    partner_address: address,
    partner: &mut Partner,
    ctx: &mut TxContext): Option<NFTCard>{

    let partner_id = object::id(partner);

    // assert!(partner::partner_owner_address(partner) == sender, ERROR_NOT_OWNER);
    if(partner::partner_owner_address(partner) != partner_address) {
      return option::none<NFTCard>()
    };

    let mut_card_tier = nft::borrow_mut_card_tier_by_name(card_tier_name, partner);
    let card_tier_id = object::id(mut_card_tier);
    let benefit = nft::card_tier_benefit(mut_card_tier);

    let mut_card_type = nft::borrow_mut_card_type_by_name(card_type_name, mut_card_tier);
    let card_type_id = object::id(mut_card_type);

    if(nft::card_type_current_issued_number(mut_card_type) >= nft::card_type_max_supply(mut_card_type)) {
      return option::none<NFTCard>()
    };

    let issued_number = nft::card_type_current_issued_number(mut_card_type) + 1;
    let issued_at = tx_context::epoch(ctx);

    let nft_card = nft::new_nft_card(
      partner_id,
      card_tier_id,
      card_type_id,
      issued_number,
      issued_at,
      benefit,
      ctx
    );

    nft::increase_current_issued_number(mut_card_type);

    let card_id = object::id(&nft_card);
    let partner_id = object::id(partner);

    let card_created_event = PartnerNFTCardCreatedEvent {
      card_id,
      partner_id,
      card_tier_id: card_tier_id,
      card_tier_name,
      card_type_id: card_type_id,
      card_type_name,
      issued_number: issued_number,
      issued_at: issued_at,
      benefit: benefit
    };

    event::emit(card_created_event);

    option::some<NFTCard>(nft_card)
  }

2 Likes

I’ve found this: https://github.com/move-language/move/blob/main/language/documentation/book/src/unit-testing.md#testing-annotations-their-meaning-and-usage

A test can also be annotated as an #[expected_failure]. This annotation marks that the test should is expected to raise an error. You can ensure that a test is aborting with a specific abort code by annotating it with #[expected_failure(abort_code = <code>)], if it then fails with a different abort code or with a non-abort error the test will fail. Only functions that have the #[test] annotation can also be annotated as an #[expected_failure].

It seems that #[expected_failure] annotation solves my problem. I will refactor my code to use assertion and add the #[expected_failure] annotation.

3 Likes

The expected_failure annotation is indeed the recommended approach for ensuring that your asserts fire!

1 Like