Testing the exact error message returned from failed transactions

Maybe I’m lecturing the birds how to fly, and you already use a simpler method for this. If this is the case, please comment. Until this happens, I tell you about a way to do this I have just found.

The recommended way in the Daml docs to test negative use cases is to use submitMustFail: Test Templates Using Daml Script — Daml SDK 2.5.3 documentation

Unfortunately this method isn’t good enough: the test doesn’t tell you the exact reason for the failure, which means it can also pass for any other failure, not just for the one you want to test.

It’s a straightforward idea to use the try/catch construct for extracting the error message from the failed transaction.

This can be done as described in this thread: Is there a way to catch all exceptions? - #6 by a_putkov

The problem with this approach is that try/catch doesn’t work in Daml Script as expected: Choice exercise error not caught by try-catch in Daml Script

My solution idea is to wrap the try/catch construct into an ad hoc contract’s choice where it does exactly what is needed. This takes a few lines of extra code, but it doesn’t litter the production ledger because the contract only gets created during testing.

module Main where

import DA.Assert ((===))
import Daml.Script

type AssetId = ContractId Asset

template Asset
  with
    issuer : Party
    owner  : Party
    name   : Text
  where
    ensure name /= ""
    signatory issuer
    observer owner
    choice Give : AssetId
      with
        newOwner : Party
      controller owner
      do 
        assertMsg "You cannot give the asset to yourself" $ newOwner /= owner
        create this with
           owner = newOwner

setup : Script (Party, AssetId)
setup = script do
-- user_setup_begin
  alice <- allocatePartyWithHint "Alice" (PartyIdHint "Alice")
  bob <- allocatePartyWithHint "Bob" (PartyIdHint "Bob")
  aliceId <- validateUserId "alice"
  bobId <- validateUserId "bob"
  createUser (User aliceId (Some alice)) [CanActAs alice]
  createUser (User bobId (Some bob)) [CanActAs bob]
-- user_setup_end

  aliceTV <- submit alice do
    createCmd Asset with
      issuer = alice
      owner = alice
      name = "TV"

  return (alice, aliceTV)

template TestGiveAssertion
  with 
    alice : Party
  where 
    signatory alice
    choice TestGiveAssertion_Run: Text
      with 
        cid : ContractId Asset
      controller alice
      do 
        try do
          exercise cid (Give alice)
          return "Unreachable"
        catch 
          (ex : AnyException) -> pure $ DA.Internal.Desugar.message ex

test : Script ()
test = do 
  (alice, assetCid) <- setup
  msg <- submit alice do 
    createAndExerciseCmd 
      (TestGiveAssertion alice) 
      (TestGiveAssertion_Run assetCid)
  msg === "You cannot give the asset to yourself" 
3 Likes

Hey @gyorgybalazsi,

In Daml-Finance, we created a small util method to for the use case you’ve described of validating the expected assertion message. Check it out here. You will find some samples of it being used in tests here.

2 Likes

Thank you!

Can you show me how I could use this for the example cited above (Asset contract, Give choice)?

From the examples you are pointing me to I conclude that you don’t use this for testing ledger transactions which is my concern. You are using submitMustFail to test ledger transactions instead.

You’re correct @gyorgybalazsi , try catch doesn’t work in a Script when submitting ledger transactions. However, as per the example I shared with you a workaround to this is to move part of the choice logic into its own function which allows you to effectively unit test that specific function.

I’ve rewritten your example to showcase this :

module Test where

import DA.Assert ((===))
import DA.Exception (throw)
import Daml.Script

type AssetId = ContractId Asset

template Asset
  with
    issuer : Party
    owner  : Party
    name   : Text
  where
    ensure name /= ""
    signatory issuer
    observer owner
    choice Give : AssetId
      with
        newOwner : Party
      controller owner
      do
        -- assertMsg "You cannot give the asset to yourself" $ newOwner /= owner
        checkOwner newOwner owner
        create this with
           owner = newOwner

checkOwner : (CanAssert m, Eq a) => a -> a -> m ()
checkOwner newOwner owner = assertMsg "You cannot give the asset to yourself" $ newOwner /= owner

setup : Script (Party, AssetId)
setup = script do
-- user_setup_begin
  alice <- allocatePartyWithHint "Alice" (PartyIdHint "Alice")
  bob <- allocatePartyWithHint "Bob" (PartyIdHint "Bob")
  aliceId <- validateUserId "alice"
  bobId <- validateUserId "bob"
  createUser (User aliceId (Some alice)) [CanActAs alice]
  createUser (User bobId (Some bob)) [CanActAs bob]
-- user_setup_end

  aliceTV <- submit alice do
    createCmd Asset with
      issuer = alice
      owner = alice
      name = "TV"

  return (alice, aliceTV)

template TestGiveAssertion
  with
    alice : Party
  where
    signatory alice
    choice TestGiveAssertion_Run: Text
      with
        cid : ContractId Asset
      controller alice
      do
        try do
          exercise cid (Give alice)
          return "Unreachable"
        catch
          (ex : AnyException) -> pure $ DA.Internal.Desugar.message ex

test : Script ()
test = do
  (alice, assetCid) <- setup
  msg <- submit alice do
    createAndExerciseCmd
      (TestGiveAssertion alice)
      (TestGiveAssertion_Run assetCid)
  msg === "You cannot give the asset to yourself"

  validateAssertionFailure
    (checkOwner alice alice)
    "You cannot give the asset to yourself"

exception TestFailureException
  with
    text : Text
  where
    message "TestFailureException(text=" <> text <> ")"

validateAssertionFailure : Script a -> Text -> Script ()
validateAssertionFailure assertion expectedFailureMessage =
  try do
    assertion
    throw TestFailureException with
      text = "Expected test failure succeeded - expectedFailureMessage=" <> expectedFailureMessage
  catch
    (AssertionFailed msg) -> msg === expectedFailureMessage
    myTestException@(TestFailureException _) -> debug "got to here" -- assertFail $ show myTestException

In this scenario, the error message is always static but this workaround could be more useful when you have multiple assertions in a choice and you want to check the right error message is returned or the error message is dynamic depending on the function input (as per the Daml Finance example).

1 Like

Thanks, that’s also a viable approach.