(Part 8) Ethereum Testing - Async Tests, Refactoring And Performance Vs. Cleaner Code (PT 8)
Repository
https://github.com/igormuba/EthereumTesting/tree/master/class8
What Will I Learn?
- 3 refactoring techniques for async tests
- Applying async tests to Truffle
- The performance hit of the cleaner async code
Requirements
- Internet connection.
- Code editor.
- Browser.
Difficulty
- Intermediary.
Introduction
On the previous tutorial you were introduced to asynchronous tests for Ethereum contracts using JavaScript. The essence of asynchronous functions in JavaScript is all about making your code "wait" for things to happen.
For example, if you are testing an Ethereal transfer, the balance won't be immediately updated right after you call the transfer function. For any Ethereum operation you need to wait for:
- A miner to accept the operation.
- The miner to process the operation.
- The block with the operation to be produced and published on the blockchain.
After all that you will be able to see the result. And because you have to wait for that, you need to prepare your code to "wait" for it. So far we have used the .then
statement in our code:
That is a fair way of making the code wait, and the tests run faster. But that is a bit more complex to code and can limit what you can do and when. For example, you can only return one statement at each test to pass it as a variable for the .then
self-invoking function.
By using async
and await
we can store and work with multiple variables at the same time. Previously we have studied the theory, now let's put that into practice. So let's convert our code to use those keywords. On the code above, as I have said on the previous tutorial, I have already made the it
test units async by adding the keyword on the functions.
First case: Deploy and test, without using the blockchain
To convert the test cases that do not have a then
statement we can simply turn the anonymous function into an async
function, and then tell the test to await
for the result of an operation.
I will take the following test to show how I have converted them to asynchronous functions:
it("Should deploy the exchange", async ()=>
Exchange.deployed().then(instance=>{
exchangeAddress=Exchange.address;
console.log("Exchange address is");
console.log(exchangeAddress);
assert.notEqual(exchangeAddress, null, "the address is null");
})
);
As it is, what it does is, wait to ensure that the exchange contract was deployed, and then execute the operations to save the address of the contract and the assertion. Because we only require the contract to be deployed, and we don't have to execute other operations with it after the deployment, this one is simple.
First, instead of passing a variable with the instance of the contract (that was actually not necessary because we didn't use it in the end), we just call an asynchronous function with:
Exchange.deployed().then(async () => // rest of code omitted
And then we look for what pieces of code need to use the blockchain. Because, as said before, the blockchain is inherently slow.
In this case, we don't make any calls to the blockchain, so we don't have to make any call with the await
keyword!
There is another test called "Should deploy the ERC20" that does the same thing, it just deployed the ERC20 contract and stores its address on a variable for later usage.
In the end, those two tests, converted to async
, will look like:
it("Should deploy the ERC20", async ()=>{
await Erc20.deployed();
erc20Address = Erc20.address;
console.log("ERC20 address is");
console.log(erc20Address);
assert.notEqual(erc20Address, null, "the address is null");
});
it("Should deploy the exchange", async ()=>{
await Exchange.deployed();
exchangeAddress = Exchange.address;
console.log("Exchange address is");
console.log(exchangeAddress);
assert.notEqual(exchangeAddress, null, "the address is null");
});
As you can see, no then statement, and we make the code literally wait for the contracts to be deployed!
Second case: Deploy and make one blockchain operation
In this case, we are checking that the contract was deployed and then, after the deployment, we make an operation with the deployed contract. After the operation, we then make the assertion to make sure we have got the expected results.
This is the test we will modify, and on this one, we will make a little bit more of work.
it("Should allow exchange to spend user balance", ()=>
Erc20.deployed().then(instance=>{
instance.approve(exchangeAddress, 100);
return instance.allowance(accounts[0], exchangeAddress);
}).then(exchangeAllowance=>{
assert.equal(exchangeAllowance.valueOf(), 100, "the exchange is not allowed to move 100 tokens");
})
);
First thing, we wait to ensure that the token was deployed:
let instance = await Erc20.deployed();
And then well to the blockchain to allow the exchange to spend tokens from our balance:
await instance.approve(exchangeAddress, 100);
The next code may not seem like a call to the blockchain, but it is. We will call the ERC20 contract to see how much is the exchange allowed to spend from the user's balance. Because this is a blockchain call, even a small delay will cause the JavaScript code to consider the result as "null", and even though we have allowed the exchange to spend from the user's balance, it will give us an error like this:
To avoid this error, we tell the test to wait for a result on the operation:
let exchangeAllowance = await instance.allowance(accounts[0], exchangeAddress);
And this fixes this error:
So be mindful when you are using the blockchain, as either passive or active calls (that just reads values or actually makes changes) can cause errors because of the small delays!
With the assertion, the final code looks like:
it("Should allow exchange to spend user balance", async ()=>{
let instance = await Erc20.deployed();
await instance.approve(exchangeAddress, 100);
let exchangeAllowance = await instance.allowance(accounts[0], exchangeAddress);
assert.equal(exchangeAllowance.valueOf(), 100, "the exchange is not allowed to move 100 tokens");
});
Third case: Multiple Operations in one test
Here is a huge advantage of asynchronous functions. We have the following two tests, that the sole purpose is to fund the account of the test user with ETH and tokens. We could merge those two operations into a single test. A test we could call something like "The account was funded". For this, we will build a test "from scratch" with the functionality of the tests.
The tests are:
it("User balance should have 100 tokens,", ()=>
Exchange.deployed().then(instance=>{
instance.depositToken(erc20Address, 100);
return instance.getTokenBalance(accounts[0], erc20Address);
}).then(tokenBalance=>{
console.log("token balance:");
console.log(tokenBalance);
assert.equal(tokenBalance, 100, "the user does not have 100 tokens in balance");
})
);
it("User balance should have 100 wei", ()=>
Exchange.deployed().then(instance=>{
instance.sendTransaction({from: accounts[0], value: 100});
return instance.getEthBalance(accounts[0]);
}).then(ethBalance=>{
assert.equal(ethBalance, 100, "the user does not have 100 wei in balance");
})
);
And they show on the results as:
They will merge into the test:
it("Should fund the user balance", async () =>{
});
And inside the test we will grab an instance of the contracts so that we can call their functions:
let token = await Erc20.deployed();
let exchange = await Exchange.deployed();
Next, make the token and Ethereum deposits:
await exchange.depositToken(erc20Address, 100);
await exchange.sendTransaction({from: accounts[0], value: 100});
Again, in the code above, the test execution will stop until the functions are finished.
We then make the (blockchain) calls to retrieve the balances from the contract and store their variables:
let tokenBalance = await exchange.getTokenBalance(accounts[0], erc20Address);
let ethBalance = await exchange.getEthBalance(accounts[0]);
Again, they have to use await
, else, the variables will be null in the assertions. The assertions are:
assert.equal(tokenBalance, 100, "the user does not have 100 tokens in balance");
assert.equal(ethBalance, 100, "the user does not have 100 wei in balance");
And in the end, those tests were replaced by this one:
it("Should fund the user balance", async () =>{
let token = await Erc20.deployed();
let exchange = await Exchange.deployed();
await exchange.depositToken(erc20Address, 100);
await exchange.sendTransaction({from: accounts[0], value: 100});
let tokenBalance = await exchange.getTokenBalance(accounts[0], erc20Address);
let ethBalance = await exchange.getEthBalance(accounts[0]);
console.log("token balance:");
console.log(tokenBalance);
assert.equal(tokenBalance, 100, "the user does not have 100 tokens in balance");
assert.equal(ethBalance, 100, "the user does not have 100 wei in balance");
});
Though the code is smaller and simpler, the execution is a bit slower for this single test than it was for the two tests!
You can use a similar logic for the other tests on the exchange contract. The last two tests become:
it("Should place sell orders", async ()=>{
let instance = await Exchange.deployed();
await instance.sellToken(erc20Address, 20, 10);
await instance.sellToken(erc20Address, 21, 10);
await instance.sellToken(erc20Address, 22, 10);
let sellOrders = await instance.getSellOrders(erc20Address);
let firstOrderPrice = sellOrders[0][0];
let firstOrderVolume = sellOrders[1][0];
let secondOrderPrice = sellOrders[0][1];
let secondOrderVolume = sellOrders[1][1];
let thirdOrderPrice = sellOrders[0][2];
let thirdOrderVolume = sellOrders[1][2];
assert.equal(firstOrderPrice, 20, "the first order is not priced at 20");
assert.equal(firstOrderVolume, 10, "the first order does not have 10 tokens");
assert.equal(secondOrderPrice, 21, "the second order is not priced at 21");
assert.equal(secondOrderVolume, 10, "the second order does not have 10 tokens");
assert.equal(thirdOrderPrice, 22, "the third order is not priced at 22");
assert.equal(thirdOrderVolume, 10, "the third order does not have 10 tokens");
});
it("Should buy 15 tokens", async ()=>{
let instance = await Exchange.deployed();
await instance.sendTransaction({from: accounts[0], value: 500});
await instance.buyToken(erc20Address, 21, 15);
let sellOrders = await instance.getSellOrders(erc20Address);
let firstOrderPrice = sellOrders[0][0];
let firstOrderVolume = sellOrders[1][0];
let secondOrderPrice = sellOrders[0][1];
let secondOrderVolume = sellOrders[1][1];
assert.equal(firstOrderPrice, 21, "the first order is not priced at 21");
assert.equal(firstOrderVolume, 5, "the first order does not have 5 tokens");
assert.equal(secondOrderPrice, 22, "the second order is not priced at 22");
assert.equal(secondOrderVolume, 10, "the second order does not have 10 tokens");
});
Performance hit
On the last two tests from the previous section, you can clearly see the performance hit you get by using these functions. Check the time it took to execute the "Should place sell orders" and the "Should buy 15 tokens" tests using the previous method:
And see how much slower it was after refactoring them to use asynchronous functions:
So, pretty much like anything, the price for the simpler and easier to understand code is performance.
Curriculum
Last 2 tutorials:
The first class of the series:
Beneficiaries
This post has as beneficiaries
- @utopian.pay with 5%
- @steempeak with 5%
using the SteemPeak beneficiary tool:
Thank you for your contribution @igormuba.
After reviewing your tutorial we suggest the following points listed below:
Your tutorials are excellent, and the structure of your contribution is very good.
We suggest you put comments in your code. The code that you explain isn't very easy and so the comments would help the less experienced readers.
Excellent screenshots with the outputs presented.
Thank you for your work in developing this tutorial.
Looking forward to your upcoming tutorials.
Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.
To view those questions and the relevant answers related to your post, click here.
Need help? Chat with us on Discord.
[utopian-moderator]
Thank you for your review, @portugalcoin! Keep up the good work!
Hi @igormuba!
Your post was upvoted by @steem-ua, new Steem dApp, using UserAuthority for algorithmic post curation!
Your post is eligible for our upvote, thanks to our collaboration with @utopian-io!
Feel free to join our @steem-ua Discord server
Hi, @igormuba!
You just got a 0.31% upvote from SteemPlus!
To get higher upvotes, earn more SteemPlus Points (SPP). On your Steemit wallet, check your SPP balance and click on "How to earn SPP?" to find out all the ways to earn.
If you're not using SteemPlus yet, please check our last posts in here to see the many ways in which SteemPlus can improve your Steem experience on Steemit and Busy.
Hey, @igormuba!
Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!
Get higher incentives and support Utopian.io!
Simply set @utopian.pay as a 5% (or higher) payout beneficiary on your contribution post (via SteemPlus or Steeditor).
Want to chat? Join us on Discord https://discord.gg/h52nFrV.
Vote for Utopian Witness!