Test scenarios
Test scenarios are important not just for testing SmartPy code but for compiling contracts. Test scenarios mimic the Tezos blockchain to ensure that contracts work correctly before deployment. Then they generate the Michelson code for the contract and other metadata files that you can use in your dApps.
Unlike code within a SmartPy module, test scenario code is standard Python. Therefore, you can do things in test scenarios that you can't do in modules, such as using external libraries and calling external APIs.
INFO
You must define test scenarios in Python .py
files, not SmartPy .spy
files.
Test example
This code defines a simple smart contract and then creates a test scenario for it:
@sp.module
def main():
class MyCounter(sp.Contract):
def __init__(self, initialValue):
sp.cast(initialValue, sp.nat)
self.data.value = initialValue
@sp.entrypoint
def increment(self, update):
assert update < 6, "Increment by less than 6"
self.data.value += update
@sp.add_test()
def test():
# Create a test scenario
# Specify the output folder and a module or list of modules
# to import into the scenario
scenario = sp.test_scenario("MyCounter tests", main)
# Create an instance of a contract
# Automatically calls the __init__() method of the contract's class
contract = main.MyCounter(5)
# Add the contract to the scenario
scenario += contract
# Call an entrypoint
contract.increment(3)
# Call an entrypoint and expect it to fail
contract.increment(12, _valid = False)
# Check the expected value in the contract storage
scenario.verify(contract.data.value == 8)
Creating test scenarios
- @sp.add_test()
The
@sp.add_test()
annotation defines a block of code that contains one or more test functions.
- sp.test_scenario(name: str, modules: list[sp.module] | sp.module | None) → test_scenario
The
sp.test_scenario
function creates a test scenario, which is a simulated Tezos environment.Each test function should have only one test scenario and creating it should be the first instruction in the function. Then you can create instances of contracts and add them to the scenario to simulate originating them to Tezos.
The
sp.test_scenario
function accepts a name for the scenario and one module or a list of modules to import. These modules become available inside the test scenario, so at minimum the test must import the module that contains the smart contracts it tests.python@sp.module def main(): class MyContract(sp.Contract): pass ...etc @sp.add_test() def test(): # Create a test scenario scenario = sp.test_scenario("A Test", main)
WARNING
You must create the test scenario before instantiating any contracts because SmartPy uses the test scenario to pre-compile the contract.
After you create the test scenario, you can create instances of contracts and add them to the scenario as described in Testing contracts.
Running test scenarios
Test scenarios run automatically when you compile a contract with the python
command. For more information, see Compiling contracts.
Importing modules
You must import modules into the test scenario before using them.
- To import modules from separate SmartPy
.spy
files, use theadd_module
command. - To import inlined modules from the same Python file, either include them in the
sp.test_scenario
command or use theadd_module
command.
For more information about importing modules, see Modules.
TIP
When you import modules from SmartPy files, the add_module
command returns a module handle. You must assign this handle to a variable and use it to access the elements in the module.
- sc.add_module(module: filepath | sp.module) → module
To add a module from an
.spy
file to the test scenario, use the filepath of the.spy
file:python@sp.add_test() def test(): scenario = sp.test_scenario("A Test") m = sc.add_module("my/local/files/contracts.spy") contract = m.MyContract() scenario += contract
The handle
m
is a module and the definitions of any types, contracts, constants, and other elements within the imported module can be used in the test scenario in the same way as for inlined modules.INFO
For more information on how SmartPy resolves file paths to modules, see Filepath resolution.
To add an inlined module to the test scenario, use the module handle:
python@sp.module def main(): class MyContract(sp.Contract): pass # ...etc @sp.add_test() def test(): scenario = sp.test_scenario("A Test") scenario.add_module(main)
Logging
These functions write information to the test log, which is stored in [scenario_name]/log.txt
, where [scenario_name]
is the first parameter of the sp.test_scenario
function.
- scenario.h<N>(content: str)
Add a section heading of the level
<N>
.<h1>
is the highest section level.pythonscenario.h1("a title") scenario.h2("a subtitle") scenario.h3('Equivalent to <h3> HTML tag.') scenario.h4("Equivalent to <h4> HTML tag.")
- scenario.p(content: str)
Add a text paragraph to the scenario equivalent to
<p>
.pythonscenario.p("Equivalent to <p> HTML tag.")
- scenario.show(expression, html = True)
Write the result of an expression to the log.
Parameter Type Description html bool True
by default,False
to export not in HTML but in source code formatpythonscenario.show(expression, html = True) scenario.show(contract.data.myParameter1 * 12) scenario.show(contract.data)
Flags
Flags change how the compiler runs the simulation and compiles the contract.
Boolean flags
Boolean flags are activated by passing their name to the add_flag
function. To deactivate them, pass the name prefixed with "no-"
. For example:
# Enable erase-comments flag
scenario.add_flag("erase-comments")
# Disable erase-comments flag
scenario.add_flag("no-erase-comments")
Flag | Description | Default Value |
---|---|---|
default-check-no-incoming-transfer | Sets entrypoints in the scenario to fail when tez is sent with the smart contract call | False |
disable-dup-check | Remove the DUP protection on tickets | False |
dump-michel | Dump Michel intermediate language | False |
erase-comments | Remove compiler comments from output files | False |
simplify | Simplify output files by removing steps that don't effect results | True |
simplify-via-michel | Use Michel intermediate language | False |
The exceptions
flag
The exceptions
flag controls how the compiler renders exceptions in compiled contracts. For example, some values for this flag change error messages to integers to save space in the generated contract. Other values others provide debugging information in a string for the error message. This flag must be added to the test scenario before the contract's instantiation.
WARNING
The values metadata-id
, line
, and unit
replace error message strings that you specify in assert
and raise
statements:
- The
metadata-id
andline
options change error message strings to numbers to save space. - The
unit
option changes error message strings toUNIT
.
The other values control only the generated message when you do not specify a message in the contract code.
Level | Description |
---|---|
full-debug | Includes full debugging information about the failure in generated error messages, such as the type of failure, the line number, and parameters. This option is extremely costly in terms of size and gas. |
debug-message | Includes reduced debugging information about the failure in generated error messages. This option is still very costly in terms of size and gas. |
default-line | Uses line numbers for generated error messages to indicate the line that caused the error. |
metadata-id | Replaces string error messages with a nat and provides a mapping from nat to string to be put in the metadata. The error map is accessible via c.get_error_map() . |
line | Replaces string error messages with an integer that points to the line number of the failure. |
default-unit | Retains string error messages when they are provided. Replaces all generated errors with UNIT . |
unit | Replaces all error messages with UNIT . |
verify-or-line (the default) | Retains string error messages when they are provided. Replaces all generated errors with an integer that points to the line number of the failure. |
When the exceptions
flag's value is metadata-id
, the compiler replaces error message strings with a nat number. Then SmartPy generates a mapping between the nat number and the original message.
For example, this code replaces a string error message with a number and verifies that the number maps to the message. Then it stores the error message mapping in the contract metadata and uploads it to IPFS as described in Metadata:
import smartpy as sp
@sp.module
def main():
class MyErrorMessages(sp.Contract):
@sp.entrypoint
def alwaysFails(self):
raise "This is a long exception message."
@sp.add_test()
def test():
scenario = sp.test_scenario("MyErrorMessages", main)
scenario.add_flag("exceptions", "metadata-id")
contract = main.MyErrorMessages()
scenario += contract
try:
contract.alwaysFails()
except Exception as e:
# Verify exception number and message
assert e.value == "0"
assert e.expansion == "This is a long exception message."
# Add error message mapping to contract metadata
metadata_dict = sp.create_tzip16_metadata(error_map=contract.get_error_map())
# Pin error message mapping to IPFS
sp.pin_on_ipfs(metadata_dict, name = "Error messages for MyErrorMessages contract")
protocol
flag
The protocol
flag is used to specify a specific protocol. For example, scenario.add_flag("protocol", "Nairobi")
tells SmartPy to simulate Tezos and compile and run contracts with the Nairobi protocol.