Skip to content

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.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
    scenario = sp.test_scenario("MyCounter tests")

    # 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. Modules are auto-imported when interacting with a scenario element.

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")

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 the add_module command.
  • Inlines modules are automatically imported when accessing one of its elements. You can force the import by either including them in the sp.test_scenario command or using 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 = scenario.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, you may 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.

You can set flags in two ways:

  • In the SMARTPY_FLAGS environment variable, as in SMARTPY_FLAGS="--simplify" python welcome.py for Boolean flags or SMARTPY_FLAGS="--output ./output_folder" python welcome.py for flags that take parameters

  • With the scenario.add_flag function, as in scenario.add_flag("simplify") for Boolean flags or scenario.add_flag("output", "./output_folder") for flags that take parameters

Flags set with the SMARTPY_FLAGS environment variable override flags set with the scenario.add_flag function.

scenario.add_flag(flag, *args) → 

Add a flag to the test scenario.

Boolean flags

To activate a Boolean flag, pass its name to the scenario.add_flag function or prefix its name with -- in the SMARTPY_FLAGS environment variable. To deactivate them, pass its 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")

These Boolean flags are available:

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 duplicate protection for ticketsFalse
dump-michelDump Micheline 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
single-entrypoint-annotationAnnotate the entrypoint in a contract with only one entrypointTrue
single-entry-point-annotationEquivalent to single-entrypoint-annotationTrue

Flags that take parameters

To use a flag that takes parameters, pass its name and parameter to the scenario.add_flag function or prefix its name with -- in the SMARTPY_FLAGS environment variable and then add the parameter. For example:

python
# Compile for Rio protocol
scenario.add_flag("protocol", "rio")

These flags are available:

FlagDescriptionParametersDefault
exceptionsControls how the compiler renders exceptions in compiled contractsSee belowverify-or-line
outputThe folder to write output to or None to wrote no outputA path to a folder relative to the current working directory, overriding the value passed to sp.test_scenarioThe value passed to sp.test_scenario
protocolThe protocol to simulate for testing and compilationquebec, rio or seoulseoul

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")
    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")