Skip to main content

Testing Contracts

Registering and displaying contractsโ€‹

@sp.add_test(name = "A Test")
def test():
# Create a scenario
scenario = sp.test_scenario()
# Instantiate a contract
c1 = MyContract()

# Add the contract to the scenario
scenario += c1 # which is equivalent to `scenario.register(c1, show = True)`

# To only register the smart contract but not show it
scenario.register(c1)

Contract origination optionsโ€‹

Initial storageโ€‹

Contract storages are typically determined in the contract constructors but it's possible to do it right before registering them inside scenarios.

    c2 = MyContract()
c2.init_storage(sp.record(a = 12, b = True))
scenario += c2

Initial balanceโ€‹

Additionally to entry points, contracts have an additional method that can be called once, before origination.

c1.set_initial_balance(expression)

Test accountsโ€‹

Test accounts can be defined by calling sp.test_account(seed) where seed is a string.

A test account contains some fields:

  • <account>.address
  • <account>.public_key_hash
  • <account>.public_key
  • <account>.secret_key

See Cryptography

admin = sp.test_account("Administrator")
alice = sp.test_account("Alice")
bob = sp.test_account("Robert")

Calls to entry pointsโ€‹

# Call entry_point
c1.my_entrypoint(12)
# Call entry_point with customized block attributes.
c1.my_entrypoint(13).run(
sender = None, # sp.TAddress
source = None, # sp.TAddress
chain_id = None, # sp.TChainId
level = None, # sp.TNat
now = None, # sp.TTimestamp
voting_powers = None, # sp.TMap(TKeyHash, TNat)

amount = None, # sp.TMutez
valid = True, # sp.TBool
show = True, # sp.TBool
exception = None, # any
)

The .run(...) method and its parameters are all optional.

Calls optional argumentsโ€‹

Generic context arguments are:

ParameterTypeAccessorDescription
sendersp.TAddress or sp.test_accountsp.senderThe simulated sender of the transaction.
Specific to call.
sourcesp.TAddress or sp.test_accountsp.sourceThe simulated source of the transaction.
Specific to call.
chain_idsp.TChainIdsp.chain_idThe simulated chain_id.
Preserved until changed.
levelsp.TNatsp.levelThe simulated block level.
Preserved until changed.
nowsp.TTimestampsp.nowThe simulated block timestamp.
Preserved until changed.
voting_powerssp.TMap(sp.TKeyHash, sp.TNat)sp.total_voting_power,
sp.voting_power
The simulated voting powers for the test.
Preserved until changed.

Specific context arguments are:

ParameterTypeDescription
amountsp.TMutezThe simulated amount sent. It populates sp.amount.
validsp.TBoolTells the interpreter if the transaction is expected to fail or not. True by default.
showsp.TBoolShow or hide the transaction. True by default.
exceptionany typeThe expected exception raised by the transaction. If present, valid must be False.

Exceptionsโ€‹

sp.is_failing(<expression>)

Returns True when an expression (views, lambdas, ...) results in failure, and False when the expression succeeds.

Content of exceptionsโ€‹

sp.catch_exception(<expression>, t = <type of the exception>)

t is optional, just using sp.catch_exception(<expression>) will be valid in most situations.

This method is used to test failing conditions of expressions (views, lambdas, ...). It returns an sp.TOption of type t that will contain sp.some(<exception>) when the expression fails or sp.none if it succeeds.

Viewsโ€‹

Views, both off-chain or on-chain, can now be called from test scenarios the same way as entry points. The example below shows how to do it.

Exampleโ€‹

import smartpy as sp

class MyContract(sp.Contract):
def __init__(self, param):
self.init(param)

@sp.offchain_view()
def state(self, param):
sp.verify(param < 5, "This is false: param > 5")
sp.result(self.data * param)

@sp.add_test(name = "Minimal")
def test():
scenario = sp.test_scenario()
c1 = MyContract(1)
scenario += c1

""" Test views """

# Display the offchain call result
scenario.show(c1.state(1))

# Assert the view result
scenario.verify(c1.state(2) == 2)
# Assert call failures
scenario.verify(sp.is_failing(c1.state(6))); # Expected to fail
scenario.verify(~ sp.is_failing(c1.state(1))); # Not expected to fail

# Assert exception result
# catch_exception returns an option:
# sp.none if the call succeeds
# sp.some(<exception>) if the call fails
e = sp.catch_exception(c1.state(7), t = sp.TString)
scenario.verify(e == sp.some("This is false: param > 5"))

Document extra informationโ€‹

The following elements represent six levels of section headings.

<h1> is the highest section level and <p> is the lowest.

scenario.h1("a title")
scenario.h2("a subtitle")
scenario.h3('Equivalent to <h3> HTML tag.')
scenario.h4("Equivalent to <h4> HTML tag.")
scenario.p("Equivalent to <p> HTML tag.")

Expressionsโ€‹

Showing expressionsโ€‹

To show expressions, we use scenario.show(expression, html = True, stripStrings = False).

ParameterTypeDescription
htmlboolTrue by default, False to export not in html but like in source code.
stripStringsboolFalse by default, True to remove quotes around strings.
scenario.show(expression, html = True, stripStrings = False)

scenario.show(c1.data.myParameter1 * 12)
scenario.show(c1.data)

Computing expressionsโ€‹

To compute expressions, we use scenario.compute(<expression>, **context_args).

x = scenario.compute(c1.data.myParameter1 * 12)

The variable x can now be used in the sequel of the scenario and its value is fixed.

Compute optional argumentsโ€‹

Generic context arguments are:

ParameterTypeAccessorDescription
sendersp.TAddress or sp.test_accountsp.senderThe simulated sender of the computation.
Specific to computation.
sourcesp.TAddress or sp.test_accountsp.sourceThe simulated source of the computation.
Specific to computation.
chain_idsp.TChainIdsp.chain_idThe simulated chain_id.
Preserved until changed.
levelsp.TNatsp.levelThe simulated block level.
Preserved until changed.
nowsp.TTimestampsp.nowThe simulated block timestamp.
Preserved until changed.
voting_powerssp.TMap(sp.TKeyHash, sp.TNat)sp.total_voting_power,
sp.voting_power
The simulated voting powers for the test.
Preserved until changed.

Data associated with contractsโ€‹

When a variable c1 represents a contract in a scenario, we can access some associated data:

  • c1.data

    Retrieve contract storage.

  • c1.balance

    Retrieve contract balance.

  • c1.baker

    Retrieve its optional delegated baker.

  • c1.address

    Retrieve its address within the scenario.

    In storage or similar circumstances, deployed contracts get addresses of the form:

    • KT1TezoooozzSmartPyzzSTATiCzzzwwBFA1
    • KT1Tezooo1zzSmartPyzzSTATiCzzzyfC8eF
    • KT1Tezooo2zzSmartPyzzSTATiCzzzwqqQ4H
    • KT1Tezooo3zzSmartPyzzSTATiCzzzseJjWC
    • KT1Tezooo4zzSmartPyzzSTATiCzzzyPVdv3
    • KT1Tezooo5zzSmartPyzzSTATiCzzzz48Z4p
    • KT1Tezooo6zzSmartPyzzSTATiCzzztY1196
    • KT1Tezooo7zzSmartPyzzSTATiCzzzvTbG1z
    • KT1Tezooo8zzSmartPyzzSTATiCzzzzp29d1
    • KT1Tezooo9zzSmartPyzzSTATiCzzztdBMLX
    • KT1Tezoo1ozzSmartPyzzSTATiCzzzw8CmuY
    • ...
  • c1.typed

    Retrieve its testing typed contract value.

    To access entry points, one can use field notation:

    • c1.typed.my_entry_point: to access typed entry point my_entry_point of contract c1.

Dynamic contractsโ€‹

See reference Create Contract template.

Internally, SmartPy uses two types of contracts:

  • Static contracts which appear explicitly in the scenarios.
  • Dynamic contacts which are created in other contracts executed in the scenario (with sp.create_contract).

Declaring a dynamic contract of dynamic id (an integer) with the corresponding storage and full parameter types. The first dynamically created contractId is 0, then 1, etc.

dynamic_contract = scenario.dynamic_contract(contractId, tcontract, tparameter)

Return a dynamic contract that contains regular fields data, balance, baker, address, typed and a call(...) method.

Dynamic contracts addresses are of the form:

  • KT1TezoooozzSmartPyzzDYNAMiCzzpLu4LU
  • KT1Tezooo1zzSmartPyzzDYNAMiCzztcr8AZ
  • KT1Tezooo2zzSmartPyzzDYNAMiCzzxyHfG9
  • KT1Tezooo3zzSmartPyzzDYNAMiCzzvqsJQk
  • KT1Tezooo4zzSmartPyzzDYNAMiCzzywTMhC
  • KT1Tezooo5zzSmartPyzzDYNAMiCzzvwBH3X
  • KT1Tezooo6zzSmartPyzzDYNAMiCzzvyu5w3
  • KT1Tezooo7zzSmartPyzzDYNAMiCzztDqbVQ
  • KT1Tezooo8zzSmartPyzzDYNAMiCzzq2URWu
  • KT1Tezooo9zzSmartPyzzDYNAMiCzzwMosaF
  • KT1Tezoo1ozzSmartPyzzDYNAMiCzzzknqsi

Call method

Send the parameter to the dynamic contract entry_point.

It is also possible to use .run(...) on the generated call as described in Registering and Displaying Calls to Entry Points.

dynamic_contract.call(entry_point, parameter)
dynamic_contract.call(entry_point, parameter).run(
sender = ..., # TAddress
source = ..., # TAddress
amount = ..., # TMutez
now = ..., # TTimestamp
level = ..., # TNat
chain_id = ..., # TChainId
voting_powers = ..., # Dict(TKeyHash, TNat)
valid = ..., # True | False
show = ..., # True | False
exception = ..., # any
)

Assertionsโ€‹

To verify conditions, we use scenario.verify(<condition>).

To verify an equality condition, we can also use scenario.verify_equal(<left_expression>, <right_expression>) which works on both comparable and non-comparable types.

scenario.verify(c1.data.myParameter == 51)

scenario.verify_equal(c1.data.myList, [2, 3, 5, 7])

Interactive testingโ€‹

To test interactively a contract, we use scenario.simulation(<contract>).

It also provides a step-by-step mode that is very useful to understand some computation.

scenario.simulation(c1)