pen, fountain pen, writing

Ethereum Smart Contracts for Beginners – part 4

Truffle Console and Truffle Tests

  1. How Do Ethereum Smart Contracts Work?
  2. Writing Smart Contracts in Remix
  3. Setting Up Truffle to Deploy Smart Contracts
  4. Truffle Console and Truffle Tests
  5. Ethereum Apps with Web3.js

Welcome to another post exploring how to begin writing smart contracts. In this overview, we’ve previously visited Remix and seen how to configure Truffle to enable the compilation and deployment of smart contracts. In this post, I will introduce the Truffle Console, to enable for some more free-form exploration of contracts and their results, and talk a little about Truffle tests.

Not Just Truffle Console

As a developer, I have always had a special affection for those environments that mixed code interpreters with immediate evaluation of expressions. An old example were the BASIC development environments of the 1980s and 1990s. These were followed by Microsoft’s and Borland’s IDEs for Delphi, C++, and Visual Basic, which had Immediate windows and the ability to quickly evaluate an arbitrary expression. Today, my go-to environments for this mixture of automation and flexibility are Python and JavaScript consoles, but none of these are particularly helpful when coding in Ethereum.

Truffle provides something to fill precisely this niche, not only once but twice. You can invoke a console with the command truffle console. This is an interactive REPL (Read-Evaluate-Print-Loop) powered by JavaScript and with access to the Web3.js library (documentation here), which provides an API for JavaScript clients to interact with the Ethereum blockchain.

Truffle also gives you access  to another console to play with, accessible with truffle develop. There are a few differences between them, but they are very similar. For example, they:

  • allow you to execute most of the commands of the Truffle command line tool, including compiling, deploying and debugging contracts. 
  • give you a basic JavaScript console, with access to web3.js.
  • give access to all your contracts in a convenient way, by giving you objects named exactly like the contract.

The visible difference between these two consoles is that truffle develop offers you its own simulation of the blockchain, allowing you to start testing without installing your own client. It’s like Truffle Console + Ganache in one.

With truffle console, instead, you can connect to your local node, and if you start the console after migrating your contracts, these will already be deployed inside the console as well (unlike truffle develop). If you start both consoles in different shells, you can have the same artifacts (base contracts) but a different network state, as by default you will be interacting with different networks. But given they are functionally the same, I ignore this difference in the rest of this post.

Old-style computer code in black and green

Accessing Contracts in a Console

When you start the console, you should already have your system setup and some contracts written. If you have not deployed them when you start the console, you can do so with the command

migrate

For my example, I am using the TestContract.sol file I showed in my previous post.

To check what is deployed already in your system, enter the command

networks

Network: development (id: 1570633992615)
  Migrations: 0x56c7865De6Aa9aaF4f2C013b7f45b1134AC3770c
  TestContract: 0x35Fe3696F6C35a24b4351B1B430A011a22597820 

Once a contract is deployed, truffle makes it available to you in this way.

let contract = await TestContract.deployed()

This represents your interface to the contract. Let’s have a look at what you can do with it.

truffle(development)> Object.keys(contract)

[ 'constructor',
  'methods',
  'abi',
  'address',
  'transactionHash',
  'contract',
  'testCallNoArgs',
  'testCallWithArgs',
  'testMethod',
  'testUpdate',
  'sendTransaction',
  'send',
  'allEvents',
  'getPastEvents' ]

You can now check the contract’s address by writing

contract.address

'0x35Fe3696F6C35a24b4351B1B430A011a22597820'

which should give you the same value you obtained with networks.

Another important property is the abi, which describes the methods you can call in the contract with all their properties, including the list of inputs and outputs, whether the method can receive Ether, or whether it can change the state. The contract instance lets you also send Ether to this contract, using

await instance.send(web3.utils.toWei(1, "ether"))

Accessing Methods and Functions

But you’ll most likely be interested mainly in invoking the contract’s methods themselves. Truffle gives a really easy way to do that.

(await instance.testCallNoArgs()).toNumber()
10

(await instance.testCallWithArgs(25)).toNumber()
250

truffle(development)> result = await instance.testMethod(22)
{ tx:
   '0xf48ce7481cec7e4cd6f0203b0603c2a96ea5822a0ed20496128be273981d0ec8',
  receipt:
  [...]
}

truffle(development)> (await instance.testCallNoArgs()).toNumber()
22

The above examples show how we can call a function that does not change the state of the smart contract, and invoke a method that does. The first two calls are defined as viewin the contract, and so they are functions that simply return a value. They may read from the contract’s persistent storage but cannot change it.

On the other hand, the third call deliberately changes the state, and so is not a function that returns a value. Instead, it is called via a transaction sent to the contract and its result is a receipt of the transaction, with information about the latter. This includes the transaction hash and signature, the total gas cost, and a log of any events that the contract emitted.

The second call to testCallNoArgs() merely demonstrates that testMethod did change the storage.

Default Invocation Type

The final function in the contract, testUpdate, is ambiguous. It is declared as returning a value but it changes state as well. This means that by default Truffle calls it by sending a transaction, and never returns its value.

truffle(development)> await instance.testUpdate(37)
{ tx:
   '0xb51fc5b4c0e17777aa7ccc9207a25977151381410d97bd8f3821e49336f8b084',
  receipt:
  [...]
}

truffle(development)> (await instance.testCallNoArgs()).toNumber()
37

This is not the whole story, however. The methods in a contract instance are more than just functions, they have methods of their own as well. Among these are call, sendTransaction and estimateGas. The last one gives an estimate of how much a transaction executing that function will cost. The other two allow the method to be invoked either as function, passing the result thereof to the console (call), or as a transaction, paying gas and returning its receipt (sendTransaction). Here is an illustration:

truffle(development)> (await instance.testCallNoArgs()).toNumber()
45

truffle(development)> await instance.testUpdate.call(5)
true

truffle(development)> (await instance.testCallNoArgs()).toNumber()
45

truffle(development)> await instance.testUpdate.sendTransaction(5)
{ tx:
   '0x5cdf88497a7854172fdfb3894c8385ca34c2b3172d27fe7d50a42fb251a845cf',
  receipt:
  [...]
}

truffle(development)> (await instance.testCallNoArgs()).toNumber()
5

When I call the ambiguous function testUpdate it returns a value, but it does not change the contract storage. On the other hand, when I invoke it with sendTransaction, it does just the opposite.

The Importance of Being Constant

For a final take on this, let us add  a new function to the contract, just like testCallNoArgs, with the sole difference that it is not declared as a view:

function testCallNoArgsNotView() public returns (uint) {
    return a;
}

Now, we compile (compile) and migrate (migrate --reset) the contract again and compare:

(await instance.testCallNoArgs()).toNumber() 
10

await instance.testCallNoArgsNotView()
{ tx:
   '0x9c940e7205201b35a4c0876a5787223a7af72e8f1302c77ef0be7d409acd1c60',
  receipt:
  [...]
}

These functions are exactly the same. They both return a value and do not change the state of the contract. They both should be called as functions by default. However, Truffle has decided to invoke the second function by sending a transaction, and the only reason to do so was that the function lacked an appropriate modifier.

For an explanation, you can read the Solidity docs, but here are the basics. Solidity functions can be tagged by the modifiers pure and view, which in previous versions of Solidity were both called constant.

They are both promises that the function will not change the contract’s state. Since Solidity 0.5.0, such functions are invoked with the opcode STATICCALL instead of CALL, which prevents the EVM from affecting the state. This is the reason why Truffle automatically invokes them as with .call() instead of .sendTransaction. The difference between these two modifiers is that a pure function also promises not to read from storage.

Truffle Tests

All of the above was performed inside the console, which is not great if you want to do a series of actions repeatedly or create your own application. One way to script a series of actions is inside an automated test.

You can write tests both in Solidity and in JavaScript, but since my goal here is to explore how to invoke a contract from JavaScript, I’ll focus only on the latter.

A test is a great way to systematically explore a contract’s API. Truffle uses the Mocha framework for test structure, and Chai for assertions so it will be familiar to JavaScript developers. The tests can be executed from the command line with truffle test or from the console with simply test.

Because they’re still pretty much part of the Truffle environment, we have access to contract abstractions in the same way we have them in the console, and to do so we can use the same techniques shown above. So, at the beginning of the test file, we typically have something like this

const TestContract = artifacts.require("TestContract");

This is followed by blocks that organize tests in contexts. At the top level, there should be a single contract block, and inside this there can be many context blocks. Finally, inside contexts we can write individual test items, introduced by it. I won’t go into how to write tests in this post, so I just leave you with an example.

An Example Test File

const TestContract = artifacts.require("TestContract");
let instance;

function isTransaction(result) {
    if (! result.tx) return false;
    return true;
}

contract('TestContract', (accounts) => {
    beforeEach('setup', async () => {
        instance = await TestContract.deployed();   
    });
    
    context('A view function with no arguments', () => {
        const expected = 10;
        
        it('Can be called by default', async () => {
            const result = await instance.testCallNoArgs();
            assert.equal(result, expected);
        });
        
        it('Can be called explicitly with call', async () => {
            const result = await instance.testCallNoArgs.call();
            assert.equal(result, expected);
        });
        
        it('Can be called explicitly as Tx', async () => {
            const result = await instance.testCallNoArgs.sendTransaction();
            assert(result !== expected);
            assert(isTransaction(result));
        });
        
        it('Can be called via instance.methods', async () => {
            const methodCallResult = await instance.methods['testCallNoArgs()'].call();         
            assert.equal(methodCallResult, expected);

            const methodSendResult = await instance.methods['testCallNoArgs()'].sendTransaction();          
            assert(isTransaction(methodSendResult));
        });
    });

    context('A view function with arguments', () => {
        const expected = 250;
        
        it('Can be called by default', async () => {
            const result = await instance.testCallWithArgs(25);
            assert.equal(result, expected);
        });
        
        it('Can be called explicitly with call', async () => {
            const result = await instance.testCallWithArgs.call(25);
            assert.equal(result, expected);
        });
        
        it('Can be called explicitly as Tx', async () => {
            const result = await instance.testCallWithArgs.sendTransaction(25);
            assert(result !== expected);
            assert(isTransaction(result));
        });
        
        it('Can be called via instance.methods', async () => {
            const methodCallResult = await instance.methods['testCallWithArgs(uint256)'].call(25);          
            assert.equal(methodCallResult, expected);

            const methodSendResult = await instance.methods['testCallWithArgs(uint256)'].sendTransaction(25);           
            assert(isTransaction(methodSendResult));            
        });
        
        it('... but function signature must be fully specified', async () => {
            try {
                const methodCallResult = await instance.methods['testCallWithArgs()'].call(25);
                assert(false, "Should have reverted");
            } catch {
                assert(true);
            }
        });         
    });
    
    context('A method transaction', () => {
        
        it('Can be sent by default', async () => {
            const valueToWrite = 25;
            const receipt = await instance.testMethod(valueToWrite);
            assert(isTransaction(receipt));
            
            const valueRead = await instance.testCallNoArgs();
            assert.equal(valueRead, valueToWrite);
        });

        it('Can be sent explicitly', async () => {
            const valueToWrite = 32;
            const receipt = await instance.testMethod.sendTransaction(valueToWrite);
            assert(isTransaction(receipt));
            
            const valueRead = await instance.testCallNoArgs();
            assert.equal(valueRead, valueToWrite);
        });

        it('Can be called, but will not change state', async () => {
            const previousValue = await instance.testCallNoArgs();
            
            const valueToWrite = 28;
            const receipt = await instance.testMethod.call(valueToWrite);
            
            const valueRead = await instance.testCallNoArgs();

            assert.notEqual(valueRead.toNumber(), valueToWrite);
            assert.equal(valueRead.toNumber(), previousValue.toNumber());
        });
        
        it('Can be called via instance.methods', async () => {
            const valueToWrite = 41;
            const methodCallResult = await instance.methods['testMethod(uint256)'].sendTransaction(valueToWrite);          
            assert(isTransaction(methodCallResult));
            
            const valueRead = await instance.testCallNoArgs();
            assert.equal(valueRead, valueToWrite);
        });     
    });
});

And the output looks like this:

Example output of a Truffle test, showing several categorized tests and their execution times

This is a good place to stop this post. We’ve gone in a whirlwind view of Truffle’s support for testing smart contracts, both through the console and in automated tests.
In the next post of this series, I will talk about developing command line applications for the same goal, without relying on Truffle at all.

See you then, and enjoy your coding.

Leave a Reply