To guarantee secure and high-quality smart contracts, rigorous testing strategies should be implemented. These will significantly differ from standard testing techniques typically used in low-risk web 2 applications.
Testing in Plutus has been be thoroughly covered in the 8th lecture of the Plutus Pioneer Program.
My background, which is largely in software quality in test automation, means I was ecstatic to discover the blockchain testing concepts, especially with Plutus.
In three Plutus Testing related articles, we will go over special techniques on how to test Plutus smart contracts using the emulator trace, calculate test coverage, the notion of optics and lenses, and Property Based Testing.
1. “Manual” Testing of a Token Sale Smart Contract
1.1. Token Sale Use Case Description
To show testing strategies, in the 8th lecture of the Plutus Pioneer Program, Lars presented a Use Case implementation with State Machine, an example of a Token Sale.
In this example, a seller wants to buy a certain amount of Tokens by locking them in a contract and then setting their prices. In this way, the seller is able to retrieve their locked Tokens at any time.
Figure 1: Modelization of the flow of UTXOs of the Token Sale Use Case
Upon implementing our on-chain and off-chain Token Sale code using the state machine mechanism, as covered in our last article, we can attempt our code by using the emulator trace.
The Plutus Pioneer Program GitHub repository of week 8 could help with checking details of the implementation:
1.2. Emulator Trace Configuration
Prior to testing out our code, first, we should configure the emulator trace.
Each fund first needs some initial fund defining. So, we must fund three wallets needed for our use case, not only with 1000 ADA each but also with 1000 customized Tokens we called “A”.
Figure 2: Configuring the emulator by funding three wallets with ADA and “A” Token
Next, we should set up the use case steps:
Wallet 1, representing the seller, sets the price of the Token to 1 ADA, and after that locks 100 A Token in the contract.
Wallet 2 buys 20 ADA
Wallet 3 buys 5 Tokens
Wallet 1 (the seller) retrieved 40 A tokens and 10 ADA from the Token Sale contract.
Figure 3: Configuring the EmulatorTrace with a specific Token Sale case
The previous code is written as a Test Suite. This meant the code could be run as a test executable file.
After running the test suite, we can check the file balances of the three wallets.
Figure 4: Final Balances of the Wallets after running the Token Sale Test Suite
What follows is an explanation of the final balances we see in the previous figure:
Wallet 2: Bought 20 A Token using 20 ADA
Wallet 3: Bought 5 A Tokens using 5 ADA
Wallet1: Placed 100 A Tokens and withdrew 40 A Tokens from the contract again.
1.3 Minimal deposit of ADA for the contract
In wallet 1, we can discover that 2 ADA are missing: We withdrew 10 ADA, but we got just 8 ADA.
The reasoning behind this is simple: to set up the token sale UTXO, we must deposit a minimum amount of ADA. However, this amount is variable in the real blockchain (testnet or mainnet) and depends on the size of the UTXO.
In the emulator trace, however, for simplification reasons, the min ADA is fixed and set to 2 ADA.
The minimal ADA deposit was not mentioned explicitly in our code. Thus, the state machine takes care of that and sets it automatically, representing a clear advantage of using the state machine concept.
2. Automation Testing in Plutus
2.1 Tasty Framework in Haskell
In the , we prepared what can only be described as a „manual“ test in Plutus.
Nevertheless, automated tests bring more advantages, supplying the ability to configure and run regression tests. These tests could, thus, be configured to automatically execute once a new software release is deployed.
Various testing frameworks in Haksell provide a good automation experience, which is achieved by organizing the tests suites, grouping them, labeling them, etc.
Here, Plutus utilized the „Tasty Test Framework“
Hackage offers more information about this framework: https://hackage.haskell.org/package/tastyThrough the Tasty framework, tests have the type „TestTree“, which, as the name indicates, means a tree of tests. Remember this type for later in Plutus tests.
By using the Tasty framework, we cam combine different type of tests, like unit tests, golden tests, QuickCheck/SmallCheck properties, and any other tests, into a single test suite. This is achieved by grouping these tests in groups and sub-groups.
Figure 5: Example of implementing a Test in Haskell using Tasty framework
2.2 Plutus Contract Test Module
In Plutus, a special package is available for testing our smart contracts, named: “Plutus Contract Test”.
To explore Plutus Contract Tests, we must develop an understanding of the „checking predicates“ of the module Plutus.Contract.Test
First, it’s worth defining what a predicate means in Haskell:
A predicate is a function of some value of type a to a Result, i.e. a Bool-like value with Okay as True and Fail as False, which carries additional data in each branch.
So, this means we can say that a predicate checks a condition and returns true if the assertions are met, but false if not.
2.2.1 Checking Predicates of the Plutus Contract Test
„checking predicates“ of the module Plutus.Contract. The test includes a few important functions:
22.214.171.124 checkPredicate function
o Function Input types:
Trace Predicate (see next point 126.96.36.199)
EmulatorTrace (the same type as used in the manual testing section)
o Function Output type:
- Test Tree: The same type seen in the Tasty framework in Haskell
Figure 6: “Checking Predicates” of the “Plutus.Contract.Test” Module
188.8.131.52 Trace Predicate:
Trace Predicate is simply a predicate on a Trace, which means a function responsible for checking a certain condition on a Trace.
In the trace predicate definition, we can notice different combinators of the TracePredicate types (not, (.&&.), (.||.) )
Figure 7: “TracePredicate” Type in the “Plutus.Contract.Test” Module
So, this means there are numerous testing predicates available.
In our Token Sale contract, we will pick just one, the “walletFundsChange”.
By using this function, we can check, after running the tests, if the wallet funds have changed as predicted in the input Value; this is, of course, excluding the fees.
The calculation of fees is complicated. So, the “walletFundsChange” function makes our life easier by automatically excluding fees and calculating the exact change of funds in our testing wallet after conducting the tests.
Figure 8: “walletFundsChange” predicate in the “Plutus.Contract.Test” Module
184.108.40.206 „checkPredicateOptions“ function:
Almost identical input and output types as the earlier “checkPredicate” function.
There is just one added input with type “CheckOptions”
Figure 9: “CheckOptions” in the “Plutus.Contract.Test” Module
Thus, for us to set the checkOptions, we must change the type “Lens’
”. Consequently, this leads to us working on “optics”, as part of Haskell, as discussed later.
Furthermore, we notice another essential function that we will later utilize, the “EmulatorConfig” function. Through this function, we can configure the emulator as required before running the tests.
In the next article about testing in Plutus, we will present the following concepts:
Emulator Trace Based Tests
Optics and Lenses in Haskell
Support also our PeakChain Automotive Solutions in Project Catalyst Fund 9!