Playing with SmartPy Smart Contracts: Part 2

Mathis Selvi
Tezos Israel
Published in
7 min readNov 13, 2020

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:

Example of a test, using SmartPy.io

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.

Alice participates in the crowdfunding with 100 tez

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:

Bob contributes 35 tez and Elie contributes 250 tez

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 funds
  • someoneElse 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!

Owner requests his funds

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.

Someone else requests the funds

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!

--

--

Mathis Selvi
Tezos Israel

🛠️ Working on Tezos 👟 Fashion Enthusiast 💿 Music Lover