用 Solidity 写测试用例

Solidity test contracts live alongside Javascript tests as .sol files. When truffle test is run, they will be included as a separate test suite per test contract. These contracts maintain all the benefits of the Javascript tests: namely a clean-room environment per test suite, direct access to your deployed contracts and the ability to import any contract dependency. In addition to these features, Truffle’s Solidity testing framework was built with the following issues in mind:

  • Solidity tests shouldn’t extend from any contract (like a Test contract). This makes your tests as minimal as possible and gives you complete control over the contracts you write.

  • Solidity tests shouldn’t be beholden to any assertion library. Truffle provides a default assertion library for you, but you can change this library at any time to fit your needs.

  • You should be able to run your Solidity tests against any Ethereum client.

Example

Let’s take a look at an example Solidity test before diving too deeply. Here’s the example Solidity test provided for you by truffle unbox metacoin:

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/MetaCoin.sol";

contract TestMetacoin {
  function testInitialBalanceUsingDeployedContract() {
    MetaCoin meta = MetaCoin(DeployedAddresses.MetaCoin());

    uint expected = 10000;

    Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
  }

  function testInitialBalanceWithNewMetaCoin() {
    MetaCoin meta = new MetaCoin();

    uint expected = 10000;

    Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
  }
}

This produces the following output:

$ truffle test
Compiling ConvertLib.sol...
Compiling MetaCoin.sol...
Compiling truffle/Assert.sol
Compiling truffle/DeployedAddresses.sol
Compiling ../test/TestMetacoin.sol...

  TestMetacoin
    ✓ testInitialBalanceUsingDeployedContract (61ms)
    ✓ testInitialBalanceWithNewMetaCoin (69ms)

  2 passing (3s)

Test structure

To better understand whats happening, let’s discuss things in more detail.

Assertions

Your assertion functions like Assert.equal() are provided to you by the truffle/Assert.sol library. This is the default assertion library, however you can include your own assertion library so long as the library loosely integrates with Truffle’s test runner by triggering the correct assertion events. You can find all available assertion functions in Assert.sol.

Deployed addresses

The addresses of your deployed contracts (i.e., contracts that were deployed as part of your migrations) are available through the truffle/DeployedAddresses.sol library. This is provided by Truffle and is recompiled and relinked before each suite is run to provide your tests with Truffle’s a clean room environment. This library provides functions for all of your deployed contracts, in the form of:

DeployedAddresses.<contract name>();

This will return an address that you can then use to access that contract. See the example test above for usage.

In order to use the deployed contract, you’ll have to import the contract code into your test suite. Notice import "../contracts/MetaCoin.sol"; in the example. This import is relative to the test contract, which exists in the ./test directory, and it goes outside of the test directory in order to find the MetaCoin contract. It then uses that contract to cast the address to the MetaCoin type.

Test contract names

All test contracts must start with Test, using an uppercase T. This distinguishes this contract apart from test helpers and project contracts (i.e., the contracts under test), letting the test runner know which contracts represent test suites.

Test function names

Like test contract names, all test functions must start with test, lowercase. Each test function is executed as a single transaction, in order of appearance in the test file (like your Javascript tests). Assertion functions provided by truffle/Assert.sol trigger events that the test runner evaluates to determine the result of the test. Assertion functions return a boolean representing the outcome of the assertion which you can use to return from the test early to prevent execution errors (as in, errors that Ganache or Truffle Develop will expose).

before / after hooks

You are provided many test hooks, shown in the example below. These hooks are beforeAll, beforeEach, afterAll and afterEach, which are the same hooks provided by Mocha in your Javascript tests. You can use these hooks to perform setup and teardown actions before and after each test, or before and after each suite is run. Like test functions, each hook is executed as a single transaction. Note that some complex tests will need to perform a significant amount of setup that might overflow the gas limit of a single transaction; you can get around this limitation by creating many hooks with different suffixes, like in the example below:

import "truffle/Assert.sol";

contract TestHooks {
  uint someValue;

  function beforeEach() {
    someValue = 5;
  }

  function beforeEachAgain() {
    someValue += 1;
  }

  function testSomeValueIsSix() {
    uint expected = 6;

    Assert.equal(someValue, expected, "someValue should have been 6");
  }
}

This test contract also shows that your test functions and hook functions all share the same contract state. You can setup contract data before the test, use that data during the test, and reset it afterward in preparation for the next one. Note that just like your Javascript tests, your next test function will continue from the state of the previous test function that ran.

Advanced features

Solidity tests come with a few advanced features to let you test specific use cases within Solidity.

Testing for exceptions

You can easily test if your contract should or shouldn’t raise an exception (i.e., for require()/assert()/revert() statements; throw on previous versions of Solidity).

This topic was first written about by guest writer Simon de la Rouviere in his tutorial Testing for Throws in Truffle Solidity Tests. N.B. that the tutorial makes heavy use of exceptions via the deprecated keyword throw, replaced by revert(), require(), and assert() starting in Solidity v0.4.13.

Also, since Solidity v0.4.17, a function type member was added to enable you to access a function selector (e.g.: this.f.selector), and so, testing for throws with external calls has been made much easier:

pragma solidity ^0.5.0;

import "truffle/Assert.sol";

contract TestBytesLib2 {
    function testThrowFunctions() public {
        bool r;

        // We're basically calling our contract externally with a raw call, forwarding all available gas, with 
        // msg.data equal to the throwing function selector that we want to be sure throws and using only the boolean
        // value associated with the message call's success
        (r, ) = address(this).call(abi.encodePacked(this.IThrow1.selector));
        Assert.isFalse(r, "If this is true, something is broken!");

        (r, ) = address(this).call(abi.encodePacked(this.IThrow2.selector));
        Assert.isFalse(r, "What?! 1 is equal to 10?");
    }

    function IThrow1() public pure {
        revert("I will throw");
    }

    function IThrow2() public pure {
        require(1 == 10, "I will throw, too!");
    }
}

Testing ether transactions

You can also test how your contracts react to receiving Ether, and script that interaction within Solidity. To do so, your Solidity test should have a public function that returns a uint, called initialBalance. This can be written directly as a function or a public variable, as shown below. When your test contract is deployed to the network, Truffle will send that amount of Ether from your test account to your test contract. Your test contract can then use that Ether to script Ether interactions within your contract under test. Note that initialBalance is optional and not required.

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/MyContract.sol";

contract TestContract {
  // Truffle will send the TestContract one Ether after deploying the contract.
  uint public initialBalance = 1 ether;

  function testInitialBalanceUsingDeployedContract() {
    MyContract myContract = MyContract(DeployedAddresses.MyContract());

    // perform an action which sends value to myContract, then assert.
    myContract.send(...);
  }

  function () {
    // This will NOT be executed when Ether is sent. \o/
  }
}

Note that Truffle sends Ether to your test contract in a way that does not execute a fallback function, so you can still use the fallback function within your Solidity tests for advanced test cases.