Testing contracts
After you have created a test scenario as described in Test scenarios, you can originate (deploy) your contracts to it and test their operation.
TIP
All work with smart contracts must take place inside the test scenario. For example, you can't check the storage of a contract with a command like if contract.data.storagevalue == 5
. Instead, you must evaluate the state of the contract within the scenario, as in scenario.verify(contract.data.storagevalue == 5)
.
Instantiating and originating contracts
To originate a contract within a test scenario, create an instance of the contract, which automatically calls its class's __init__()
method. Then add it to the scenario with the +=
operator, as in this example:
def main():
class MyContract(sp.Contract):
def __init__(self):
pass
class MyContract2(sp.Contract):
def __init__(self, x):
self.data.x = x
pass
class MyContract3(sp.Contract):
def __init__(self, x, y):
self.data.x = x
self.data.y = y
@sp.add_test()
def test():
scenario = sp.test_scenario(main, "A Test")
c1 = main.MyContract()
c2 = main.MyContract2(2)
c3 = main.MyContract3(x=5, y=2)
scenario += c1
scenario += c2
scenario += c3
Testing entrypoints
- scenario.verify(expression)
To verify conditions within a scenario, use
scenario.verify(<condition>)
.smartpyscenario.verify(contract.data.myParameter == 51)
- scenario.verify_equal(expr1, expr2)
To verify an equality condition, use
scenario.verify_equal(<left_expression>, <right_expression>)
which works on both comparable and non-comparable types.smartpyscenario.verify_equal(contract.data.myList, [2, 3, 5, 7])
After you create an instance of a contract and add it to the scenario, you can call its entrypoints as methods on the contract object. Then you can use functions such as scenario.verify
to verify things in the test scenario such as the current state of the contract storage. For example, this test scenario calls an entrypoint and verifies that its storage changed as expected:
@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):
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
scenario = sp.test_scenario("Test for my smart contract", 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
# Check the expected value in the contract storage
scenario.verify(contract.data.value == 5)
# Call an entrypoint
contract.increment(3)
# Check the expected value in the contract storage
scenario.verify(contract.data.value == 8)
If the entrypoint accepts more than one parameter, you must pass the values as a record or as an implied record by naming the values, as in this example:
scenario = sp.test_scenario(main, "A Test")
contract = main.MyContract()
scenario += contract
# Implied record
contract.exampleEntrypoint(a=5, b=6)
# Explicit record
contract.exampleEntrypoint(sp.record(a=5, b=6))
After the entrypoint parameters, you can include any of these context parameters to change how the entrypoint runs and is evaluated:
Parameter | Type | Accessor | Description |
---|---|---|---|
_sender | sp.address or sp.test_account | sp.sender | The simulated sender of the computation. Specific to computation. |
_source | sp.address or sp.test_account | sp.source | The simulated source of the computation. Specific to computation. |
_chain_id | sp.chain_id | sp.chain_id | The simulated chain_id. Preserved until changed. |
_level | sp.nat | sp.level | The simulated block level. Preserved until changed. |
_now | sp.timestamp | sp.now | The simulated block timestamp. Preserved until changed. |
_voting_powers | sp.map[sp.key_hash, sp.nat] | sp.total_voting_power , sp.voting_power | The simulated voting powers for the test. Preserved until changed. |
_amount | sp.mutez | sp.amount | The simulated amount of mutez to send with the smart contract call. |
_valid | sp.bool | None | Tells the interpreter if the transaction is expected to fail or not. True by default. |
_exception | any type | None | The expected exception raised by the transaction. If present, valid must be False. |
For example, to test when an entrypoint throws an exception, pass _valid = False
, as in this example:
@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():
scenario = sp.test_scenario("Test for my smart contract", main)
contract = main.MyCounter(5)
scenario += contract
# Call an entrypoint in a way that causes an exception
contract.increment(10, _valid = False)
- sp.catch_exception(expression, [t]) → sp.option[t]
Catches an exception in a expression.
The parameter
t
is optional; usingsp.catch_exception(<expression>)
is valid in most situations.This method is used to test failing conditions of expressions (views, lambdas, ...). It returns an sp.option of type
t
that containsp.Some(<exception>)
when the expression fails orNone
if it succeeds.
Contract instance methods
In addition to methods for each entrypoint, contracts have these instance methods:
- contract.get_source()
Returns the source code of the contract. See metadata.
- contract.get_offchain_views()
Returns the off-chain views of the contract. See metadata.
- contract.get_generated_michelson()
Returns the Michelson code the contract. See metadata.
- contract.get_error_map()
Returns the contract's error map. See metadata and
"exceptions"
flag.
These instance methods can be called only before adding the contract to the scenario:
- contract.set_initial_balance(balance: sp.tez)
Set the initial balance for the contract.
pythoncontract.set_initial_balance(sp.tez(20))
- contract.data = x: any
- Replace a field in the contract's storage.
This is mostly useful to initialize the metadata big map. See metadata.
Contract data
When a variable contract
represents a contract in a scenario, we can access some associated data:
contract.data
: Contract storagecontract.balance
: Contract balancecontract.baker
: Contract optional delegated bakercontract.address
: Contract address within the scenarioDetails
In storage or similar circumstances, deployed contracts get addresses of the form:
KT1TezoooozzSmartPyzzSTATiCzzzwwBFA1
KT1Tezooo1zzSmartPyzzSTATiCzzzyfC8eF
KT1Tezooo2zzSmartPyzzSTATiCzzzwqqQ4H
KT1Tezooo3zzSmartPyzzSTATiCzzzseJjWC
KT1Tezooo4zzSmartPyzzSTATiCzzzyPVdv3
KT1Tezooo5zzSmartPyzzSTATiCzzzz48Z4p
KT1Tezooo6zzSmartPyzzSTATiCzzztY1196
KT1Tezooo7zzSmartPyzzSTATiCzzzvTbG1z
KT1Tezooo8zzSmartPyzzSTATiCzzzzp29d1
KT1Tezooo9zzSmartPyzzSTATiCzzztdBMLX
KT1Tezoo1ozzSmartPyzzSTATiCzzzw8CmuY
- ...
contract.typed
Retrieve its testing typed contract value.
To access entrypoints, you can use field notation:
contract.typed.my_entrypoint
: Typed entrypoint my_entrypoint of contractcontract
.
Testing expressions (views and lambdas)
Testing views and lambdas is different from testing entrypoints because views and lambdas return SmartPy expressions. SmartPy does not evaluate these expressions until you tell it to.
- sp.is_failing(expression)
Returns True when an expression results in failure and False when the expression succeeds.
- sp.catch_exception(expression, [t]) → sp.option[t]
Evaluates an expression, catches any failures in it, and returns an option.
This method is used to test failing conditions of expressions. It returns an sp.option of type
t
that containssp.Some(<exception>)
when the expression fails orNone
when it succeeds. Thet
parameter is optional; in most cases you can leave it out.
- scenario.show(expression, html = True)
Evaluate an expression and writes its result to the log.
Parameter Type Description html bool True
by default,False
to export not in HTML but in source code format.pythonscenario.show(expression, html = True) scenario.show(contract.data.myParameter1 * 12) scenario.show(contract.data)
- scenario.compute(expression, **context_args) → t
Evaluate an expression and return its result.
smartpyx = scenario.compute(c1.data.myParameter1 * 12)
This example calls an on-chain view and then evaluates the expression it returns:
@sp.module
def main():
class MyCounter(sp.Contract):
def __init__(self, initialValue):
sp.cast(initialValue, sp.nat)
self.data.value = initialValue
@sp.onchain_view
def getValue(self):
return self.data.value
@sp.add_test()
def test():
scenario = sp.test_scenario("view_test", main)
contract = main.MyCounter(5)
scenario += contract
expression = contract.getValue()
scenario.show(expression)
scenario.verify(expression == 5)
This example tests an expression that fails. Note that the creation of the expression does not fail because SmartPy has not evaluated the expression yet. Then the test scenario tests the failure, which implicitly evaluates the expression.
@sp.module
def main():
class MyContract(sp.Contract):
@sp.onchain_view
def alwaysFails(self):
assert 1 == 2, "Failure"
@sp.add_test()
def test():
scenario = sp.test_scenario("expression_test", main)
contract = main.MyContract()
scenario += contract
expression = contract.alwaysFails() # No exception yet
scenario.verify(sp.is_failing(expression))
scenario.verify(
sp.catch_exception(expression, sp.string) == sp.Some("Failure")
)
Testing dynamic contracts
Contracts in SmartPy can be created in two ways, statically as shown above, or dynamically via a call to the [sp.create_contract] statement from within an entrypoint
.
We can refer to a contract that was created dynamically using scenario.dynamic_contract(<module>.<Contract>, offset)
, this returns a handle that can then be used as for static contracts.
- scenario.dynamic_contract(template_ref: sp.Contract, offset:int | None) → sp.contract[t]
Returns a handle to the dynamic contract of class type
template_ref=<module>.<Contract>
that was created atoffset
.The
template_ref
is used to check that the referenced contract has the correct class type.The
offset
, if given, specifies the position in the list of dynamic contracts created so far. Sooffset=0
would refer to the first dynamically created contract andoffset=-1
would refer to the most recently created dynamic contract. If not given it will default to the most recently created dynamic contract.For example, to refer to the most recently created dynamic contract and check the class type is
main.MyContract
, we would usepythondyn = scenario.dynamic_contract(main.MyContract)
Where-as
pythondyn = scenario.dynamic_contract(main.MyContract, offset=-2)
will refer to the last but one dynamic contract and check the class type is
main.MyContract
.The handle
dyn
can now be used to call entrypoints, verify data etcpythondyn.myEntrypoint(3) scenario.verify(dyn.data == 3)