Starting with SmartPy Part 3: Testing Patterns and Analytics
Verifying Smart Contract Behavior and Security
Writing a smart contract is half the battle, but now we have to perform thorough testing to ensure that our work performs correctly and securely. Usually, this would entail deploying our experimental contract to a testnet and repeatedly invoking the contract to test for unexpected behavior, which is a tiring and time-consuming process. SmartPy’s online editor relieves us from the majority of this burden with its built-in simulation suite, which enables local contract invocations and automatic assertion verification.
Just like the basic test we wrote in Part 1, we begin defining a SmartPy test with the annotation @addTest()
and the method header def test()
. Note that test name
s cannot have spaces. The testScenario
object includes functionality to render HTML to make the test output presentable.
We can start building our HTML output by creating a header element with h1("Event Planner")
. Then, we initialize firstOwner
and secondOwner
to be of type address
with the sp.address()
constructor for later use. Calling EventPlanner(firstOwner)
will create an instance of the EventPlanner
contract with firstOwner
as the owner
. We transform the contract into HTML with scenario += c1
.
@addTest(name = "AdvancedTest")
def test():
scenario = sp.testScenario()
# Create HTML output for debugging
scenario.h1("Event Planner")
# Initialize test addresses
firstOwner = sp.address("tz1-firstOwner-address-1234")
secondOwner = sp.address("tz1-secondOwner-address-5678")
# Instantiate EventPlanner contract
c1 = EventPlanner(firstOwner)
# Print contract instance to HTML
scenario += c1
The next step is to invoke the various entry points of the EventPlanner
in order to verify that it functions correctly. We can print the name of a specific invocation by adding the result of h2()
. An invocation is denoted by an expression of the form <contract>.<entryPoint>(<paramsField1> = <val1>, <paramsField2> = <val2>, <...>).run(sender = <address>)
, which will invoke the entryPoint
of contract
with params
encapsulating the fields paramsField1, paramsField2, …
representing the values val1, val2, …
respectively. run(sender = <address>)
will set the sp.sender
to the given address
, and turn the invocation details into HTML.
# Invoke EventPlanner entry points and print results to HTML
scenario.h2("Set date for Tezos Meetup to 11-28-2017")
scenario += c1.setDate(name = "Tezos Meetup", newDate = "11-28-2017").run(sender = firstOwner)
scenario.h2("Set number of guests for Tezos Meetup to 80")
scenario += c1.setNumGuests(name = "Tezos Meetup", newNumGuests = 80).run(sender = firstOwner)
scenario.h2("Change owner")
scenario += c1.changeOwner(newOwner = secondOwner).run(sender = firstOwner)
scenario.h2("New owner sets date for Tezos Meetup to 03-21-2019")
scenario += c1.setDate(name = "Tezos Meetup", newDate = "03-21-2019").run(sender = secondOwner)
scenario.h2("Old owner attempts to set date for Tezos Meetup")
scenario += c1.setDate(name = "Tezos Meetup", newDate = "10-15-2018").run(sender = firstOwner, valid = False)
Finally, we can perform automatic verification of the expected end state of the EventPlanner
after testing with scenario.verify()
statements. SmartPy will infer types for numerics and dates. When working with addresses, a special syntax must be followed, as shown by the comparison statement (c1.data.owner) == sp.address('tz1-secondOwner-address-5678')
.
# Verify expected results
scenario.verify((c1.data.nameToEvent["Tezos Meetup"].date) == '03-21-2019')
scenario.verify((c1.data.nameToEvent["Tezos Meetup"].numGuests) == 80)
scenario.verify((c1.data.owner) == sp.address('tz1-secondOwner-address-5678'))
We’re completed our exploration of the SmartPy simulation suite! Here is the full EventPlanner
test for your reference:
@addTest(name = "AdvancedTest")
def test():
scenario = sp.testScenario()
# Create HTML output for debugging
scenario.h1("Event Planner")
# Initialize test addresses
firstOwner = sp.address("tz1-firstOwner-address-1234")
secondOwner = sp.address("tz1-secondOwner-address-5678")
# Instantiate EventPlanner contract
c1 = EventPlanner(firstOwner)
# Print contract instance to HTML
scenario += c1
# Invoke EventPlanner entry points and print results to HTML
scenario.h2("Set date for Tezos Meetup to 11-28-2017")
scenario += c1.setDate(name = "Tezos Meetup", newDate = "11-28-2017").run(sender = firstOwner)
scenario.h2("Set number of guests for Tezos Meetup to 80")
scenario += c1.setNumGuests(name = "Tezos Meetup", newNumGuests = 80).run(sender = firstOwner)
scenario.h2("Change owner")
scenario += c1.changeOwner(newOwner = secondOwner).run(sender = firstOwner)
scenario.h2("New owner sets date for Tezos Meetup to 03-21-2019")
scenario += c1.setDate(name = "Tezos Meetup", newDate = "03-21-2019").run(sender = secondOwner)
scenario.h2("Old owner attempts to set date for Tezos Meetup")
scenario += c1.setDate(name = "Tezos Meetup", newDate = "10-15-2018").run(sender = firstOwner, valid = False)
# Verify expected results
scenario.verify((c1.data.nameToEvent["Tezos Meetup"].date) == '03-21-2019')
scenario.verify((c1.data.nameToEvent["Tezos Meetup"].numGuests) == 80)
scenario.verify((c1.data.owner) == sp.address('tz1-secondOwner-address-5678'))
Here’s the full contract, with tests pre-loaded in the IDE.