Tips for Writing Solidity Tests with Truffle

At first glance the Truffle Framework seems to be all setup for writing, deploying and testing contracts, and it’s very well done. There are however some instances where you may want to augment Truffle with a few tips in order to streamline your test writing and running.

I’ll talk about 3 specific tips:

  1. Using `async` and `await` vs. promises
  2. Creating a shared context for multiple tests
  3. Embedding tests in other tests

Using these tips should help reduce a significant amount of code duplication in your tests and allow you to write more readable tests.

Using `async` and `await` vs. promises

Truffle comes with some tests out of the box and each test involves a lot of repeated boilerplate (which we’ll talk about in 2) and uses promises.

it("should call a function that depends on a linked library", function() {
    var meta;
    var metaCoinBalance;
    var metaCoinEthBalance;
return MetaCoin.deployed().then(function(instance) {
      meta = instance;
      return meta.getBalance.call(accounts[0]);
    }).then(function(outCoinBalance) {
      metaCoinBalance = outCoinBalance.toNumber();
      return meta.getBalanceInEth.call(accounts[0]);
    }).then(function(outCoinBalanceEth) {
      metaCoinEthBalance = outCoinBalanceEth.toNumber();
    }).then(function() {
      assert.equal(metaCoinEthBalance, 2 * metaCoinBalance,
"Library function returned unexpected function, linkage may be broken");
    });
  });

A bulk of the test is basic Promise “cruft”. Let’s see what that test looks like rewritten with async and await.

it("should call a function that depends on a linked library", async function() {
    var meta = await MetaCoin.deployed();
    var metaCoinBalance = 
(await meta.getBalance.call(accounts[0])).toNumber();
    var metaCoinEthBalance = 
(await meta.getBalanceInEth.call(accounts[0])).toNumber();
assert.equal(metaCoinEthBalance, 2 * metaCoinBalance,
"Library function returned unexpected function, linkage may be broken");
  });

Wow, what a difference!

You can see that the savings comes from turning the test function itself into an async function and then having it await for each of the values it needs. Therefore, we don’t need to write functions to handle each promise returned by the Truffle Contract instance meta of our contract MetaCoin.

The other savings comes from being able to wrap the await response in brackets and convert directly from the object of type BigNumber into a JavaScript number using:

(await meta.getBalance.call(accounts[0])).toNumber();

Every uint256 returned from Solidity to JavaScript is an object of type BigNumber and can be converted to a number or string.

Creating a shared context for multiple tests

Another useful trick is creating a shared context of variables that you would like to use in multiple tests, across multiple contracts.

The way Truffle is initially setup doesn’t make this immediately obvious, and it took some Mocha digging to find the right approach.

Here’s how Truffle sets up a test out of the box:

contract('MetaCoin', function(accounts) { ... tests ... }

Now if you wanted to require this test in another test, it would be difficult. And how would you pass the same basket of variables (context) created inside this test to other tests, i.e. the “shared context” you’re trying to achieve.

Here’s a slight modification on running a test that has an export, returns a context JSON object, all placed in a new file called shared.js:

const run = exports.run = async(accounts) => {
  const meta = await MetaCoin.deployed();
  it("should have MetaCoin deployed", () => {
    assert(meta !== undefined, "MetaCoin is deployed");
  });
  return { meta }
};
contract('Shared', run);

Now we have a nice test function that is async and returns a context object with our contract instance. It will await the deployed instance of our Truffle Contract and we can return the instance to be used inside our other contracts like so:

const shared = require('./shared.js');
contract('MetaCoinSale', (accounts) => {
  let meta; //explained in a moment
  it("should have the shared context", async() => {
    context = await shared.run(accounts);
    meta = context.meta;
    //or...
    ({ meta, ... } = context);
    //make sure not undefined
    assert(meta !== undefined, 'has MetaCoin instance');
  });
... tests can use MetaCoin instance now ...
});

And there we have it. The first test in your actual contract test script will wait for the shared context to be returned, because we made the entire test runner function async.

//shared.js
const run = exports.run = async(accounts) => { ... }

You can also declare several other variables in shared.js and return them using the context object. In your contract tests, simply use assignment (or destructuring to keep the lines of code minimal) for variables. Also to keep things convenient, I keep context variables scoped to the main test runnerfunction so that all tests can access them. You’re off to the races in minimizing your test code.

Why declare variables like meta for the MetaCoin contract instance outsideall your tests like this? It greatly simplifies each test you write, otherwise you would have to call:

const meta = await MetaCoin.deployed();

From inside each test case. Some might not like this, but the approach has been working out fine for me so far.

Embedding tests in other tests

This is essentially the same modification of decoupling the test runner function from the truffle contract call as in shared.js. For MetaCoin it looks something like this:

const run = exports.run = (accounts) => {
  ... MetaCoin tests ...
};
contract('MetaCoin', run);

You would then use this in another contract like so:

const metaTests = require('./metacoin.js');
contract('MetaCoinSale', (accounts) => {
  ...
  metaTests.run(accounts);
  ...
});

Note that an actual contract test runner is NOT async:

const run = exports.run = (accounts) => {

It will derail your whole shabang if it was… I’m getting a bit tired writing this post but give it a shot making it async and watch it break :)

All your test can still be async like this though:

const run = exports.run = (accounts) => {
  it("should do some async magic", async() => { ... }
};
contract('MetaCoin', run);

And that’s it! Those are my tips. Go forth, write small tiny tests and make some great, secure smart contracts.

猜你喜欢

转载自blog.csdn.net/tianlongtc/article/details/80602425
今日推荐