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.