Published on

Truffle How to Test a None View Function That Returns

Authors

As a developer I have become quite spoiled with being able to easily test that a function returns a value. These seems like it should be easy with Solidity and Truffle but it ended up being quite tricky. Say I have the following code:

//...
Person [] internal persons;

event PersonAdded(uint256 id, string message);

function addPerson(string name) external returns (uint256) {
    // create some object on storage
    //...

    emit PersonAdded(newPersonId, "Hello");

    return newPersonId;
}

In our truffle test this was our first stab at testing this:

it('should create a different person id for each new person added', async () => {
  const person1Id = await personContract.addPerson.call('James')
  const person2Id = await personContract.addPerson.call('Sally')

  console.log(`person1Id: [${person1Id.toNumber()}], person2Id: [${person2Id.toNumber()}]`)
  expect(person1Id).to.not.equal(person2Id)
})

This fails on the expect line and console.logs the following:

person1Id: [0], person2Id: [0]

After plenty of pain and trial and error it turns out that the truffle api is not the same as web3.

In web3 you use either:

  • call: if you are calling a Solidity view function (i.e. you are only reading from the chain)
const outputOfSomeMethod = someContract.someMethodThatReads.call(param1, param2);
  • send: if you are calling a Solidity that updates/deletes/adds state to the chain
const outputOfSomeMethod = someContract.someMethodThatChangesState.send(param1, param2);

Confusingly the truffle api for a contract does not match this completely:

  • call: similar to web3 you use this on a contract method if you intend to only read values
    • But you can hit a method that normally writes to the chain but truffle will instead return the last stored value
  • Just the method name: you simply call the method name without call/send if you want to write.
    • To see the result of this call you can:
      • Hacky approach: Do a call on the method with the same parameters
      • Correct approach: Look at the response from the method call which is a transaction receipt and look in that for the required event that gets fired.

Hacky Approach

The below approach is not the recommended way of doing this and is very hacky. It is also not at all how you would do this when running this in web3:

it('should create a different person id for each new person added', async () => {
  await personContract.addPerson('James')
  const person1Id = await personContract.addPerson.call('James')

  await personContract.addPerson('Sally')
  const person2Id = await personContract.addPerson.call('Sally')

  console.log(`person1Id: [${person1Id.toNumber()}], person2Id: [${person2Id.toNumber()}]`)
  expect(person1Id).to.not.equal(person2Id)
})

In the above the parameters used in the call have to match the parameters from the first method invocation.

Listening to Events

This is the correct way to handle this. Using the openzeppelin-test-helpers we can instead do the following to test our function works correctly:

//imports
const { BN, constants, expectEvent, shouldFail } = require('openzeppelin-test-helpers')

//...
//for the sake of illustration pretend that the id of Sally is 1
it('should create a different person id for each new person added', async () => {
  await personContract.addPerson.call('James')
  const { logs } = await contract.addPerson('Sally')

  expectEvent.inLogs(logs, 'PersonAdded', {
    id: new BN(1),
    message: 'Hello!',
  })
})