Automated testing | Algorand Developer Portal

Automated testing

Automated testing is a higher-order use case capability provided by AlgoKit Utils that builds on top of the core capabilities. It allows you to use terse, robust automated testing primitives that work across any testing framework (including jest and vitest) to facilitate fixture management, quickly generating isolated and funded test accounts, transaction logging, indexer wait management and log capture.

To see some usage examples check out the all of the automated tests and the various *.spec.ts files (AlgoKit Utils dogfoods it’s own testing library). Alternatively, you can see an example of using this library to test a smart contract with the tests for the on-chain voting tool.

Module import

The testing capability is not exposed from the root algokit module so there is a clear separation between testing functionality and non-testing functionality.

To access all of the functionality in the testing capability individually, you can import the testing module:

import * as algotesting from '@algorandfoundation/algokit-utils/testing';

Algorand fixture

In general, the only entrypoint you will need to use the testing capability is just by importing the algorandFixture since it exposes the rest of the functionality in a manner that is easy to integrate with an underlying test framework like Jest or vitest:

import { algorandFixture } from '@algorandfoundation/algokit-utils/testing';

Using with Jest

To integrate with Jest you need to pass the fixture.newScope method into Jest’s beforeEach method (for per test isolation) or beforeAll method (for test suite isolation) and then within each test you can access fixture.context to access the isolated fixture values.

Per-test isolation

import { describe, test, beforeEach } from '@jest/globals';

import { algorandFixture } from './testing';

describe('MY MODULE', () => {

const fixture = algorandFixture();

beforeEach(fixture.newScope, 10_000); // Add a 10s timeout to cater for occasionally slow LocalNet calls

test('MY TEST', async () => {

const { algorand, testAccount /* ... */ } = fixture.context;

// Test stuff!

});

});

Test suite isolation

import { describe, test, beforeAll } from '@jest/globals';

import { algorandFixture } from './testing';

describe('MY MODULE', () => {

const fixture = algorandFixture();

beforeAll(fixture.newScope, 10_000); // Add a 10s timeout to cater for occasionally slow LocalNet calls

test('MY TEST', async () => {

const { algorand, testAccount /* ... */ } = fixture.context;

// Test stuff!

});

});

Using with vitest

To integrate with vitest you need to pass the fixture.beforeEach method into vitest’s beforeEach method (for per test isolation) or beforeAll method (for test suite isolation) and then within each test you can access fixture.context to access the isolated fixture values.

Per-test isolation

import { describe, test, beforeEach } from 'vitest';

import { algorandFixture } from './testing';

describe('MY MODULE', () => {

const fixture = algorandFixture();

beforeEach(fixture.newScope, 10_000); // Add a 10s timeout to cater for occasionally slow LocalNet calls

test('MY TEST', async () => {

const { algorand, testAccount /* ... */ } = fixture.context;

// Test stuff!

});

});

Test suite isolation

import { describe, test, beforeAll } from 'vitest';

import { algorandFixture } from './testing';

describe('MY MODULE', () => {

const fixture = algorandFixture();

beforeAll(fixture.newScope, 10_000); // Add a 10s timeout to cater for occasionally slow LocalNet calls

test('MY TEST', async () => {

const { algorand, testAccount /* ... */ } = fixture.context;

// Test stuff!

});

});

Fixture configuration

When calling algorandFixture() you can optionally pass in some fixture configuration, with any of these properties (all optional):

Using the fixture context

The fixture.context property is of type AlgorandTestAutomationContext exposes the following properties:

Log capture fixture

If you want to capture log messages from AlgoKit that are issued within your test so that you can assert on them or parse them for debugging information etc. then you can use the log capture fixture.

import { algoKitLogCaptureFixture } from '@algorandfoundation/algokit-utils/testing';

The log capture fixture works by setting the logger within the AlgoKit configuration to be a TestLogger during the test run.

Using with Jest

To integrate with Jest you need to pass the fixture.beforeEach method into Jest’s beforeEach method and then within each test you can access fixture.context to access per-test isolated fixture values.

import { describe, test, beforeEach, afterEach } from '@jest/globals';

import { algoKitLogCaptureFixture } from './testing';

describe('MY MODULE', () => {

const logs = algoKitLogCaptureFixture();

beforeEach(logs.beforeEach);

afterEach(logs.afterEach);

test('MY TEST', async () => {

const { algorand, testAccount } = fixture.context;

// Test stuff!

const capturedLogs = logs.testLogger.capturedLogs;

// do stuff with the logs

});

});

Using with vitest

To integrate with vitest you need to pass the fixture.beforeEach method into vitest’s beforeEach method and then within each test you can access fixture.context to access per-test isolated fixture values.

import { describe, test, beforeEach, afterEach } from 'vitest';

import { algoKitLogCaptureFixture } from './testing';

describe('MY MODULE', () => {

const logs = algoKitLogCaptureFixture();

beforeEach(logs.beforeEach);

afterEach(logs.afterEach);

test('MY TEST', async () => {

const { algorand, testAccount } = fixture.context;

// Test stuff!

const capturedLogs = logs.testLogger.capturedLogs;

// do stuff with the logs

});

});

Snapshot testing the logs

If you want to quickly pin some behaviour of what logic you have does in terms of invoking AlgoKit methods you can do a snapshot test / approval test of the captured log output.

const { algorand, testAccount } = fixture.context;

const result = await algorand.client

.getTypedClientById(HelloWorldContractClient, { id: 0 })

.deploy();

expect(

logging.testLogger.getLogSnapshot({

accounts: [testAccount],

transactions: [result.transaction],

apps: [result.appId],

}),

).toMatchSnapshot();

Waiting for indexer

Often there will be things that you do in your test that you may want to assert in using data that is exclusively in indexer such as transaction notes. The problem is indexer asynchronously indexes the data in algod, even when devmode is turned on and algod instantly confirms transactions.

This means it’s easy to create tests that are flaky and have intermittent test failures (sometimes indexer is up to date and other times it hasn’t caught up yet).

The testing capability provides mechanisms for waiting for indexer to catch up, namely:

Logging transactions

When testing, it can be useful to capture all of the transactions that have been issued with a given test run. They can then be asserted on, or used for waiting for indexer, etc.

The testing capability provides the ability to capture transactions via the TransactionLogger class.

Getting a test account

When testing, it’s often useful to ephemerally generate random accounts, fund them with some number of Algo and then use that account to perform transactions. By creating an ephemeral, random account you naturally get isolation between tests and test runs and don’t need to start from a specific blockchain network state. This makes test less flakey, and also means the same test can be run against LocalNet and (say) TestNet.

The key when generating a test account is getting hold of a dispenser and then ensuring the test account is funded.

To make it easier to quickly get a test account the testing capability provides the following mechanisms:

The parameters object that controls test account generation, GetTestAccountParams, has the following properties: