Adding a JS-Test to Our Game

The course is part of this learning path

Adding a JS-Test to Our Game
1h 2m

In this lecture, we run through testing in Solidity and carry out a variety of tests on the game that we have been creating in previous courses in this learning path.


Now in this lecture, I want to go ahead and create a very very simple test to just test if our game board at the beginning of the game is really empty. And this basically should demonstrate how easy it is to create a test in JavaScript for our Solidity files. Now I'm here in the test folder on the left side, and I just hit a new file, and I'm calling this '1_simple_test.js.' And the one, this index, beforehand, is a numerical index, to help Truffle determine in which order the tests should be executed. Before we start, let's have a look at the Truffle's suite documentation, how these tests are built up. And they are always built up in the same way.

First, outside of the test, you are importing the artifacts that you need, and these artifacts are chasing representation of your smart contracts. They contain the ABI array; they contain the addresses; they contain everything that Truffle needs in order to interact with your smart contracts.

Then it starts with the 'contracts' keyword. And if you come from JavaScript testing, and if you've worked with Mocha before, then you usually use the 'describe' keyword, and inside Truffle, use the 'contract' keyword. So, this is how you start the actual test. And then inside the test, you have the test cases which start with 'it.' 'It should put 10,000 Meta Coin in the first account,'; 'it should call a function that depends on the linked library,'; it should do something; it should do something. And then you test these test cases inside your test. And to simply start, I would say we write our first simple test to test if our game board is empty at the beginning. So, we need two things in order to test this. The one is the GameManager, which can start a game; and the other one is the 'ThreeInARow,' where we get from the GameManager that starts the game, the address where the 'ThreeInARow' game was deployed, and then use this 'ThreeInARow' artifact to interact with our smart contract.

Let's get started. 'const GameManager = artifact .require(''GameManager''); const ThreeInARow = artifact.require(ThreeInARow.)' And then we have the 'contract' keyword, where we describe the actual test cases. We call this 'ThreeInARow' Test, and here we get the accounts. And then have a function. If you've never encountered this way of writing function arguments, then you maybe familiar with this one. That is a callback function, and the short version is this one. It's the same thing, just in a newer version. And then we have one test inside it. Not if— I usually make this mistake very often. 'it should have an empty game board at the beginning.' And then we have another callback function here without any arguments, and that is the function that gets executed when the test starts. All right, now for simplicity reasons, I will just at this point 'cosole.log(accounts).' Why can't I do this? Because we are in JavaScript, and in JavaScript, you can always 'cosole.log.' And I will just open the terminal. Start Ganache.

If you haven't started Ganache without any block mining time, please; or else, you will wait forever to run the test. And then I open a second terminal. I go to the right folder, and I start Truffle test, and you see we are compiling the smart contracts. And... Defects... My bad. And it should output us the 10 accounts that we have here, given to us by the contract function. From here, we are going to work with the GameManager, and I said we want to use this async function. So, we want to use the 'await' keyword. So, we have to make, first of all, this function an async function, and tell our JavaScript interpreter to know that this is going to use the 'await' keyword. And the first thing is, we need the instance, we need the address of the GameManager, and the instance of the GameManager at the address where it was deployed.

And we can do this with 'gameManagerInstance' is, we can say, 'GameManager.deployed();' and this will give us the instance of the GameManager at the address where it was deployed. And if you add the 'await' keyword, then we don't have to add the then, and so on. We can skip this by using the 'await' keyword. So, how does Truffle know where it was deployed? Because when it's using the Truffle test functionality, it is going to run the same migrations, and the migrations will give it back an address, where it was deployed. Or, on a real environment, if you use the deployed keyword, then it knows this by... We don't have any networks yet here. But if you remember back when we deployed a smart contract over here, then it knows inside this JSON file in the build contracts folder, the artifacts that JSON files have, the addresses saved where it is deployed on a specific network. So, that is very important to know where this magic comes from. It's not going to redeploy anything.

Then we have the 'gameManagerInstance,' and when we have the 'gameManagerInstance,' then we have to start a new game. And starting a new game is not as hard as you might imagine. It is quite easy. And when we start a new game, then we get back transaction receipt. So, the 'txReceipt' is 'await gameManagerInstance.startNewGame();' but we're not quite done here yet because starting a new game costs us 0.1 Ether. If you remember in GameManagers in the Solidity file. startNewGame() will start a new game in 'ThreeInARow' and send the value off to the 'ThreeInARow' smart contract. So, we have to somehow find a way to give this startNewGame 0.1 Ether during the transaction. And in every Truffle version, where you interact with smart contracts, If first you give it the arguments of the function— in our case, dysfunction doesn't have any arguments— but if it had arguments: arg, arg1, arg2, then we first give them the arguments, and at the end there is an optional last parameter which is a configuration object.

And here can say 'value,' and that's going to be 0.1 Ether. So, I could write now one and 17 zeros, or I just access the web3 object. And the web3 object is only present in the tests. There is no injection; no visible injection, but web3 is going to be injected into our tests, and at any point you can access the web3 object. That means you can do 'web3.utils.Wei,' and then say '0.1, ether.' All right, now let's just 'console.log(txRecepit).' I wanted to see what is inside here. Run the test again. Let's see what happens. All right. We see we have here in the log output, not only the transaction and the receipt, we also have the logs as a separate object inside our transaction receipt. And inside the logs, we have the 'EventGameCreated' log, and inside the arguments of this log of this event, we have our event arguments. And if we see the event arguments over here, 'EventGameCreated,' then we have two arguments that is an '_player,' and an '_gameAddress,' and that one we can use this game address.

We can use to say our 'ThreeInARow' game, we don't have to get the deployed instance because we don't know where it is deployed, but we can get the address, and we can say it is deployed at a specific address using this transaction receipt. Let me show you how this works. Let 'ThreeInARowInstance = await,' and here it gets interesting, 'txReceipt.logs,' the first one which is the only one that we have here, 'args._gameAddress.' So, this will take our transaction receipt, the logs, the arguments, the game address that was emitted during 'startaNewGame,' and tell our 'ThreeInARow' artifact that the instance is available at this specific address, and then give us back the instance. And now we can say... We can interact with our 'ThreeInARow' smart contract as if it was just another JavaScript function. That means we can call all the functions that we have defined in our 'ThreeInARow' smart contract as if they were just available from our JavaScript. That also means we can get the board. Where is it, 'getBoard' and see what is inside. Now let's do this. 'let board = await threeInARowInstance.getBoard().' And then I would say, we just 'console.log()' this and see what is inside. We save this, and then we output our game. And here is our game board. We have an array of arrays. We have an array of three arrays, which is our first row, our second row, and our third row.

And now we just have to make sure that programmatically that they are all zero. Now let's start with the first one. And for this we can use 'assert.' And this is the child library for assertions, and it works like this: assert, where we assert that something is equal to an expected value, is equal to a value that is given here. For example, 'board[0][0]' must be the first, the top left corner of the board must be a zero address, zero. And we could say the first row, first column must be zero. And if we execute this now, then it should go well through. And now let me show you what happens if it's not asserting. First the right one. So perfect. I should have an empty game board at the beginning. And now let's change this, let's say, we expect there is a one inside this game board, which isn't, but I just want to show you how the assertion, what happens when the assertion fails. So, it's a very clear assertion error. There is one failing test: 'the first row, first column must be zero, expected 1, but it equals to zero.' Okay. Now we have tested the top left corner, but that's not the whole board. Here's a little coding challenge. Test the rest of the game board, make sure that everything is zero. Let me save this. If you want to do this challenge, then pause the video now, I will show you in a second how this is done, in my way. There's probably tons of other ways. But the way I do it is one possible solution.

All right. It's basically two for-loops. 'for(let i = 0;' And then they should go from 0, 1, 2. And then, 'for let(j=0;' And then we can say 'assert.equal' that zero the 'board[i][j].' Let's say that 'i row' and the 'j column' must be zero. Obviously this should go perfectly through. And our test is done. All right. In the next lecture, I want to write a little bit more complicated test cases. And if you have any questions, or if you get stuck somewhere, then check out the course Q&A section, where we are always here to help. And I'll see you in the next lecture.

About the Author

Tom is a CTO, senior back-end developer, and systems architect with over twenty years of hands-on development experience in a variety of languages and systems. He has a CS master's degree and has been working with Ethereum and blockchain technologies since 2016.

Covered Topics