Skip to content
On this page

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:

python
@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.

Importing modules

You must import modules into the test scenario before using them.

  • To import modules from separate SmartPy .spy files, use the add_module command.
  • To import inlined modules from the same Python file, either include them in the sp.test_scenario command or use the add_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.

python
scenario.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>.

python
scenario.p("Equivalent to <p> HTML tag.")
scenario.show(expression, html = True) → 

Write the result of an expression to the log.

ParameterTypeDescription
htmlboolTrue by default, False to export not in HTML but in source code format
python
scenario.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.

scenario.add_flag(flag, *args) → 

Add a flag to the test scenario.

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:

python
# Enable erase-comments flag
scenario.add_flag("erase-comments")
# Disable erase-comments flag
scenario.add_flag("no-erase-comments")
FlagDescriptionDefault Value
default-check-no-incoming-transferSets entrypoints in the scenario to fail when tez is sent with the smart contract callFalse
disable-dup-checkRemove the DUP protection on ticketsFalse
dump-michelDump Michel intermediate languageFalse
erase-commentsRemove compiler comments from output filesFalse
simplifySimplify output files by removing steps that don't effect resultsTrue
simplify-via-michelUse Michel intermediate languageFalse

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 and line options change error message strings to numbers to save space.
  • The unit option changes error message strings to UNIT.

The other values control only the generated message when you do not specify a message in the contract code.

LevelDescription
full-debugIncludes 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-messageIncludes reduced debugging information about the failure in generated error messages. This option is still very costly in terms of size and gas.
default-lineUses line numbers for generated error messages to indicate the line that caused the error.
metadata-idReplaces 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().
lineReplaces string error messages with an integer that points to the line number of the failure.
default-unitRetains string error messages when they are provided. Replaces all generated errors with UNIT.
unitReplaces 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:

python
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.