IntersectMBO / IntersectMBO/cardano-node-emulator
Questions about testing smart contracts
- Dominant language
- Haskell
- Stars
- 7
- Forks
- 13
- PR merge metrics
- No merged PRs in 30d
Description
In plutus-apps v1.2.0, there was the `EmulatorTrace` monad that allowed for creating arbitrary unit test scenarios. I was able to create general purpose endpoints like those in this [module](https://github.com/fallen-icarus/cardano-swaps/blob/main/test/Test/Internal.hs). I could then just create scenarios that submitted one transaction after another where I would test whether the last transaction in the scenario passed or failed. Here is an example [module](https://github.com/fallen-icarus/cardano-swaps/blob/main/test/Test/OneWaySwap/Swap.hs) that used this approach (it submits one arbitrary transaction after another in a single unit test). The benefit of this approach was that it worked for all smart contracts and all transactions; I didn't need to create other endpoints just to see how other smart contract executions or transaction inputs would impact the contract I was testing. Even testing for double satisfaction was very straightforward.
In the process of trying to migrate away from plutus-apps v1.2.0, I have been trying to convert my smart contract tests to follow the examples [here](https://github.com/input-output-hk/cardano-node-emulator/tree/main/cardano-node-emulator/test/Plutus/Examples). However, after reading through a lot of the documentation and trying it out, it seems the tests make heavy use of modeling internal state and actions which does not seem appropriate for my use case. Ultimately, a blockchain action is just a transaction and the state is the current UTxO set, so I would rather just be able to specify what transaction to submit next like I did with `EmulatorTrace`. I tried using `ContractModel` like this (using a dummy state and a single `Transact` action) but the types don't seem to allow for this (see bottom of comment for example code). While quickcheck-contractmodel's `ThreatModel` does not seem to require modeling internal state, it seems very limited in what it can do (e.g., AFAICT it doesn't support testing reference script usage). Even the dynamic logic unit tests seem to require modeling internal state and actions.
The reason why I don't think modeling state and actions is the right approach for me (and arguably others) is that it makes it very difficult to test smart contract composability. All of my DApps (DEX, lending/borrowing, options trading, secondary market) are composable with each other and all other DApps within a single transaction. They are also composable with themselves within a single transaction (you can use 25 swaps from the DEX in a single transaction). Having to model the state and actions of all possible compositions just doesn't seem feasible and also doesn't seem necessary; I just want to know whether or not a transaction fails after a given series of previous transactions. Being able to just specify what transaction to submit next is IMO infinitely easier. I do think there is a place for property based testing like those in the examples, but unit tests like those I described seem to be more useful to me as a DApp developer.
So my questions are:
1) Did I miss something that would allow me to replicate the behavior of my previous tests?
2) If no, is there any intention to bring back some of the capabilities from `EmulatorTrace`?
What I've tried
-- | Mint or burn native tokens. Can use either a reference script or a local script.
data TokenMint = TokenMint
{ mintTokens :: [(PlutusTx.TokenName,Integer)]
, mintRedeemer :: PlutusTx.Redeemer
, mintPolicy :: L.Versioned L.MintingPolicy
, mintReference :: Maybe (L.Versioned L.TxOutRef)
} deriving stock (Generic,Show,Eq)
-- | Create a transaction output at the specified address with the specified value, datum,
-- and reference script.
data Output = Output
{ outputAddress :: L.Address
, outputValue :: V.Value
, outputDatum :: PlutusTx.OutputDatum
, outputReferenceScript :: L.ReferenceScript
} deriving (Generic,Show,Eq)
-- | Used to create a transaction with the specified constraints.
data TransactionParams = TransactionParams
{ tokens :: [TokenMint]
, outputs :: [Output]
} deriving (Generic,Show,Eq)
-- | Create and submit a general transaction.
transact
:: (E.MonadEmulator m)
=> CardanoAddress
-> [PaymentPrivateKey]
-> TransactionParams
-> m ()
transact mainWallet privKeys TransactionParams{..} =
let mintWitnessMap = C.BuildTxWith $ Map.fromList $ flip map tokens $ \TokenMint{..} ->
( V.policyId mintPolicy
, unsafeFromRight $
C.toCardanoMintWitness mintRedeemer mintReference (Just mintPolicy)
)
mintValue = flip foldMap tokens $ \TokenMint{..} ->
flip foldMap mintTokens $ \(tn,i) ->
V.singleton (V.policyId mintPolicy) (unsafeFromRight $ C.toCardanoAssetName tn) i
outs = flip map outputs $ \Output{..} ->
C.TxOut
(unsafeFromRight $ C.toCardanoAddressInEra testnet outputAddress)
(C.toCardanoTxOutValue outputValue)
(unsafeFromRight $ C.toCardanoTxOutDatum outputDatum)
outputReferenceScript
tx =
C.CardanoBuildTx $ E.emptyTxBodyContent
{ C.txMintValue = C.TxMintValue C.MaryEraOnwardsBabbage mintValue mintWitnessMap
, C.txOuts = outs
}
in void $ E.submitUnbalancedTx mempty mainWallet privKeys tx
data TestState = TestState deriving (Generic,Show,Eq)
type Wallet = Integer
instance ContractModel TestState where
data Action TestState = Transact Wallet [Wallet] TransactionParams
deriving (Eq,Show,Generic)
initialState = TestState
nextState _ = QCCM.wait 1
arbitraryAction _ = pure $ Transact 1 [1] $ TransactionParams [] []
But the above results in various `Missing instance of HasVariables for non-Generic type ...` errors.
Contributor guide
Assessment
This issue has not been assessed yet.