algorand-typescript-testing is a companion package to Algorand TypeScript that enables efficient unit testing of Algorand TypeScript smart contracts in an offline environment. This package emulates key AVM behaviours without requiring a network connection, offering fast and reliable testing through a familiar TypeScript interface.
The algorand-typescript-testing package provides:
algorand-typescript is a prerequisite for algorand-typescript-testing, providing stubs and type annotations for Algorand TypeScript syntax. It improves code completion and type checking when writing smart contracts. Note that this code isn't directly executable in a standard Node.js environment; it's compiled by puya-ts into TEAL for Algorand Network deployment.
Traditionally, testing Algorand smart contracts involved deployment on sandboxed networks and interacting with live instances. While robust, this approach can be inefficient and lacks versatility for unit testing Algorand TypeScript code.
algorand-typescript-testing leverages TypeScript's testing ecosystem for unit testing without network deployment, enabling rapid iteration and granular logic testing.
NOTE: While
algorand-typescript-testingoffers valuable unit testing capabilities, it's not a replacement for comprehensive testing. Use it alongside other test types, particularly those running against the actual Algorand Network, for thorough contract validation.
algorand-typescript-testing is distributed via npm. Install the package using npm:
npm i @algorandfoundation/algorand-typescript-testing
Let's write a simple contract and test it using the algorand-typescript-testing framework.
algorand-typescript-testing includes a TypeScript transformer (puyaTsTransformer) that ensures contracts (with .algo.ts extension) and tests (with .algo.spec.ts or .algo.test.ts extensions) behave consistently between Node.js and AVM environments.
The transformer replicates AVM behaviour, such as integer-only arithmetic where 3 / 2 produces 1. For code requiring standard Node.js behaviour (e.g., 3 / 2 produces 1.5), place it in separate .ts files and reference them from test files.
The transformer also redirects @algorandfoundation/algorand-typescript imports to @algorandfoundation/algorand-typescript-testing/internal to provide executable implementations of Algorand TypeScript constructs like Global, Box, Uint64, and clone.
If there are tests that do not need to be executed in the AVM context, such as end-to-end tests, simply use .test.ts or .spec.ts file extensions without .algo part and the transformer will skip them.
If you are using vitest with @rollup/plugin-typescript plugin, configure puyaTsTransformer as a before stage transformer of the typescript plugin in vitest.config.mts file.
import typescript from '@rollup/plugin-typescript'
import { defineConfig } from 'vitest/config'
import { puyaTsTransformer } from '@algorandfoundation/algorand-typescript-testing/vitest-transformer'
export default defineConfig({
esbuild: {},
test: {
setupFiles: 'vitest.setup.ts',
},
plugins: [
typescript({
transformers: {
before: [puyaTsTransformer],
},
}),
],
})
algorand-typescript-testing package also exposes additional equality testers which enable smart contract developers to write terser tests by avoiding type casting in assertions. They can be set up in the beforeAll hook in the setup file, vitest.setup.ts.
import { beforeAll, expect } from 'vitest'
import { addEqualityTesters } from '@algorandfoundation/algorand-typescript-testing'
beforeAll(() => {
addEqualityTesters({ expect })
})
If you are using jest with ts-jest, @jest/globals and ts-node plugins, configure puyaTsTransformer as a before stage transformer of the typescript plugin in jest.config.ts file.
import { createDefaultEsmPreset, type JestConfigWithTsJest } from 'ts-jest'
const presetConfig = createDefaultEsmPreset({})
const jestConfig: JestConfigWithTsJest = {
...presetConfig,
testMatch: ['**/*.algo.test.ts'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
astTransformers: {
before: ['node_modules/@algorandfoundation/algorand-typescript-testing/test-transformer/jest-transformer.mjs'],
},
},
],
},
extensionsToTreatAsEsm: ['.ts'],
}
export default jestConfig
algorand-typescript-testing package also exposes additional equality testers which enable smart contract developers to write terser tests by avoiding type casting in assertions. They can be set up in the beforeAll hook in the setup file, jest.setup.ts.
import { beforeAll, expect } from '@jest/globals'
import { addEqualityTesters } from '@algorandfoundation/algorand-typescript-testing'
beforeAll(() => {
addEqualityTesters({ expect })
})
You'll also need to run jest with the --experimental-vm-modules and --experimental-require-module flags in the package.json. This requires node 20.17 or greater.
{
"name": "puya-ts-demo",
"scripts": {
"test:jest": "tsc && node --experimental-vm-modules --experimental-require-module node_modules/jest/bin/jest"
},
"engines": {
"node": ">=20.17"
}
}
There is also a patch file ts-jest+29.2.5.patch that needs to be applied to ts-jest package for the puyaTsTransformer to work with the test files.
<rootDir>\patches folder."postinstall": "patch-package", script in package.json file.
The patch will then be applied with every npm install call.diff --git a/node_modules/ts-jest/dist/legacy/compiler/ts-compiler.js b/node_modules/ts-jest/dist/legacy/compiler/ts-compiler.js
index 5198f8f..addb47c 100644
--- a/node_modules/ts-jest/dist/legacy/compiler/ts-compiler.js
+++ b/node_modules/ts-jest/dist/legacy/compiler/ts-compiler.js
@@ -234,7 +234,7 @@ var TsCompiler = /** @class */ (function () {
var _a;
// Initialize memory cache for typescript compiler
this._parsedTsConfig.fileNames
- .filter(function (fileName) { return constants_1.TS_TSX_REGEX.test(fileName) && !_this.configSet.isTestFile(fileName); })
+ .filter(function (fileName) { return constants_1.TS_TSX_REGEX.test(fileName); })
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
.forEach(function (fileName) { return _this._fileVersionCache.set(fileName, 0); });
/* istanbul ignore next */
After the setup, the examples provided using vitest can be converted to work with jest by simply swapping the import {...} from 'vitest' with import {...} from '@jest/globals'.
import {
arc4,
assert,
Bytes,
GlobalState,
gtxn,
LocalState,
op,
readonly,
Txn,
uint64,
Uint64,
} from '@algorandfoundation/algorand-typescript'
export default class VotingContract extends arc4.Contract {
topic = GlobalState({ initialValue: 'default_topic', key: Bytes('topic') })
votes = GlobalState({ initialValue: Uint64(0), key: Bytes('votes') })
voted = LocalState<uint64>({ key: Bytes('voted') })
@arc4.abimethod()
public setTopic(topic: string): void {
this.topic.value = topic
}
@arc4.abimethod()
public vote(pay: gtxn.PaymentTxn): boolean {
assert(op.Global.groupSize === 2, 'Expected 2 transactions')
assert(pay.amount === 10_000, 'Incorrect payment amount')
assert(pay.sender === Txn.sender, 'Payment sender must match transaction sender')
if (this.voted(Txn.sender).hasValue) {
return false // Already voted
}
this.votes.value = this.votes.value + 1
this.voted(Txn.sender).value = 1
return true
}
@readonly
public getVotes(): uint64 {
return this.votes.value
}
public clearStateProgram(): boolean {
return true
}
}
import { Uint64 } from '@algorandfoundation/algorand-typescript'
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
import { afterEach, describe, expect, test } from 'vitest'
import VotingContract from './contract.algo'
describe('Voting contract', () => {
const ctx = new TestExecutionContext()
afterEach(() => {
ctx.reset()
})
test('vote function', () => {
// Initialize the contract within the testing context
const contract = ctx.contract.create(VotingContract)
const voter = ctx.defaultSender
const payment = ctx.any.txn.payment({
sender: voter,
amount: 10_000,
})
const result = contract.vote(payment)
expect(result).toEqual(true)
expect(contract.votes.value).toEqual(1)
expect(contract.voted(voter).value).toEqual(1)
})
test('setTopic function', () => {
// Initialize the contract within the testing context
const contract = ctx.contract.create(VotingContract)
const newTopic = ctx.any.string(10)
contract.setTopic(newTopic)
expect(contract.topic.value).toEqual(newTopic)
})
test('getVotes function', () => {
// Initialize the contract within the testing context
const contract = ctx.contract.create(VotingContract)
contract.votes.value = 5
const votes = contract.getVotes()
expect(votes).toEqual(5)
})
})
This example demonstrates key aspects of testing with algorand-typescript-testing for ARC4-based contracts:
ARC4 Contract Features:
arc4.Contract as the base class for the contract.@arc4.abimethod decorator.@arc4.abimethod({readonly: true}) or @readonly.Testing ARC4 Contracts:
arc4.Contract instance within the test context.ctx.any for generating random test data.Transaction Handling:
ctx.any.txn to create test transactions.State Verification:
NOTE: Thorough testing is crucial in smart contract development due to their immutable nature post-deployment. Comprehensive unit and integration tests ensure contract validity and reliability. Optimizing for efficiency can significantly improve user experience by reducing transaction fees and simplifying interactions. Investing in robust testing and optimization practices is crucial and offers many benefits in the long run.
To dig deeper into the capabilities of algorand-typescript-testing, continue with the following sections.