Playing with SmartPy Smart Contracts: Part 2
In the first part, we coded our first smart contract with SmartPy, a very basic crowdfunding system, simplified as the goal was (and still is) to learn the basics of SmartPy.
However, SmartPy was not made just to write smart contracts. Indeed, it also comes with some testing tools which allow developers to quickly test their smart contract. Let’s see how to use them!
As a reminder, here is the link to the smart contract: click here.
Testing our basic crowdfunding smart contract
In this article, we will be testing a few different things:
- Initiating the contract
- Participating in the crowdfunding (aka sending funds to the smart contract) with different people
- Requesting the funds as the Owner (which should work)
- Requesting the funds as someone else (which should NOT work, because only the owner of the smart contract can request his funds)
Let’s get right into it! The first thing we need to do is to add a test decorator so that SmartPy knows we’re defining a new test. Here is the line to do so:
@sp.add_test(name = "BasicCrowdfundingTest")
As you can see, you can put in the name of your choice to name
. Now, we simply add the method header right after our test definition:
def test():
We’re now ready to start writing our test! :)
To make it simple, to test a SmartPy smart contract, we use test scenarios. So we will need to create a test scenario for our test. Let’s create one and call it scenario
by calling the test_scenario()
method.
scenario = sp.test_scenario()
Easy, right?
Before we continue, here is an important detail: basically, SmartPy.io computes the scenario and then displays it as an HTML document on their online IDE. Here is an example of what it will look like, in our case:
As you can see in the example above, we see some headings as well as a balance
, our smart contract storage
and the different entry points.
So far, in our smart contract test, here is what we have:
@sp.add_test(name = "BasicCrowdfundingTest")
def test():
scenario = sp.test_scenario()
We can now work with that scenario
to add some headings and conduct our test. To add a heading, simply call the scenario.h1()
(replace h1()
with h2()
or h3()
depending on the type of heading you want). Let’s do so:
scenario.h1("Basic Crowdfunding Test")
scenario.h2("[TEST] Initiating the contract")
Under our h2
heading, let’s start our first test. For this, we will need an owner with a Tezos address, that we can provide by calling the address()
method. In this article, we will give our “testing users” (including the Owner) some explicit addresses (whereas, in reality, it would be a real tz1
address).
owner = sp.address("tz1-owner-address")
Here, our owner’s address is now tz1-owner-address
. As we will need another address for the last test (where someone other than the owner attempts to request the funds from the smart contract), I will go ahead and create this someone else and give him an address right now (but we will use it later).
someoneElse = sp.address("tz1-someoneElseAddress")
Same method, different person, different address.
Quick recap, here is how our testing code looks so far:
@sp.add_test(name = "BasicCrowdfundingTest")
def test():
scenario = sp.test_scenario()
scenario.h1("Basic Crowfunding Testing")
scenario.h2("[TEST] Initiating the contract")
owner = sp.address("tz1-owner-address")
someoneElse = sp.address("tz1-someoneElseAddress")
We’re now ready to initiate our smart contract. As we’ve seen in the first part, we need to pass our owner
when initiating our smart contract. Let’s create our contract, name it c1
and call our smart contract.
c1 = BasicCrowdfunding(owner)
scenario += c1
Done. As you can see, we pass owner
to your BasicCrowdfunding
contract. And then, we simply add our c1
contract to the scenario
(in the second line).
We now have our very first test! If you were to run this in SmartPy, it would display what we’ve seen in the example screenshot above.
After this test, our smart contract is launched and ready to receive contributions. Let’s contribute then, shall we?
First, let’s add a new title to separate our tests.
scenario.h2("[TEST] Sending funds")
To contribute to this crowdfunding, we will take 3 different people who will send different amounts of tez to the contract, and we will need to create them test accounts. Let’s do so:
Alice = sp.test_account("Alice")
Bob = sp.test_account("Bob")
Elie = sp.test_account("Elie")
Alice, Bob and Elie now each have a test account used to call our smart contract. Let’s first start with Alice.
scenario.h3("Alice participates in the crowdfunding")
scenario += c1.addFunds().run(sender = Alice, amount = sp.tez(100))
What do we do here? First, we create a h3
heading for more visibility in the HTML document SmartPy displays. Then, we call our addFunds()
entry point from our c1
smart contract. The .run(sender = Alice, amount = sp.tez(100))
method allows us to specify as who we’re calling the contract (Alice
in our case) as well as the amount of tez we want to contribute, 100 tez (which is basically the amount Alice would send to the smart contract).
Let’s run this test on SmartPy and see how it goes.
Awesome! As we can see, our addFunds
method was called, and the new balance is now 100 tez, which reflects in the smart contract storage
with the ContributedAmount
being 100 as well.
Let’s do the same with Bob and Elie!
scenario.h3("Bob participates in the crowdfunding")
scenario += c1.addFunds().run(sender = Bob, amount = sp.tez(35))
scenario.h3("Elie participates in the crowdfunding")
scenario += c1.addFunds().run(sender = Elie, amount = sp.tez(250))
This time, Bob sends 35 tez and Elie sends 250 tez. Let’s have a look at the results:
Seems to be going well. After Bob contributes, ContributedAmound
is 135. Then, once Elie calls the contract and participates as well, with 250 tez, the new ContributedAmount
is now 385 tez.
Quick recap, here is how our testing code looks so far:
@sp.add_test(name = "BasicCrowdfundingTest")
def test():
scenario = sp.test_scenario()
scenario.h1("Basic Crowfunding Testing")
scenario.h2("[TEST] Initiating the contract")
owner = sp.address("tz1-owner-address")
someoneElse = sp.address("tz1-someoneElseAddress")
c1 = BasicCrowdfunding(owner)
scenario += c1
scenario.h2("[TEST] Sending funds")
Alice = sp.test_account("Alice")
Bob = sp.test_account("Bob")
Elie = sp.test_account("Elie")
scenario.h3("Alice participates in the crowdfunding")
scenario += c1.addFunds().run(sender = Alice, amount = sp.tez(100))
scenario.h3("Bob participates in the crowdfunding")
scenario += c1.addFunds().run(sender = Bob, amount = sp.tez(35))
scenario.h3("Elie participates in the crowdfunding")
scenario += c1.addFunds().run(sender = Elie, amount = sp.tez(250))
Finally, we just have two more things to test:
owner
requests his fundssomeoneElse
requests the funds
Let’s go ahead and try to request funds as the owner.
scenario.h2("[TEST] Owner cashing out funds - should work")
scenario += c1.cashoutFunds().run(sender = owner)
Same as usual, we create an h2
heading for more visibility. We then call the cashoutFunds()
method from our c1
contract, which we run()
as the owner, by passing sender = owner
to the run()
method.
Let’s run it and see!
It works! As we can see, the transaction is OK
, and we transfered the whole ContributedAmount
(from the balance) to the owner (tz1-owner-address
).
We should try with someoneElse
now. Remember, in the beginning of this article we created a someoneElse
variable and assigned him with the "tz1-someoneElseAddress"
. We will now use this variable.
scenario.h2("[TEST] Someone else cashing out funds - should not work")
scenario += c1.cashoutFunds().run(sender = someoneElse, valid = False)
This time, we pass sender = someoneElse
to run()
method. Do not mind the valid = False
, it’s simply because, by default, valid
is set to True
and if the validity of a transaction does not match its expected validity, SmartPy shows an alert. In this test, we know for sure it is not going to work, since we’re calling the contract as someone else (not the owner) to request the funds, but we still want SmartPy to display our test (and not an alert) so we set valid
to False
manually.
And voila! As we can see, the transaction is KO
and did not go through. Why? Well, simply because in our smart contract, when doing the cashoutFunds()
entry point, we check that the person who called the contract (the sender
) is the owner. SmartPy displays the following error:
Error: WrongCondition in line 16: sp.sender == self.data.admin
Indeed, the sender
(someoneElse
)’s address is not the same address as we stored in storage
, under self.data.admin
(the owner
‘s address).
Also as you can see, the balance is 0 tez. It’s normal: in our previous test, the owner requested his funds, so the balance was transferred to him. ;)
Here is how the whole testing code should look:
@sp.add_test(name = "BasicCrowdfundingTest")
def test():
scenario = sp.test_scenario()
scenario.h1("Basic Crowfunding Testing")
scenario.h2("[TEST] Initiating the contract")
owner = sp.address("tz1-owner-address")
someoneElse = sp.address("tz1-someoneElseAddress")
c1 = BasicCrowdfunding(owner)
scenario += c1
scenario.h2("[TEST] Sending funds")
Alice = sp.test_account("Alice")
Bob = sp.test_account("Bob")
Elie = sp.test_account("Elie")
scenario.h3("Alice participates in the crowdfunding")
scenario += c1.addFunds().run(sender = Alice, amount = sp.tez(100))
scenario.h3("Bob participates in the crowdfunding")
scenario += c1.addFunds().run(sender = Bob, amount = sp.tez(35))
scenario.h3("Elie participates in the crowdfunding")
scenario += c1.addFunds().run(sender = Elie, amount = sp.tez(250))
scenario.h2("[TEST] Owner cashing out funds - should work")
scenario += c1.cashoutFunds().run(sender = owner)
scenario.h2("[TEST] Someone else cashing out funds - should not work")
scenario += c1.cashoutFunds().run(sender = someoneElse, valid = False)
What’s next?
That’s it for the second part. In the next part, we’ll be looking into deploying the smart contract directly from SmartPy.io :) Stay tuned!