June 26, 2024

Blockchain App Integration Testing

Today I want to show you how one can use docker compose in order to bootstrap a local miniwasm blockchain where one can deploy and test smart contracts in isolation.

You can find the code below as a working example in this repository.

Requirements

Before we look at the smart contract test, we have to speak about the requirements. In a typical application of our customers, a requirement might be to update UI state when the relevant smart contracts have been interacted with. To this end one can leverage websocket subscriptions to events concerning the smart contracts.

For today’s post I would like to simplify these requirements a bit: Imagine an application which shows the current balance of an address to the user. Whenever the balance on the blockchain changes, the UI should update.

Implementation

The implementation of these requirements is pretty straightforward. We need to store the value of the balance as state in the front end, potentially using something like zustand, redux or vanilla javascript (I went for the latter in my code, but you can make it as fancy as you like). Then we need to initialize the state with the value from the chain, which requires some IO, typically an async operation in JavaScript. Additionally we have to set up a subscription to change events for this value in order to get real time updates to it. You can read up the implementation details in the repository, they are not very important for this post.

Testing

Since we cannot “code in the dark” and just hope that we got the implementation right, we have to test. If we were to test this functionality by hand, we would have to:

  1. Make some kind of html based user interface to show the balance in the browser,
  2. have some kind of wallet at the ready to be able to send funds to arbitrary addresses,
  3. start the app,
  4. transfer the funds via the wallet,
  5. see that the balance updates.

Probably, we would not get the implementation straight on the first attempt. For example, I was struggling a bit to get the right query filter for my subscription:

wsClient.subscribe(
  'Tx',
  {
    'transfer.recipient': address,
  },
  async () => (balance = await getBalance(lcdClient, address)),
)

It took me some attempts to find the correct value. Testing each one of these by hand would have been very expensive in sense of development time spent.

Please also note that this is a very simple and contrived example. Usually, requirements are much more complex.

Let us consider the blockchain on which to run tests on. We could use the public Initia testnet for this, however we are then limited to 30 INIT per day per person, so potentially we can quickly run out of funds to test. Of course we could “recycle” the money by sending it back from the receiving address but this also takes time and is annoying.

One idea could be to use our own testnet chain with our own coins of which we could have lots and lots. Better, but so far we would still spam our public testnet with many pointless transactions and we would have to do the gymnastics with the user interface and the button clicking.

Automation

So what we really want is to automatically run the tests. To give you a feel on what such a test could look like, here is an excerpt from the test file in the repo.

describe('Live user balance', () => {
  test('The balance updates when funds are received', async () => {
    // arrange
    const randomWallet = createRandomWallet()
    const balance = await liveBalance({
      websocketUrl: 'ws://localhost:26657/websocket',
      lcdUrl: 'http://localhost:1317',
    })(randomWallet.key.accAddress)

    onTestFinished(balance.destroy)

    // assert
    expect(balance.getBalance()).toBe(0)

    await waitForBoth([
      // act
      sendFunds(randomWallet.key.accAddress, 1000),
      // assert
      vi.waitFor(() => expect(balance.getBalance()).toBe(1000)),
    ])
  })
})

So here we:

  1. Create a random wallet / address,
  2. programmatically start our subscription,
  3. check the initial value to be correct (namely 0),
  4. wait for the balance to change value1 while we send some funds to the address.

This test is only possible with a precondition: We need to have an account with more or less unlimited funds on the blockchain we want to run our tests on. Let us spawn one!

Preparation

In order to have a local blockchain available, we need to start and configure a node before we can run the tests. You can see in the CI script that we use docker compose for this. We use the official miniwasm docker image but change the entrypoint to be:

#!/bin/sh

if [ -f /root/.minitia/config/genesis.json ]; then
  echo "genesis.json found. Skipping initialization commands..."
else
  echo "genesis.json not found. Running initialization commands..."
  minitiad init operator --chain-id testnet &&
  minitiad keys add operator --keyring-backend test &&
  minitiad genesis add-genesis-account init1j65sfxpkpstety502upxk0t6xhvcuclxawpqt8 1000000000000000umin --keyring-backend test &&
  minitiad genesis add-genesis-validator operator --keyring-backend test &&
  minitiad start
fi

echo "Starting minitiad..."
minitiad start

This means that before we start the chain for the first time, we credit ourselves a very high amount of funds to be used in the tests.

Please refer for the complete setup to the repository. Once all is in place we can start the local block chain with

docker compose up -d

and stop it with

docker compose down -v

which will also delete any state the blockchain had after our tests (probably some random addresses funded with 1000umin).

Development experience

With the local blockchain running in the background and our wallet being as rich as it can possibly be, we can now start developing by running

yarn test

which will start vitest in development mode, so every change to the source files will trigger a rerun of the test. Now rerunning the tests after changes to the code takes less than 800ms in the smallest github code space instance.

Build on Top

I hope you enjoyed this demonstration of a viable test setup for app developers on Initia chains such as Contro. I recommend to fork or clone our repository when you want to build something similar. Please also let me know if you have further comments on the set up, found a better way to do things or have questions on how to get it running for yourselves.

Until then have a glob time!

Levin - Lead Web Developer

Footnotes

  1. Please note that getBalance is synchronous and does not do a query to the blockchain or a remote server. It simply returns the current local value of the balance.

codesmart contractstesting