Lesson 7

Test-driven Solidity

2

Since our contracts are mainly interacted with through tests during the development process, it makes sense for us to adopt a test-driven process while coding.

That's why, in this chapter, we're going to iterate on our app by writing the tests first, and then implement the necessary features in our contracts to make them pass. Let's get started!

Retrieving the user info

The next step in our DApp is being able to retrieve a user's info (for now, that's only the ID and username) based on their ID. To do this in a test-driven manner, let's start by writing the test.

If you want to return structures in Solidity, you'll have to convert them to tuples, which in turn will be interpreted as arrays in the JavaScript environment.

Therefore, if our structure looks like this...

...we should expect to get a JavaScript array where the first element (of index 0) represents the ID, and the second one (of index 1) is the username.

test/integration/users.js

This seems right, now let's write that getUserFromId function!

contracts/users/UserStorage.sol

Alright, let's try this out. If you run truffle test, you'll see that our test unfortunately fails:

Oh no!

The reason the function returns "0x7472697374616e00000000..." instead of "tristan" is because it's hex-encoded. Whenever we retrieve a bytes32 value in a JavaScript environment, we have to convert it to a string before using it in our assertions!

Again, the web3 library has a utility function called toAscii which we can use:

test/integration/users.js
Hmm, still failing?

As you can see, the test still fails though because the returned string has a bunch of \u0000 characters at the end of it. Again, this is due to the fact that it's a bytes32 object and therefore must be exactly 32 characters long. \u000 is simply a representation of a "null" character.

In order to remove these trailing null characters, we can simply replace them through a simple JavaScript RegEx:

test/integration/users.js
Finally it passes!

Using a public state variable

While our Solidity code above works great, there's actually a cleaner way to get all information for a profile.

By adding the keyword public in front of our profiles state variable, Solidity will automatically generate the getter function for us. In other words, we can skip the getUserFromId function altogether!

contracts/users/UserStorage.sol

This is a great time saver and also makes our code much more maintainable, since we don't have to return every single struct field in a separate function.

In our test file, we don't have to change anything, except that we call storage.profiles instead of storage.getUserFromId.

test/integration/users.js

Run your test again, and you'll see that it works just as well as before. Nifty!

Creating the TweetStorage contract

Now that we're getting a hang of testing, let's write some more for our upcoming TweetStorage contract!

Make a new folder called tweets inside your contracts folder, and add a file called TweetStorage.sol in it:

Our updated file structure!
contracts/tweets/TweetStorage.sol

As usual, we also need to add a line to our deployment file so that our test can actually interact with the contract:

migrations/2_deploy_storage.js

Now that that's out of the way, the things that we are interested in testing are the following:

  1. Creating a new tweet (and get its newly added ID)

  2. Get a tweet's data based on its ID (for now, that info will be the tweet's ID, text, author ID and creation date).

As we saw with our UserStorage, integration tests (in JavaScript) are a great way to verify the behaviour of our contracts from a user's perspective, whereas unit tests (in Solidity) are necessary to get the actual returned data from a writable function that performs a transaction.

When creating a tweet, we expect to be able to call a function named createTweet in the TweetStorage contract, pass a user ID and a text string as data, and get the newly created tweet's ID in return. In order to verify that tweet ID's value, we'll need to write a unit test.

Here's how we could write that test:

test/unit/TestTweetStorage.sol
If we try to run it, it will obviously fail, since the "createTweet" function doesn't exist yet.

In order for it to pass, we're going to use some logic that's very similar to the one in the UserStorage contract, using a struct, a mapping, and a state variable keeping track of the latest ID:

contracts/tweets/TweetStorage.sol

Note that we're using string and not bytes32 to store our tweet text. As we know, bytes32 has a maximum limit of 32 characters, which isn't nearly enough for a tweet, so it's not very suitable for this use case.

We're also using the the built-in now variable – which uses the current block's Unix timestamp – to set the time when the tweet was posted.

Run the tests again, and you'll see that they pass!

Getting the Tweet data

Now we can move on to the second test – getting the tweet's data! This one on the other hand, can be written in JavaScript. In fact, writing a unit test for this would result in an error since you cannot pass strings from one contract to another in Solidity.

Create a new file called tweets.js in test/integration and add the following code:

test/integration/tweets.js

Notice how we're using the parseInt function before we compare the tweetId and userId to a number. The reason for this is that web3 uses a special number type (called bigNumber), in order to support Ethereum's standard numeric data type (which is much larger than the one built into JavaScript). Since we're dealing with such small numbers in this test, it's okay to convert them like this.

This all looks good! However, if we run the test...

Oh no! Not quite there yet...


It seems like our tweet hasn't been created, since it returns an ID of 0 instead of 1. But didn't we create the tweet earlier in our Solidity test?

Actually no. Our Solidity tests and JavaScript tests are completely separate! Just because we've created the tweet in our Solidity test, does not mean we can retrieve it in our JavaScript test – each test in Truffle uses a clean room environment so that they don't accidentally share state with each other (which is a good thing)!

The solution is that we'll simply have to recreate the tweet in our tweets.js file as well. We can do this in a special before() function, which runs before any other test:

test/integration/tweets.js
And now it passes!

You should now have an idea of how to combine unit tests and integrations tests in an efficient way to test different aspects of your contracts. You should also be aware of some of the gotchas with using types like bytes32 and uint in a JavaScript environment, and how to convert them into something more suitable for your application frontend.

In the next chapter, we will start creating our controller contracts in order to add more advanced smart contract logic to our DApp.

Comments

Saleh A-Aziz Habtor

in tweet.js


instead of this line of code:


const [tweetId, text, userId] = tweet




write this if get tweet is not iterable error:


const tweetId = tweet[0]

const text = tweet[1]

const userId = tweet[2]