Testing
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.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 } from '@jest/globals'
import { algorandFixture } from './testing'
describe('MY MODULE', () => {
const fixture = algorandFixture()
beforeEach(fixture.beforeEach, 10_000)
test('MY TEST', async () => {
const { algod, testAccount /* ... */ } = fixture.context
// Test stuff!
})
})
Occasionally there may be a delay when first running the fixture setup so we add a 10s timeout to avoid intermittent test failures (10_000
).
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 } from 'vitest'
import { algorandFixture } from './testing'
describe('MY MODULE', () => {
const fixture = algorandFixture()
beforeEach(fixture.beforeEach, 10_000)
test('MY TEST', async () => {
const { algod, 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):
algod?: Algodv2
- An optional algod client, if not specified then it will create one against environment variables defined network (if present) or default LocalNetindexer?: Indexer
- An optional indexer client, if not specified then it will create one against environment variables defined network (if present) or default LocalNetkmd?: Kmd
- An optional kmd client, if not specified then it will create one against environment variables defined network (if present) or default LocalNettestAccountFunding?: AlgoAmount
- The amount of funds to allocate to the default testing account, if not specified then it will get10
ALGOs
Using the fixture context¶
The fixture.context
property is of type AlgorandTestAutomationContext
exposes the following properties from which you can pick which ones you want in a given test using an object destructuring assignment:
algod: Algodv2
- Proxy Algod client instance that will log sent transactions intransactionLogger
indexer: Indexer
- Indexer client instancekmd: Kmd
- KMD client instancetransactionLogger: TransactionLogger
- Transaction logger that will log transaction IDs for all transactions issued byalgod
testAccount: Account
- Funded test account that is ephemerally created for each testgenerateAccount: (params: GetTestAccountParams) => Promise<Account>
- Generate and fund an additional ephemerally created accountwaitForIndexer: () => Promise<void>
- Wait for the indexer to catch up with all transactions logged by transactionLoggerwaitForIndexerTransaction: (transactionId: string) => Promise<TransactionLookupResult>
- Wait for the indexer to catch up with the given transaction ID
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 () => {
// 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 () => {
// 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. The only problem is this output will contain identifiers that will change for every test run and thus will constantly break the snapshot. In order to work around this you can use the getLogSnapshot
method on the TestLogger
, which will replace those changing values with predictable strings to keep the snapshot integrity intact.
This might look something like this:
const { algod, indexer, testAccount } = fixture.context
const result = await algokit.deployApp(getAppDeploymentParams(), algod, indexer)
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:
algotesting.runWhenIndexerCaughtUp(run: () => Promise<T>)
- Executes the given action every 200ms up to 20 times until there is no longer an error with astatus
property with404
and then returns the result of the action; this will work for any call that calls indexer APIs expecting to return a single recordalgorandFixture.waitForIndexer()
- Waits for indexer to catch up with all transactions that have been captured by thetransactionLogger
in the Algorand fixturealgorandFixture.waitForIndexerTransaction(transactionId)
- Waits for indexer to catch up with the single transaction of the given ID
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.
The TransactionLogger
has the following methods:
logRawTransaction(signedTransactions: Uint8Array | Uint8Array[])
- Logs the IDs of the given signed transaction(s)capture(algod)
- Returns a proxyalgosdk.Algodv2
instance that wraps the givenalgod
client that will calllogRawTransaction
for every call tosendRawTransaction
on that algod instancesentTransactionIds
- Returns the currently captured list of transaction IDs that have been loggedclear()
- Clears the current list of transaction IDswaitForIndexer(indexer)
- Waits for the given indexer instance to catch up with the currently logged transaction IDs
The easiest way to use this functionality is via the Algorand fixture, which automatically provides a transactionLogger
and a proxy algod
connected to that transactionLogger
.
Getting a test account¶
When testing, it's often useful to ephemerally generate random accounts, fund them with some number of ALGOs 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:
algotesting.getTestAccount(testAccountParams, algod, kmd?)
- Generates a random new account, logs the mnemonic of the account (unless suppressed), funds it from the dispenseralgorandFixture.testAccount
- A test account that is always generated for every test (log output suppressed to reduce noise, but worth noting that means the mnemonic isn't logged for this account), by default it is given 10 ALGOs unless overridden in the fixture configalgorandFixture.generateAccount(testAccountParams)
- Allows you to quickly generate a test account with thealgod
andkmd
instances that are part of the given fixture
The parameters object that controls test account generation, GetTestAccountParams
, has the following properties:
initialFunds: AlgoAmount
- Initial funds to ensure the account hassuppressLog?: boolean
- Whether to suppress the log (which includes a mnemonic) or not (default: do not suppress the log)