Testing
The LIGO command-line interpreter provides commands to test your LIGO code. It provides three main ways to test code:
ligo run test
: Runs automated tests in LIGO codeligo run interpret
: Interprets a LIGO expression in the context of a LIGO fileligo run dry-run
: Simulates running a contract based on a given parameter and storage value
LIGO testing tools are in beta and may change. No production test procedure should rely on these tools alone.
Testing with ligo run test
The command ligo run test
runs automated tests on a contract.
When running the ligo run test
command, LIGO code has access to an additional Test
module.
This module provides ways of originating contracts and executing transactions in a simulated environment, as well as additional helper functions that allow you to control different parameters of the Mavryk testing library.
The LIGO interpreter uses the same library that Mavryk internally uses for testing.
To originate a contract in the test simulation, use the Test.Next.originate
function, which accepts these parameters:
- The contract itself
- The initial storage value
- The starting balance of the contract in mav
The function returns an object that has these values:
taddr
: The address of the deployed contract in the simulationsize
: The size of the deployed contract in bytes, as an integercode
: The Michelson code of the contract
You can get the storage of a deployed contract by passing the address of the contract to the Test.Next.Typed_address.get_storage
function.
For example, this LIGO file includes a simple counter contract:
To test the contract, create a function to originate the contract in the test simulation, call it, and verify the result. You can put the test functions in the same file or a separate file.
This example shows a test in a separate file. It follows these basic steps:
- It imports the contract file with the
import
directive. - It creates a function named
run_test1
for the test. - In the function, it creates a value for the initial storage of the contract.
- It originates the contract to the test simulation with the initial storage.
- It verifies that the deployed contract has the storage value.
- It calls the
increment
entrypoint with theTest.Next.Contract.transfer_exn
function, passing the entrypoint, the parameter, and 0 mav. - It verifies the updated storage value.
The run test
command evaluates all top-level definitions and prints any
entries that begin with the prefix test
as well as the value that these
definitions evaluate to. If any of the definitions fail, it prints a message
with the line number where the problem occurred.
You can also log messages to the console with the Test.Next.IO.log
function.
To run the tests, pass the file with the tests to the run test
command.
If the file imports other files, pass the folders that contain these files in the --library
argument, as in this example:
The response shows that the functions at the top level of the file ran successfully:
Creating transactions
The function Test.Next.Contract.transfer_exn
creates a transaction in the test simulation, as in the example in the previous section.
It takes these parameters:
- The target entrypoint or account to call
- The parameter to pass
- The amount of mav to include
If the transaction succeeds, it returns the gas consumption. If it fails, it fails the test.
For greater control, such as to test error conditions and error messages, you can use the function Test.Next.Contract.transfer
.
The function takes the same parameters but returns an option of the type test_exec_result
, which is Fail
if the transaction failed and Success
if it succeeded.
In case of success the value is the gas consumed and in case of failure the value is an object of the type test_exec_error
that describes the error.
If you create a transaction with Test.Next.Contract.transfer
and the transaction fails, the test does not automatically fail.
You must check the result of the transaction to see if it succeeded or failed.
For example, this contract is similar to the contract in an earlier example, but it only allows the number in storage to change by 5 or less with each transaction:
This test verifies that the error works by passing a number larger than 5 and handling the error:
Generating test accounts
You can use test accounts to simulate real accounts in tests.
For example, assume that you want to allow only an administrator account to call the reset
entrypoint in the contract from the previous example.
This version adds an administrator address to the contract storage.
It checks the sender of the transaction in the reset
entrypoint and fails if the addresses don't match:
To generate test accounts, pass a nat to the Test.Next.Account.address
function, which returns an address.
Then use the Test.Next.State.set_source
function to set the source account for transactions.
This example creates an admin account and user account.
It attempts to call the reset
entrypoint as the user account and expects it to fail.
Then it calls the reset
entrypoint as the admin account and verifies that the entrypoint runs correctly:
By default, the test simulation has two test accounts.
To create more, pass the number of accounts and a list of their balances or an empty list to use the default balance to the Test.Next.State.Reset
function, as in the following example.
The default balance is 4000000 mav minus %5 that is frozen so the account can act as a validator.
Testing events
To test events, emit them as usual with the Mavryk.emit
function and use the Test.Next.State.last_events
function to capture the most recent events, as in this example:
Unit testing functions
You can use the run test
command to run unit tests of functions.
A common way of unit testing functions is to create a map of input values and expected output values and iterate over them. For example, consider a map binding addresses to amounts and a function removing all entries in that map that have an amount less than a given threshold:
You can test this function against a range of thresholds with the LIGO test framework.
First, include the file under test and reset the state with 5 bootstrap accounts:
Now build the balances
map that serves as the test input:
The test loop will call the function with the compiled map
defined above, get the size of the resulting map, and compare it to an
expected value with Test.Next.Compare.eq
.
The call to remove_balances_under
and the computation of the size of the resulting map is achieved through the primitive Test.Next.Michelson.run
.
This primitive runs a function on an input, translating both (function and input)
to Michelson before running on the Michelson interpreter.
More concretely Test.Next.Michelson.run f v
performs the following:
- Compiles the function argument
f
to Michelsonf_mich
- Compiles the value argument
v
(which was already evaluated) to Michelsonv_mich
- Runs the Michelson interpreter on the code
f_mich
with the initial stack[ v_mich ]
The function that is being compiled is called tester
.
We also print the actual and expected sizes for good measure.
Here is the complete test file:
You can now execute the test by running this command:
The response shows the expected and actual results of each test run:
Testing with ligo run interpret
The command ligo run interpret
interprets a LIGO expression in a
context initialised by a source file. The interpretation is done using
Michelson's interpreter.
For example, suppose you have a function that encodes input values into a specific format. This function takes two input values and uses them as the key and value for an entry in a map:
To encode values with this function, pass the LIGO expression to call the function to the run interpret
command and include the LIGO file in the --init-file
argument:
The response is the Michelson-encoded value of the output of the function:
You can use the run interpret
command to interpret complex LIGO code and get the output in Michelson, such as formatting parameters for calls to entrypoints.
You can pass these arguments to set parameters for the interpretation:
--amount
: The amount of mav to send with the transaction; the default is 0--balance
: The amount of mav in the contract; the default is 0--now
: The current timestamp, such as2000-01-01T10:10:10Z
--sender
: The address for the sender of the transaction--source
: The address for the source of the transaction
Testing with ligo run dry-run
The ligo run dry-run
command runs the contract in a simulated
environment. You can use it to test contracts with given parameters
and storage values. You pass these arguments to the command:
- The contract file to run
- The parameter to pass to the contract, as a LIGO expression
- The value of the contract storage, as a LIGO expression
For example, this contract stores a number and allows callers to increment it by one:
This command tests the contract with the run dry-run
command:
The result shows the new value of the storage:
For a more complicated example, this contract stores a map and provides an entrypoint that updates elements in it:
You can test the entrypoint and view the resulting operations and storage by running this command, which uses an empty map of the same type as the contract storage as the initial value of the storage:
Note that the values of the parameter and the initial storage state are both LIGO expressions.
The result shows the empty list of operations and the new value of the storage, expressed by adding elements to an empty map:
You can pass these arguments to set parameters for the transaction:
--amount
: The amount of mav to send with the transaction; the default is 0--balance
: The amount of mav in the contract; the default is 0--now
: The current timestamp, such as2000-01-01T10:10:10Z
--sender
: The address for the sender of the transaction--source
: The address for the source of the transaction