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.
In the last article, we covered “Manual” testing techniques of a Smart Contract in Plutus. After that, we presented “Tasty Framework” in Haskell and “Plutus Contract Test” (A special package in Plutus for testing Smart Contracts). Both are used for automated tests in Plutus.
In this article, we will present the following concepts:
Emulator Trace Based Tests
Optics and Lenses in Haskell
1. Assertions in Plutus
We begin our article by checking the different assertions available for use in our tests.
You can find below the Assertions available in the “Plutus.Contract.Test” Module.
Figure 1: Assertions in the “Plutus.Contract.Test” Module
2. Emulator Trace Based Tests:
In Plutus, a number of test types are supported. This blog post will explain two categories mainly:
- Emulator Trace based testing (covered in this article)
- Property based testing (will be presented in the next article)
Now, we will move on to covering the first Emulator Trace based testing
2.1 Test implementation
2.1.1 “tests”: The TestTree function
At this juncture, we go back to our code to implement our tests. We begin by coding the “tests” function from the type “TestTree”.
First, we have used the “checkPredicateOptions”. These options are needed to configure the Emulator before running the tests.
Second, we indicate the name of our tests. Simple string: “token sale trace”
Third, we specify the Predicate: This is the main assertion we should check after conducting the test
Fourth, we define the Trace, which we call “myTrace”
Figure 2: “tests” function implementation
2.1.2 “emCfg”: the EmulatorConfig Function
Using the Emulator config, we can specify the initial wallet funds.
Figure 3: EmulatorConfig implementation
2.1.3 “myPredicate”: TracePredicate function
This function has the main assertions regarding wallet balances in the Emulator Trace. So, we should check these after running the test.
We are using the (.&&.) combinator to check the wallet Funds after running our customized Token Sale use case.
Here, we can see that we have implemented the same assertions as the manual test cases.
2.2 Running the test:
Moving on, we can run the tests using repl after importing the Tasty Haskell Testing Framework and running the defaultMain tests. This is, of course, indicated by the Tasty framework.
Figure 4: Running Plutus Automatic Tests in the repl using Tasty Framework
2.3 Emulator Trace Based Test Execution Results
Previously, all tests have passed successfully.
Moving forward, we need to check how the test execution results look in case of Failed Tests. And we can manipulate test assertions to enforce test failure.
Hence, we change the expected funds in wallet 2 after running the tests from 20 to 19, which is obviously false.
Figure 5: Changing the test assertions from the test predicate
Figure 6: Running Plutus Automatic Tests in the repl using Tasty Framework — Case of failure
We notice that the test failed as expected.
We also get the logs offering details of the test execution results, along with a comparison between the Actual Results and Expected Results.
3. Code Coverage
3.1 Implementation of the code coverage functionality
Plutus allows us to check the code coverage. Thus, how much code covered by our tests serves as a crucial indicator of our code quality.
As described in “Figure 6: Checking Predicates” (Previous article) of the “Plutus.Contract.Test” Module”, one of the checking predicates functions provided by the “Plutus.Contract.Test” module is the “checkPredicateCoverage” function.
Figure 7: “checkPredicateOptionsCoverage” function implementation
Now, we implement the “testCoverage” function by deploying the mentioned “checkPredicateCoverage” function.
Figure 8: Implementation of the test coverage function
3.2 Executing the coverageTest function and result checking:
After implementing the “testCoverage” function, we run it in the repl. Once executed, the “testCoverage” function will save the results in an HTML report.
Figure 9: Running the test coverage function in repl
Our test coverage report captures a range of meaningful indicators. This is especially true for the transition function, where we have the core business logic of our use case.
For example, the green color code means that the highlighted conditions were always true in our test.
But the “Nothing” is highlighted in Red, meaning our tests did not cover this condition.
Figure 10: Html test coverage report
4. Optics and lenses in Haskell:
One critical concept in Haskell needed as part of our Testing framework is Optics and Lenses.
Various frameworks in Haskell implement these concepts. Still, the Plutus development team uses the library “Lens” in Hackage.
Optics attempts to deal with hierarchical data types. Thus, in this way, we can either read specific data located in one particular hierarchical data type or update it.
Figure 11: Example of a hierarchical data type
Lars created an example of a hierarchical data type he calls “company” in the Plutus pioneer lecture. The company includes a staff, which represents another data type called “Person”, which contains the person’s name and address.
The main challenge in a hierarchical data type is reading or changing the address of a specific person using a simple, straightforward function.
But, Lenses solve this issue. In fact, it provides a concept similar to “accessors”, which exist in other programming languages like Java, represented by the “.” symbol.
Figure 12: Getting and Updating specific data using Lenses
In the next article about testing in Plutus, we will present the concept of Property Based Testing.
Support also our PeakChain Automotive Solutions in Project Catalyst Fund 9!