ava Documentation

Node.js test runner that lets you develop with confidence 🚀

20,845 stars JavaScript JavaScript 1 file · ~29,875 tokens #assert#async#async-functions#ava#cli#cli-app#concurrency#javascript
RAW Doc

Recipes/Babel

Configuring Babel with AVA

Translations: Français

AVA is ESM-only. @babel/register only hooks legacy require() loading, so this setup is no longer supported.

If you need Babel, precompile your files before running AVA or use an ESM-capable loader configured through nodeArguments.


Recipes/Browser Testing

Setting up AVA for browser testing

AVA is running in a Node.js environment. JavaScript that runs in a browser will likely expect the browser DOM globals to be in place.
With help from a package called jsdom,
you can write unit tests with ava also for JavaScript that will run in a browser
and relying on browser specific globals such as window, document and navigator.

Install jsdom

bash
npm install --save-dev jsdom

Writing unit tests

Use jsdom to set the globals and the DOM elements that the test target is expecting.

An example Unit Test

The JavaScript code to be tested is doing a DOM query, such as: document.querySelector('#my-element-id').

To make the code testable with ava, add the element to jsdom and set the global object.

js
import test from 'ava';
import { JSDOM } from 'jsdom';

test.before(() => {
  const dom = new JSDOM('<div id="my-element-id" />');  // insert any html needed for the unit test suite here
  global.document = dom.window.document; // add the globals needed for the unit tests in this suite.
});

test('this is an example', (t) => {
    const res = myTarget.runFunctionThatExpectsTheDocumentGlobalAndElement();

    t.truthy(res);
});

Important note

In general, adding globals to the Node.js environment is recommended against by jsdom.
Please read through the linked wiki page and make sure you understand why.


Recipes/Code Coverage

Code coverage

Translations: Español, Français, Italiano, 日本語, Português, Русский, 简体中文

Use c8 to compute the code coverage of your tests.

First install c8:

text
$ npm install --save-dev c8

At its simplest run AVA through c8. In your package.json file:

json
{
	"scripts": {
		"test": "c8 ava"
	}
}

You may want to exclude the coverage directory from source control. Assuming you're using Git, add the following to your .gitignore file:

text
coverage

Recipes/Debugging With Chrome Devtools

Debugging tests with Chrome DevTools

Translations: Français

You can debug your tests using Chrome DevTools.

Open Chrome, then navigate to chrome://inspect/. Click the Open dedicated DevTools for Node link within the Devices section.

In the DevTools for Node window, navigate to Sources and in the left-hand column select Filesystem. Add your project directory to the workspace. Make sure to grant permission.

Now run a specific test file:

console
npx ava debug test.js

The DevTools should connect automatically and your tests will run. Use DevTools to set breakpoints, or use the debugger keyword.

Run with the --break option to ensure the DevTools hit a breakpoint right before the test file is loaded:

console
npx ava debug --break test.js

By default the inspector listens on 127.0.0.1:9229. You can customize the host and the port:

console
npx ava debug --host 0.0.0.0 --port 9230 test.js

You'll have to add a connection for this port in the Connection tab.


Recipes/Debugging With Vscode

Debugging tests with Visual Studio Code

Translations: Français

You can debug your tests using Visual Studio Code.

Debugging with the debug terminal

You can use VS Code's “JavaScript Debug Terminal” to automatically debug AVA run on the command-line.

  1. From the Command Palette (F1 or command + shift + p / control + shift + p), run Debug: JavaScript Debug Terminal
  2. Run npx ava in the terminal

Creating a launch configuration

Alternatively you can create a launch configuration, which makes it easier to debug individual test files.

  1. Open a workspace for your project.
  2. In the sidebar click the Debug handle.
  3. Create a launch.json file.
  4. Select the Node.js environment.
  5. Add following to the configurations array and save changes:
json
{
  "type": "node",
  "request": "launch",
  "name": "Debug AVA test file",
  "program": "${workspaceFolder}/node_modules/ava/entrypoints/cli.js",
  "args": [
    "${file}"
  ],
  "outputCapture": "std",
  "console": "integratedTerminal", // optional
  "skipFiles": [
    "<node_internals>/**/*.js"
  ]
}

Using the debugger

Open the file(s) you want to debug. You can set breakpoints or use the debugger keyword.

Now, with a test file open, from the Debug menu run the Debug AVA test file configuration.

Debugging precompiled tests

If you compile your test files into a different directory, and run the tests from that directory, the above configuration won't work.

Assuming the names of your test files are unique you could try the following configuration instead. This assumes the compile output is written to the build directory. Adjust as appropriate:

json
{
  "type": "node",
  "request": "launch",
  "name": "Debug AVA test file",
  "program": "${workspaceFolder}/node_modules/ava/entrypoints/cli.js",
  "args": [
    "build/**/${fileBasenameNoExtension}.*"
  ],
  "outputCapture": "std",
  "console": "integratedTerminal", // optional
  "skipFiles": [
    "<node_internals>/**/*.js"
  ]
}

Using Yarn PnP (Plug'n'Play)

As Yarn's PnP (Plug'n'Play) installation strategy does not produce a node_modules folder, the ava binary must be called using yarn run:

json
{
  "type": "node",
  "request": "launch",
  "name": "Debug AVA test file",
  "runtimeExecutable": "yarn",
  "runtimeArgs": ["run", "ava"],
  "args": ["${file}"],
  "outputCapture": "std",
  "console": "integratedTerminal", // optional
  "skipFiles": [
    "<node_internals>/**/*.js"
  ]
}

Serial debugging

By default AVA runs tests concurrently. This may complicate debugging. Instead make sure AVA runs only one test at a time.

Note that, if your tests aren't properly isolated, certain test failures may not appear when running the tests serially.

If you use the debug terminal make sure to invoke AVA with npx ava --serial.

Or, if you're using a launch configuration, add the --serial argument:

json
{
  "type": "node",
  "request": "launch",
  "name": "Debug AVA test file",
  "program": "${workspaceFolder}/node_modules/ava/entrypoints/cli.js",
  "args": [
    "--serial",
    "${file}"
  ],
  "outputCapture": "std",
  "console": "integratedTerminal", // optional
  "skipFiles": [
    "<node_internals>/**/*.js"
  ]
}

Recipes/Debugging With Webstorm

Debugging tests with WebStorm

Translations: Français

This recipe is outdated.


Starting with version 2016.2, WebStorm and other JetBrains IDEs (IntelliJ IDEA Ultimate, PHPStorm, PyCharm Professional, and RubyMine with installed Node.js plugin) allow you to debug AVA tests.

Setup using Node.js

Add a new Node.js Run/Debug configuration: select Edit Configurations... from the dropdown list on the top right, then click + and select Node.js.

In the JavaScript file field specify the path to AVA in the project's node_modules folder: node_modules/.bin/ava on macOS and Linux or node_modules/.bin/ava.cmd on Windows.

In the Application parameters pass the CLI flags you're using and the test files you would like to debug, for example --verbose test.js.

In the Node parameters, pass the --inspect-brk flag to enable the Node inspector.

Save the configuration.

Setup using npm

Execute npx @ava/init in your project directory to add AVA to your package.json.

Your package.json will look something like this:

json
{
	"name": "awesome-package",
	"scripts": {
		"test": "ava"
	},
	"devDependencies": {
		"ava": "^1.0.0"
	}
}

Add a new npm Run/Debug configuration: select Edit Configurations... from the dropdown list on the top right, then click + and select npm.

Use the following configuration parameters:

  • package.json: Path to your project's package.json file
  • Command: test

Your IDE will then execute npm run test and thus call node_modules/.bin/ava and the AVA-configuration you have specified in your package.json.

In the Node parameters, pass --inspect-brk.

Don't forget to select a Node.js interpreter.

Save the configuration.

Debug

Set breakpoints in the code.

Hit the green Debug button next to the list of configurations on the top right. The Debug tool window will appear. Once the breakpoint is hit, you can evaluate variables and step through the code. When debugging multiple test files, you can switch between the processes using the dropdown in the Frames pane.


Recipes/Endpoint Testing

Endpoint testing

Translations: Español, Français, Italiano, 日本語, Português, Русский, 简体中文

AVA doesn't have a built-in method for testing endpoints, but you can use any HTTP client of your choosing, for example ky. You'll also need to start an HTTP server, preferably on a unique port so that you can run tests in parallel. For that we recommend async-listen.

Since tests run concurrently, it's best to create a fresh server instance at least for each test file, but perhaps even for each test. This can be accomplished with test.before() and test.beforeEach() hooks and t.context. If you start your server using a test.before() hook you should make sure to execute your tests serially.

Check out the example below:

js
import {createServer} from 'node:http';

import {listen} from 'async-listen';
import test from 'ava';
import ky, {HTTPError} from 'ky';

import app from './app.js';

test.before(async t => {
	t.context.server = createServer(app);
	t.context.prefixUrl = await listen(t.context.server);
});

test.after.always(t => {
	t.context.server.close();
});

test.serial('get /user', async t => {
	const {email} = await ky('user', {prefixUrl: t.context.prefixUrl}).json();

	t.is(email, '[email protected]');
});

test.serial('404', async t => {
	await t.throwsAsync(
		ky('password', {prefixUrl: t.context.prefixUrl}),
		{message: /Request failed with status code 404 Not Found/, instanceOf: HTTPError},
	);
});

Other libraries you may find useful:


Recipes/Endpoint Testing With Mongoose

Endpoint testing with Mongoose

Translations: Français

This recipe shows you how to test your endpoints with AVA and Mongoose, assuming you use Express as your framework.

Setup

This recipe uses the following libraries:

  1. mongodb-memory-server (A MongoDB in-memory Server)
  2. SuperTest (An endpoint testing library)
  3. Mongoose

Install the first two libraries by running the following code:

console
$ npm install --save-dev mongodb-memory-server supertest

You should have Mongoose installed already. If not, run the following code to install it:

(Note: You need at least Mongoose v4.11.3)

console
$ npm install mongoose

Prerequisites

You'll need a server file and a Mongoose model. See the server.js and models/User.js examples.

Note that server.js does not start the app. Instead this must be done by SuperTest, so that the app endpoints can be tested. If you're using Express for your application, make sure you have a startup file that imports app and calls app.listen().

Your test file

First, include the libraries you need:

js
// Libraries required for testing
import test from 'ava';
import request from 'supertest';
import {MongoMemoryServer} from 'mongodb-memory-server';
import mongoose from 'mongoose';

// Your server and models
import app from '../server';
import User from '../models/User';

Next start the in-memory MongoDB instance and connect to Mongoose:

js
// Create connection to Mongoose before tests are run
test.before(async t => {
	// First start MongoDB instance
	t.context.mongod = await MongoMemoryServer.create();
	// And connect
	await mongoose.connect(t.context.mongod.getUri());
});

When you run your first test, MongoDB downloads the latest MongoDB binaries. The download is ~70MB so this may take a minute.

You'll want to populate your database with dummy data. Here's an example:

js
test.beforeEach(async () => {
	const user = new User({
		email: '[email protected]',
		name: 'One'
	});
	await user.save();
});

Dummy data should be cleared after each test:

js
test.afterEach.always(() => User.remove());

Now you can use SuperTest to send off a request for your app endpoint. Use AVA for your assertions:

js
// Note that the tests are run serially. See below as to why.

test.serial('litmus get user', async t => {
	const {app} = t.context;
	const res = await request(app)
		.get('/litmus')
		.send({email: '[email protected]'});
	t.is(res.status, 200);
	t.is(res.body.name, 'One');
});

test.serial('litmus create user', async t => {
	const {app} = t.context;
	const res = await request(app)
		.post('/litmus')
		.send({
			email: '[email protected]',
			name: 'New name'
		});

	t.is(res.status, 200);
	t.is(res.body.name, 'New name');

	// Verify that user is created in DB
	const newUser = await User.findOne({email: '[email protected]'});
	t.is(newUser.name, 'New name');
});

Finally disconnect from and stop MongoDB when all tests are done:

js
test.after.always(async t => {
	await mongoose.disconnect();
	await t.context.mongod.stop();
});

And you're done!

Reusing the configuration across files

You may choose to extract the code for the test.before, test.beforeEach, test.afterEach.always and test.after.always hooks into a separate file. Have a look at https://github.com/zellwk/ava-mdb-test for an example.

Using `test.serial` instead of `test`

Your tests likely change the database. Using test() means they run concurrently, which may cause one test to affect another. Instead if you use test.serial() then the tests will run one at a time. You can then clean up your database between test runs, making the tests more predictable.

You could run tests concurrently if you create separate Mongoose connections for each test. This is harder to set up, though. More information can be found here.


Recipes/Isolated Mongodb Integration Tests

Isolated MongoDB integration tests

Translations: Français

How to run disposable MongoDB databases in your AVA tests with per-test isolation.

This uses MongoMem, which allows you to quickly run a temporary MongoDB server locally. It uses temporary file storage which is destroyed when the server stops.

Install MongoDB in-memory Server (MongoMem)

In the root directory of your app, run:

console
$ npm install --save-dev mongomem

Using MongoMem

In your test file, import the module, and run the server.

Make sure to run the server at the start of your file, outside of any test cases.

js
import test from 'ava';
import {MongoDBServer} from 'mongomem';

test.before('start server', async t => {
	await MongoDBServer.start();
})

test('some feature', async t => {
	const connectionString = await MongoDBServer.getConnectionString();

	// connectionString === 'mongodb://localhost:27017/3411fd12-b5d6-4860-854c-5bbdb011cb93'
	// Use `connectionString` to connect to the database with a client of your choice. See below for usage with Mongoose.
});

Cleaning Up

After you have run your tests, you should include a test.after.always() method to clean up the MongoDB server. This will remove any temporary files the server used while running.

This is normally cleaned up by your operating system, but it is good practise to do it manually.

js
test.after.always('cleanup', t => {
	MongoDBServer.tearDown(); // Cleans up temporary file storage
});

Debugging

If the server does not seem to start, you can set the MongoDBServer.debug = true; option before you call MongoDBServer.start(). This will allow the MongoDB server to print connection or file permission errors when it's starting. It checks and picks an available port to run the server on, so errors are likely to be related to file permissions.

Extra: Setup and use in Mongoose

Mongoose is a robust Object-Document-Mapper (ODM) for MongoDB. Refer to its documentation to get started with Mongoose.

To use Mongoose effectively with AVA, check out the Mongoose integration docs.


Recipes/Passing Arguments To Your Test Files

Passing arguments to your test files

Translations: Français

You can pass command line arguments to your test files. Use the -- argument terminator to separate AVA's arguments from your own:

js
// test.js
import test from 'ava';

test('argv', t => {
	t.deepEqual(process.argv.slice(2), ['--hello', 'world']);
});
console
$ npx ava -- --hello world

You need two -- argument terminators if you're invoking AVA through an npm test script:

json
{
	"scripts": {
		"test": "ava"
	}
}
console
$ npm test -- -- --hello world

Recipes/Puppeteer

Testing web apps using Puppeteer

Translations: Français

Dependencies

  • Puppeteer: npm install --save-dev puppeteer

Setup

The first step is setting up a helper to configure the environment:

./test/_withPage.js

js
import puppeteer from 'puppeteer';

export default async (t, run) => {
	const browser = await puppeteer.launch();
	const page = await browser.newPage();
	try {
		await run(t, page);
	} finally {
		await page.close();
		await browser.close();
	}
};

Usage example

./test/main.js

js
import test from 'ava';
import withPage from './_withPage.js';

const url = 'https://google.com';

test('page title should contain "Google"', withPage, async (t, page) => {
	await page.goto(url);
	t.true((await page.title()).includes('Google'));
});

test('page should contain an element with `#hplogo` selector', withPage, async (t, page) => {
	await page.goto(url);
	t.not(await page.$('#hplogo'), null);
});

test('search form should match the snapshot', withPage, async (t, page) => {
	await page.goto(url);
	const innerHTML = await page.evaluate(form => form.innerHTML, await page.$('#searchform'));
	t.snapshot(innerHTML);
});

Recipes/Shared Workers

Extending AVA using shared workers

Shared workers are a powerful AVA feature. A program can be loaded in a worker thread in AVA's main process and then communicate with code running in the test workers. This enables your tests to better utilize shared resources during a test run, as well as providing opportunities to set up these resources before tests start (or clean them up after).

When you use watch mode, shared workers remain loaded across runs.

Available plugins

Writing plugins

You can write your own plugins. They can be high-level, like @ava/get-port which is built using @ava/cooperate, or low-level like @ava/cooperate itself.

Here we'll discuss building low-level plugins.

Registering a shared worker

Plugins are registered inside test workers. They'll provide the path for the shared worker, which AVA will load in a worker thread in its main process. For each unique path one worker thread is started.

Plugins communicate with their shared worker using a protocol. Protocols are versioned independently from AVA itself. This allows us to make improvements without breaking existing plugins. Protocols are only removed in major AVA releases.

Plugins can be compatible with multiple protocols. AVA will select the best protocol it supports. If AVA does not support any of the specified protocols it'll throw an error. The selected protocol is available on the returned worker object.

js
import {registerSharedWorker} from 'ava/plugin';

const shared = registerSharedWorker({
  filename: path.resolve(__dirname, 'worker.js'),
  supportedProtocols: ['ava-4']
});

Within a test process you can only register one worker for each filename. Filenames are compared as-is, without normalization. If you call registerSharedWorker() a second time, the same worker instance is returned.

If for some reason you want to load the same file as multiple different workers, you can append a unique hash to the end of the filename:

js
import crypto from 'crypto';
import {registerSharedWorker} from 'ava/plugin';

const key = Math.random() > 0.5 ? 'worker-a' : 'worker-b';

const shared = registerSharedWorker<any>({
  filename: new URL(
    `file:${path.resolve(
      __dirname,
      'worker.js'
    )}#${encodeURIComponent(key)}`
  ),
  initialData: {workerKey: key},
  supportedProtocols: ['ava-4']
});

This works because the filename parameter accepts URL objects, meaning you could use a query component for the key instead if you wanted.

You can supply a teardown() function which will be called after all tests have finished. If you call registerSharedWorker() multiple times then the teardown() function will be invoked for each registration, even though you only got one worker instance. The most recently registered teardown() function is called first, and so forth. teardown() functions execute sequentially.

js
const worker = registerSharedWorker({
  filename: path.resolve(__dirname, 'worker.js'),
  supportedProtocols: ['ava-4'],
  teardown () {
    // Perform any clean-up within the test process itself.
  }
});

You can also provide some data passed to the shared worker when it is loaded. Of course, it is only loaded once, so this is only useful in limited circumstances:

js
const shared = registerSharedWorker({
  filename: path.resolve(__dirname, 'worker.js'),
  initialData: {hello: 'world'},
  supportedProtocols: ['ava-4']
});

On this shared object, protocol is set to the selected protocol. Since the shared worker is loaded asynchronously, available provides a promise that fulfils when the shared worker first becomes available. currentlyAvailable reflects whether the worker is, well, currently available.

There are two more methods available on the shared object, which we'll get to soon.

Initializing the shared worker

AVA loads the shared worker (as identified through the filename option) in a worker thread. This must be an ES module file with a default export. The filename must be an absolute path using the file: protocol or a URL instance.

The default export must be a factory method. Like when calling registerSharedWorker(), it must negotiate a protocol:

js
export default ({negotiateProtocol}) => {
  const main = negotiateProtocol(['ava-4']);
}

On this main object, protocol is set to the selected protocol. initialData holds the data provided when the worker was first registered.

When you're done initializing the shared worker you must call main.ready(). This makes the worker available in test workers. You can call main.ready() asynchronously.

Any errors thrown by the factory method will crash the worker thread and make the worker unavailable in test workers. The same goes for unhandled rejections. The factory method may return a promise.

Communicating between test workers and the shared worker

AVA's low-level shared worker infrastructure is primarily about communication. You can send messages from test workers to the shared worker, and the other way around. Higher-level logic can be implemented on top of this message passing infrastructure.

Message data is serialized using the V8 Serialization API. Please read up on some important limitations.

In the shared worker you can subscribe to messages from test workers:

js
export default async ({negotiateProtocol}) => {
  const main = negotiateProtocol(['ava-4']).ready();

  for await (const message of main.subscribe()) {
    // …
  }
}

Messages have IDs that are unique for the main AVA process. Across AVA runs you may see the same ID. Access the ID using the id property.

Access message data using the data property.

You can reply to a received message by calling reply(). This publishes a message to the test process the message originated from. You can then subscribe to replies to that message using replies().

To illustrate this here's a "game" of Marco Polo:

js
export default ({negotiateProtocol}) => {
  const main = negotiateProtocol(['ava-4']).ready();

  play(main.subscribe());
};

const play = async (messages) => {
  for await (const message of messages) {
    if (message.data === 'Marco') {
      const response = message.reply('Polo');
      play(response.replies());
    }
  }
}

(Of course this sets up many reply listeners which is rather inefficient.)

You can also broadcast messages to all connected test workers:

js
export default async ({negotiateProtocol}) => {
  const main = negotiateProtocol(['ava-4']).ready();

  for await (const message of main.subscribe()) {
  	if (message.data === 'Bingo!') {
      main.broadcast('Bingo!');
    }
  }
}

Like with reply(), broadcast() returns a published message which can receive replies. Call replies() to get an asynchronous iterator for reply messages.

Each received message has a testWorker property to represent the test worker that the message originated from. All messages from the same test worker have the same value for this property.

These test workers have a unique ID (which, like message IDs, is unique for the main process). Access it using the id property. The path of the test file is available through the file property. Use publish() to send messages directly to the test worker, and subscribe() to receive messages from the test worker. This works the same as main.subscribe().

Of course you don't need to wait for a message from a test worker to access this object. Use main.testWorkers() to get an asynchronous iterator which produces each newly connected test worker:

js
export default async ({negotiateProtocol}) => {
  const main = negotiateProtocol(['ava-4']).ready();

  for await (const testWorker of main.testWorkers()) {
    main.broadcast(`New test file: ${testWorker.file}`);
  }
}

Within test workers, once the shared worker is available, you can publish messages:

js
shared.publish('Marco');

Or subscribe to messages:

js
(async () => {
  for await (const message of shared.subscribe()) {
    if (message.data === 'Polo') {
      message.reply('Marco');
    }
  }
})();

publish() throws when the shared worker is not yet available. publish() and subscribe() throw when the worker has crashed.

Message IDs are available and are unique for the main AVA process.

Messages are always produced in their own turn of the event loop. This means you can use async/await to process a previous message or subscribe to replies and you'll be guaranteed to receive them.

Cleaning up resources

Test workers come and go while the shared worker remains. It's therefore important to clean up resources.

Messages are subscribed to using async iterators. These return when the test worker exits.

You can register teardown functions to be run when the test worker exits:

js
export default async ({negotiateProtocol}) => {
  const main = negotiateProtocol(['ava-4']).ready();

  for await (const testWorker of main.testWorkers()) {
    testWorker.teardown(() => {
      // Bye bye…
    });
  }
}

The most recently registered function is called first, and so forth. Functions execute sequentially.

More interestingly, a wrapped teardown function is returned so that you can call it manually. AVA still ensures the function only runs once.

js
export default ({negotiateProtocol}) => {
  const main = negotiateProtocol(['ava-4']).ready();

  for await (const worker of testWorkers) {
    counters.set(worker, 0);
    const teardown = worker.teardown(() => {
      counters.delete(worker);
    });

    waitForTen(worker.subscribe(), teardown);
  }
}

const counters = new WeakMap();

const waitForTen = async (messages, teardown) => {
  for await (const {testWorker} of messages) {
    const count = counters.get(testWorker) + 1;
    if (count === 10) {
      teardown();
    } else {
      counters.set(testWorker, count);
    }
  }
};

Now it's your turn

We're pretty excited about this feature! But we need more real-world experience in building AVA plugins before we can make it generally available. Please give feedback and build plugins. We'd be more than happy to promote them.

Not sure what to build? Previously folks have expressed a desire for mutexes, managing Puppeteer instances, starting (database) servers and so forth.

We could also extend the shared worker implementation in AVA itself. Perhaps so you can run code before a new test run, even with watch mode. Or so you can initialize a shared worker based on the AVA configuration, not when a test file runs.

Please comment here with ideas, questions and feedback.


Recipes/Splitting Tests Ci

Splitting tests in CI

AVA automatically detects whether your CI environment supports parallel builds using ci-parallel-vars. When parallel builds support is detected, AVA sorts the all detected test files by name, and splits them into chunks. Each CI machine is assigned a chunk (subset) of the tests, and then each chunk is run in parallel.

To disable this feature, set utilizeParallelBuilds to false in your AVA configuration.

To better distribute the tests across the machines, you can configure a custom comparator function:

ava.config.js:

js
import fs from 'node:fs';

// Assuming 'test-data.json' structure is:
// {
// 	'tests/test1.js': { order: 1 },
// 	'tests/test2.js': { order: 0 }
// }
const testData = JSON.parse(fs.readFileSync('test-data.json', 'utf8'));

export default {
	sortTestFiles: (file1, file2) => testData[file1].order - testData[file2].order,
};

Splitting tests on GitHub Actions

Although GitHub Actions doesn't support parallel builds out-of-the-box with AVA, you can configure it manually by using a matrix:

.github/workflows/test.yml:

yml
on: push
jobs:
  test:
    strategy:
      # Don't cancel test runs if one fails
      fail-fast: false
      # Run 4 jobs in parallel, each executing a subset of all tests
      matrix:
        node_index: [0, 1, 2, 3]
        total_nodes: [4]

    runs-on: ubuntu-latest
    steps:
      # Check out code and perform setup steps
      # ...

      - name: Test
        run: npx ava
        env:
          CI_NODE_INDEX: ${{ matrix.node_index }}
          CI_NODE_TOTAL: ${{ matrix.total_nodes }}

Recipes/Test Setup

Test setup

Translations: Français

Tests can be set up using the beforeEach() hook. Often though you could use a plain setup function instead. This recipe helps you decide what's best for your use case.

The `beforeEach()` hook versus setup functions

The beforeEach() hook has some downsides. For example, you cannot turn it off for specific tests, nor can you apply it to specific tests. As an alternative, you can use simple functions. This allows you to use multiple setup functions for different setup requirements and call different parts of setup from different tests. You can even have setup functions with parameters so tests can customize their own setup.

Let's say you have a function that interacts with the file system. Perhaps you run a few tests using mock-fs, and then a few that use the real file system and a temporary directory. Or you have a setup function that you run with valid data for some tests and invalid data for other tests, all within the same test file.

You could do all these things using plain setup functions, but there are tradeoffs:

beforeEach() Setup functions
⛔️   used for all tests ✅   can change or skip depending on test
⛔️   more overhead for beginners, "some magic" ✅   easier for beginners, "no magic"
✅   built-in support for observables ⛔️   must use promises for asynchronous behavior
✅   failure has friendly output ⛔️   errors are attributed to the test
✅   corresponding afterEach and afterEach.always for cleanup ⛔️   cannot easily clean up

Complex test setup

In this example, we have both a beforeEach() hook, and then more modifications within each test.

js
test.beforeEach(t => {
	setupConditionA(t);
	setupConditionB(t);
	setupConditionC(t);
});

test('first scenario', t => {
	tweakSomething(t);
	const someCondition = t.context.thingUnderTest();
	t.true(someCondition);
});

test('second scenario', t => {
	tweakSomethingElse(t);
	const someOtherCondition = t.context.thingUnderTest();
	t.true(someOtherCondition);
});

If too many variables need changing for each test, consider omitting the beforeEach() hook and performing setup steps within the tests themselves.

js
test('first scenario', t => {
	setupConditionA(t);
	setupConditionB(t, {/* options */});
	setupConditionC(t);
	const someCondition = t.context.thingUnderTest();
	t.true(someCondition);
});

// In this test, setupConditionB() is never called.
test('second scenario', t => {
	setupConditionA(t);
	setupConditionC(t);
	const someOtherCondition = t.context.thingUnderTest();
	t.true(someOtherCondition);
});

You can use t.teardown() to register a teardown function which will run after the test has finished (regardless of whether it's passed or failed).

A practical example

js
test.beforeEach(t => {
	t.context = {
		authenticator: new Authenticator(),
		credentials: new Credentials('admin', 's3cr3t')
	};
});

test('authenticating with valid credentials', async t => {
	const isValid = t.context.authenticator.authenticate(t.context.credentials);
	t.true(await isValid);
});

test('authenticating with an invalid username', async t => {
	t.context.credentials.username = 'bad_username';
	const isValid = t.context.authenticator.authenticate(t.context.credentials);
	t.false(await isValid);
});

test('authenticating with an invalid password', async t => {
	t.context.credentials.password = 'bad_password';
	const isValid = t.context.authenticator.authenticate(t.context.credentials);
	t.false(await isValid);
});

The same tests, now using setup functions, would look like the following.

js
function setup({username = 'admin', password = 's3cr3t'} = {}) {
	return {
		authenticator: new Authenticator(),
		credentials: new Credentials(username, password)
	};
}

test('authenticating with valid credentials', async t => {
	const {authenticator, credentials} = setup();
	const isValid = authenticator.authenticate(credentials);
	t.true(await isValid);
});

test('authenticating with an invalid username', async t => {
	const {authenticator, credentials} = setup({username: 'bad_username'});
	const isValid = authenticator.authenticate(credentials);
	t.false(await isValid);
});

test('authenticating with an invalid password', async t => {
	const {authenticator, credentials} = setup({password: 'bad_password'});
	const isValid = authenticator.authenticate(credentials);
	t.false(await isValid);
});

Combining hooks and setup functions

Of course beforeEach() and plain setup functions can be used together:

js
test.beforeEach(t => {
	t.context = setupAllTests();
});

test('first scenario', t => {
	firstSetup(t);
	const someCondition = t.context.thingUnderTest();
	t.true(someCondition);
});

Recipes/Testing With Selenium Webdriverjs

Setting up AVA with Selenium WebDriverJS

This recipe shows you how to use the Selenium WebDriverJS (official JavaScript implementation) with AVA to test web apps.

Setup

This recipe uses the following packages:

  1. selenium-webdriver
  2. chromedriver

Install them with:

console
$ npm install selenium-webdriver chromedriver

As part of this recipe, we will use Selenium to verify web searches on Bing and Google.

Create the following files:

  • ./test/bingtest.js
  • ./test/googletest.js

In both files, let's first include the packages:

js
import test from 'ava';
import {Builder, By, Key, until} from 'selenium-webdriver';
import 'chromedriver';

In the bingtest.js file, add the following code, which tests whether searching for webdriver on Bing, returns results.

js
test('Bing Search', async t => {
	const keyword = 'webdriver';
	const driver = new Builder().forBrowser('chrome').build();
	await driver.get('https://www.bing.com');
	await driver.findElement(By.name('q')).sendKeys(keyword + Key.ENTER);
	await driver.wait(until.titleIs(keyword + ' - Bing'));
	t.true((await driver.findElements(By.css('#b_content #b_results li'))).length > 0);
	await driver.close();
});

In the googletest.js file, instead of a single test, lets add two tests, one each for the terms 'webdriver' and 'avajs'.

Since we would like to initialize the webdriver before each test, we use the beforeEach and afterEach hooks to setup and teardown the driver respectively. Using these hooks, helps reduce the amount of code we would write in each test().

js
test.beforeEach(async t => {
	t.context.driver = new Builder().forBrowser('chrome').build();
	await t.context.driver.get('https://www.google.com');
});

test.afterEach('cleanup', async t => {
	await t.context.driver.close();
});

Now lets add the test code:

js
async function searchGoogle(driver, keyword) {
	await driver.findElement(By.name('q')).sendKeys(keyword + Key.ENTER);
	await driver.wait(until.titleIs(`${keyword} - Google Search`));
}

test('Google Search for avajs', async t => {
	const {driver} = t.context;
	await searchGoogle(driver, 'avajs');
	t.true((await driver.findElement(By.id('resultStats')).getText()).includes('results'));
});

test('Google Search for webdriver', async t => {
	const {driver} = t.context;
	await searchGoogle(driver, 'webdriver');
	t.true((await driver.findElement(By.id('resultStats')).getText()).includes('results'));
});

You're now ready to run the tests. The output should look something like this:

console
npx ava
DevTools listening on ws://127.0.0.1:49720/devtools/browser/9ebf4394-447b-4916-91cc-692d06d88896

DevTools listening on ws://127.0.0.1:49756/devtools/browser/6e19d9fe-4de6-40a3-b120-17067b3125ca

DevTools listening on ws://127.0.0.1:49757/devtools/browser/ac12c2da-eeed-40d8-9b23-4d2103ec8fac

  bingtest » Bing Search (7.2s)
  googletest » Google Search for avajs (2.5s)
  googletest » Google Search for webdriver (3.3s)

  3 tests passed

Recipes/Typescript

TypeScript

Translations: Español, Français, Italiano, Русский, 简体中文

AVA comes bundled with a TypeScript definition file. This allows developers to leverage TypeScript for writing tests.

This guide assumes you've already set up TypeScript for your project. Note that AVA's definition expects at least version 5.9.

Enabling AVA's support for TypeScript test files

Broadly speaking, there are two ways to run tests written in TypeScript:

  1. Build first, then test against the build output
  2. Configure loaders which build test files as they're loaded

The first option is the most reliable since it doesn't rely on experimental Node.js features. You can use our @ava/typescript package, which is designed to work for projects that precompile TypeScript using the tsc command. Please see @ava/typescript for setup instructions. This package also sets up the various TypeScript file extensions for you.

You can use loaders, but you're largely on your own. Please post questions to our Discussions forum if you're stuck.

[!NOTE]
Custom loaders changed with the release of Node.js 20. This recipe assumes your Node.js version is equal to or higher than the following. For older versions, please see a previous commit.

Node.js Major Version Minimum Version
18 18.18.0
20 20.8.0
21 21.0.0

There are two components to a setup like this:

  1. Make sure AVA recognizes the extensions of your TypeScript files
  2. Install the loader through nodeArguments

tsimp may be the best loader available. The setup, assuming your TypeScript config outputs ES modules, would look like this:

package.json:

json
"ava": {
	"extensions": ["ts"],
	"nodeArguments": [
		"--import=tsimp"
	]
}

Writing tests

Create a test.ts file using ESM syntax.

ts
import test from 'ava';

const fn = () => 'foo';

test('fn() returns foo', t => {
	t.is(fn(), 'foo');
});

Using [macros](../01-writing-tests.md#reusing-test-logic-through-macros)

Macros can receive additional arguments. AVA can infer these to ensure you're using the macro correctly:

ts
import test, {ExecutionContext} from 'ava';

const hasLength = (t: ExecutionContext, input: string, expected: number) => {
	t.is(input.length, expected);
};

test('bar has length 3', hasLength, 'bar', 3);

However if you use the test.macro() helper you get much better type inference:

ts
import test from 'ava';

const macro = test.macro((t, input: string, expected: number) => {
	t.is(eval(input), expected);
});

test('title', macro, '3 * 3', 9);

Or with a title function:

ts
import test from 'ava';

const macro = test.macro({
	exec(t, input: string, expected: number) {
		t.is(eval(input), expected);
	},
	title(providedTitle = '', input, expected) {
		return `${providedTitle} ${input} = ${expected}`.trim();
	}
});

test(macro, '2 + 2', 4);
test(macro, '2 * 3', 6);
test('providedTitle', macro, '3 * 3', 9);

Typing [`t.context`](../01-writing-tests.md#test-context)

By default, the type of t.context will be the empty object ({}). AVA exposes an interface TestFn<Context> which you can use to apply your own type to t.context. This can help you catch errors at compile-time:

ts
import anyTest, {TestFn} from 'ava';

const test = anyTest as TestFn<{foo: string}>;

test.beforeEach(t => {
	t.context = {foo: 'bar'};
});

test.beforeEach(t => {
	t.context.foo = 123; // error:  Type '123' is not assignable to type 'string'
});

test.serial.failing('very long chains are properly typed', t => {
	t.context.fooo = 'a value'; // error: Property 'fooo' does not exist on type ''
});

test('an actual test', t => {
	t.deepEqual(t.context.foo.map(c => c), ['b', 'a', 'r']); // error: Property 'map' does not exist on type 'string'
});

Note that, despite the type cast above, when executing t.context is an empty object unless it's assigned.

Typing `throws` assertions

The t.throws() and t.throwsAsync() assertions are typed to always return an Error. You can customize the error class using generics:

ts
import test from 'ava';

class CustomError extends Error {
	parent: Error

	constructor(parent) {
		super(parent.message);
		this.parent = parent;
	}
}

function myFunc() {
	throw new CustomError(new TypeError('🙈'));
};

test('throws', t => {
	const err = t.throws<CustomError>(myFunc);
	t.is(err.parent.name, 'TypeError');
});

test('throwsAsync', async t => {
	const err = await t.throwsAsync<CustomError>(async () => myFunc());
	t.is(err.parent.name, 'TypeError');
});

Recipes/Vue

Testing Vue.js components

Translations: Français

Dependencies

Status

This recipe relied on legacy require() hooks. AVA is ESM-only, so this setup is no longer supported.

Precompile your Vue components before running AVA, or use tooling that provides an ESM loader for .vue files.

Coverage reporting

Follow the coverage reporting recipe, additionally adding the .vue extension to the c8 config to instrument .vue files.

json
{
	"c8": {
		"extension": [
			".js",
			".vue"
		]
	}
}

Recipes/Watch Mode

Watch mode

Translations: Français, Italiano, Русский, 简体中文

AVA comes with an intelligent watch mode. It watches for files to change and runs just those tests that are affected.

Running tests with watch mode enabled

You can enable watch mode using the --watch or -w flags:

console
$ npx ava --watch

Please note that integrated debugging and the TAP reporter are unavailable when using watch mode.

Requirements

AVA uses fs.watch(). Support for recursive mode is required. Note that this has only become available on Linux since Node.js 20. Other caveats apply, for example this won't work well on network filesystems and Docker host mounts.

Ignoring changes

By default AVA watches for changes to all files, except for those with .snap.md or .tsbuildinfo extensions, ava.config.* and files in certain directories as provided by the ignore-by-default package.

You can configure additional patterns for files to ignore in the ava section of your package.json, or ava.config.* file, using the ignoreChanges key within the watchMode object:

js
export default {
	watchMode: {
		ignoreChanges: ['coverage'],
	},
};

If your tests write to disk they may trigger the watcher to rerun your tests. Configuring additional ignore patterns helps avoid this.

Filter tests while watching

You may also filter tests while watching by using the CLI. For example, after running

console
npx ava --watch

You will see a prompt like this:

console
Type `g` followed by enter to filter test files by a glob pattern
 Type `m` followed by enter to filter tests by their title
 Type `r` followed by enter to rerun tests
 Type `u` followed by enter to update snapshots in selected tests
>

So, to run only tests numbered like

  • foo23434
  • foo4343
  • foo93823

You can type m and press enter, then type foo* and press enter. This will then run all tests that match that glob.

Afterwards you can use the r command to run the matched tests again, or a command to run all tests.

Dependency tracking

AVA tracks which source files your test files depend on. If you change such a dependency only the test file that depends on it will be rerun. AVA will rerun all tests if it cannot determine which test file depends on the changed source file.

Dependency tracking works for static import syntax, as supported by @vercel/nft. import() is supported but dynamic paths such as import(myVariable) are not.

Files accessed using the fs module are not tracked.

Watch mode and CI

If you run AVA in your CI with watch mode, the execution will exit with an error (Error : Watch mode is not available in CI, as it prevents AVA from terminating.). AVA will not run with the --watch (-w) option in CI, because CI processes should terminate, and with the --watch option, AVA will never terminate.

Manually rerunning all tests

You can quickly rerun all tests by typing r on the console, followed by Enter.

Updating snapshots

You can update failing snapshots by typing u on the console, followed by Enter.

Debugging

Sometimes watch mode does something surprising like rerunning all tests when you thought only a single test would be run. To see its reasoning you can enable a debug mode. This will work best with the verbose reporter:

console
$ DEBUG=ava:watcher npx ava --watch

Recipes/When To Use Plan

When to use `t.plan()`

Translations: Español, Français, Italiano, 日本語, Português, Русский, 简体中文

One major difference between AVA and tap/tape is the behavior of t.plan(). In AVA, t.plan() is only used to assert that the expected number of assertions are called; it does not auto-end the test.

Poor uses of `t.plan()`

Many users transitioning from tap/tape are accustomed to using t.plan() prolifically in every test. However, in AVA, we don't consider that to be a "best practice". Instead, we believe t.plan() should only be used in situations where it provides some value.

Sync tests with no branching

t.plan() is unnecessary in most sync tests.

js
test('simple sums', t => {
	// BAD: there is no branching here - t.plan() is pointless
	t.plan(2);

	t.is(1 + 1, 2);
	t.is(2 + 2, 4);
});

t.plan() does not provide any value here, and creates an extra chore if you ever decide to add or remove assertions.

Promises that are expected to resolve

js
test('gives foo', t => {
	t.plan(1);

	return somePromise().then(result => {
		t.is(result, 'foo');
	});
});

At a glance, this tests appears to make good use of t.plan() since an async promise handler is involved. However there are several problems with the test:

  1. t.plan() is presumably used here to protect against the possibility that somePromise() might be rejected; But returning a rejected promise would fail the test anyways.

  2. It would be better to take advantage of async/await:

js
test('gives foo', async t => {
	t.is(await somePromise(), 'foo');
});

Promises with a `.catch()` block

js
test('rejects with foo', t => {
	t.plan(2);

	return shouldRejectWithFoo().catch(reason => {
		t.is(reason.message, 'Hello');
		t.is(reason.foo, 'bar');
	});
});

Here, the use of t.plan() seeks to ensure that the code inside the catch block is executed.
Instead, you should take advantage of t.throwsAsync and async/await, as this leads to flatter code that is easier to reason about:

js
test('rejects with foo', async t => {
	const reason = await t.throwsAsync(shouldRejectWithFoo());
	t.is(reason.message, 'Hello');
	t.is(reason.foo, 'bar');
});

Ensuring a catch statement happens

js
test('throws', t => {
	t.plan(2);

	try {
		shouldThrow();
	} catch (err) {
		t.is(err.message, 'Hello');
		t.is(err.foo, 'bar');
	}
});

As stated in the previous example, using the t.throws() assertion with async/await is a better choice.

Good uses of `t.plan()`

t.plan() provides value in the following cases.

Tests with branching statements

In most cases, it's a bad idea to use any complex branching inside your tests. A notable exception is for tests that are auto-generated (perhaps from a JSON document). Below t.plan() is used to ensure the correctness of the JSON input:

js
import fs from 'node:fs';
import path from 'node:path';

const testData = JSON.parse(fs.readFileSync(new URL('./fixtures/test-definitions.json', import.meta.url)));

for (const testDefinition of testData) {
	test('foo or bar', t => {
		const result = functionUnderTest(testDefinition.input);

		// testDefinition should have an expectation for `foo` or `bar` but not both
		t.plan(1);

		if (testDefinition.foo) {
			t.is(result.foo, testDefinition.foo);
		}

		if (testDefinition.bar) {
			t.is(result.bar, testDefinition.foo);
		}
	});
}

Conclusion

t.plan() has plenty of valid uses, but it should not be used indiscriminately. A good rule of thumb is to use it any time your test does not have straightforward, easily reasoned about, code flow. Tests with assertions inside callbacks, if/then statements, for/while loops, and (in some cases) try/catch blocks, are all good candidates for t.plan().


01 Writing Tests

Writing tests

Translations: Français

Tests are run concurrently. You can specify synchronous and asynchronous tests. Tests are considered synchronous unless you return a promise or an observable.

You must define all tests synchronously. They can't be defined inside setTimeout, setImmediate, etc.

AVA tries to run test files with their current working directory set to the directory that contains your package.json file.

Test isolation

By default each test file is run in a new worker thread. You can fall back running in separate processes (see workerThreads CLI option).

AVA will set process.env.NODE_ENV to test, unless the NODE_ENV environment variable has been set. This is useful if the code you're testing has test defaults (for example when picking what database to connect to). It may cause your code or its dependencies to behave differently though. Note that 'NODE_ENV' in process.env will always be true.

Declaring tests

To declare a test you call the test function you imported from AVA. Provide the required title and implementation function. Titles must be unique within each test file. The function will be called when your test is run. It's passed an execution object as its first argument.

js
import test from 'ava';

test('my passing test', t => {
	t.pass();
});

Running tests serially

Tests are run concurrently by default, however, sometimes you have to write tests that cannot run concurrently. In these rare cases you can use the .serial modifier. It will force those tests to run serially before the concurrent ones.

js
test.serial('passes serially', t => {
	t.pass();
});

Note that this only applies to tests within a particular test file. AVA will still run multiple tests files at the same time unless you pass the --serial CLI flag.

You can use the .serial modifier with all tests, hooks and even .todo(), but it's only available on the test function.

Promise support

Tests may return a promise. AVA will wait for the promise to resolve before ending the test. If the promise rejects the test will fail.

js
test('resolves with unicorn', t => {
	return somePromise().then(result => {
		t.is(result, 'unicorn');
	});
});

Async function support

AVA comes with built-in support for async functions.

js
test(async function (t) {
	const value = await promiseFn();
	t.true(value);
});

// Async arrow function
test('promises the truth', async t => {
	const value = await promiseFn();
	t.true(value);
});

Observable support

AVA comes with built-in support for observables. If you return an observable from a test, AVA will automatically consume it to completion before ending the test.

js
test('handles observables', t => {
	t.plan(3);
	return Observable.of(1, 2, 3, 4, 5, 6)
		.filter(n => {
			// Only even numbers
			return n % 2 === 0;
		})
		.map(() => t.pass());
});

Running specific tests

During development it can be helpful to only run a few specific tests. This can be accomplished using the .only modifier:

js
test('will not be run', t => {
	t.fail();
});

test.only('will be run', t => {
	t.pass();
});

You can use the .only modifier with all tests. It cannot be used with hooks or .todo().

Note: The .only modifier applies to the test file it's defined in, so if you run multiple test files, tests in other files will still run. If you want to only run the test.only test, provide just that test file to AVA.

Skipping tests

Sometimes failing tests can be hard to fix. You can tell AVA to temporarily skip these tests using the .skip modifier. They'll still be shown in the output (as having been skipped) but are never run.

js
test.skip('will not be run', t => {
	t.fail();
});

You must specify the implementation function. You can use the .skip modifier with all tests and hooks, but not with .todo(). You can not apply further modifiers to .skip.

If the test is likely to be failing for a while, use .failing() instead.

Skipping tests conditionally

Use test.skipIf() to skip a test based on a runtime condition. test.runIf() is the inverse: the test only runs when the condition is true.

js
test.skipIf(process.platform === 'win32')('not on Windows', t => {
	t.pass();
});

test.runIf(process.platform === 'linux')('Linux only', t => {
	t.pass();
});

These work with other modifiers like .serial and .failing:

js
test.serial.skipIf(process.platform === 'win32')('serial, not on Windows', t => {
	t.pass();
});

test.failing.skipIf(process.platform === 'win32')('expected failure, not on Windows', t => {
	t.fail();
});

Test placeholders ("todo")

You can use the .todo modifier when you're planning to write a test. Like skipped tests these placeholders are shown in the output. They only require a title; you cannot specify the implementation function.

js
test.todo('will think about writing this later');

You can signal that you need to write a serial test:

js
test.serial.todo('will think about writing this later');

Failing tests

You can use the .failing modifier to document issues with your code that need to be fixed. Failing tests are run just like normal ones, but they are expected to fail, and will not break your build when they do. If a test marked as failing actually passes, it will be reported as an error and fail the build with a helpful message instructing you to remove the .failing modifier.

This allows you to merge .failing tests before a fix is implemented without breaking CI. This is a great way to recognize good bug report PR's with a commit credit, even if the reporter is unable to actually fix the problem.

js
// See: github.com/user/repo/issues/1234
test.failing('demonstrate some bug', t => {
	t.fail(); // Test will count as passed
});

Before & after hooks

AVA lets you register hooks that are run before and after your tests. This allows you to run setup and/or teardown code.

test.before() registers a hook to be run before the first test in your test file. Similarly test.after() registers a hook to be run after the last test. Use test.after.always() to register a hook that will always run once your tests and other hooks complete. .always() hooks run regardless of whether there were earlier failures, so they are ideal for cleanup tasks. Note however that uncaught exceptions, unhandled rejections or timeouts will crash your tests, possibly preventing .always() hooks from running.

test.beforeEach() registers a hook to be run before each test in your test file. Similarly test.afterEach() registers a hook to be run after each test. Use test.afterEach.always() to register an after hook that is called even if other test hooks, or the test itself, fail.

If a test is skipped with the .skip modifier, the respective .beforeEach(), .afterEach() and .afterEach.always() hooks are not run. Likewise, if all tests in a test file are skipped .before(), .after() and .after.always() hooks for the file are not run.

You may not need to use .afterEach.always() hooks to clean up after a test. You can use t.teardown() to undo side-effects within a particular test. Or use registerCompletionHandler() to run cleanup code after AVA has completed its work.

Like test() these methods take an optional title and an implementation function. The title is shown if your hook fails to execute. The implementation is called with an execution object. You can use assertions in your hooks. You can also pass a macro function and additional arguments.

.before() hooks execute before .beforeEach() hooks. .afterEach() hooks execute before .after() hooks. Within their category the hooks execute in the order they were defined. By default hooks execute concurrently, but you can use test.serial to ensure only that single hook is run at a time. Unlike with tests, serial hooks are not run before other hooks:

js
test.before(t => {
	// This runs before all tests
});

test.before(t => {
	// This runs concurrently with the above
});

test.serial.before(t => {
	// This runs after the above
});

test.serial.before(t => {
	// This too runs after the above, and before tests
});

test.after('cleanup', t => {
	// This runs after all tests
});

test.after.always('guaranteed cleanup', t => {
	// This will always run, regardless of earlier failures
});

test.beforeEach(t => {
	// This runs before each test
});

test.afterEach(t => {
	// This runs after each test
});

test.afterEach.always(t => {
	// This runs after each test and other test hooks, even if they failed
});

test('title', t => {
	// Regular test
});

Hooks can be synchronous or asynchronous, just like tests. To make a hook asynchronous return a promise or observable, or use an async function.

js
test.before(async t => {
	await promiseFn();
});

test.after(t => {
	return new Promise(/* ... */);
});

Keep in mind that the .beforeEach() and .afterEach() hooks run just before and after a test is run, and that by default tests run concurrently. This means each multiple .beforeEach() hooks may run concurrently. Using test.serial.beforeEach() does not change this. If you need to set up global state for each test (like spying on console.log for example), you'll need to make sure the tests themselves are run serially.

Remember that AVA runs each test file in its own process. You may not have to clean up global state in a .after()-hook since that's only called right before the process exits.

Test context

Hooks can share context with the test:

js
test.beforeEach(t => {
	t.context.data = generateUniqueData();
});

test('context data is foo', t => {
	t.is(t.context.data + 'bar', 'foobar');
});

If .before() hooks treat t.context as an object, a shallow copy is made and passed to .beforeEach() hooks and / or tests. Other types of values are passed as-is. The .after() and .after.always() hooks receive the original context value.

For .beforeEach(), .afterEach() and .afterEach.always() hooks the context is not shared between different tests, allowing you to set up data such that it will not leak to other tests.

By default t.context is an object but you can reassign it:

js
test.before(t => {
	t.context = 'unicorn';
});

test('context is unicorn', t => {
	t.is(t.context, 'unicorn');
});

Retrieving test metadata

Access data about the currently loaded test file run by reading test.meta.

Available properties:

  • file: path to the test file, as a file URL string
  • snapshotDirectory: directory where snapshots are stored, as a file URL string
js
import test from 'ava';

console.log('Test file currently being run:', test.meta.file);

Reusing test logic through macros

Additional arguments passed to the test declaration will be passed to the test implementation. This is useful for creating reusable test macros.

You could use plain functions:

js
function macro(t, input, expected) {
	t.is(eval(input), expected);
}

test('2 + 2 = 4', macro, '2 + 2', 4);
test('2 * 3 = 6', macro, '2 * 3', 6);

However the preferred approach is to use the test.macro() helper:

js
import test from 'ava';

const macro = test.macro((t, input, expected) => {
	t.is(eval(input), expected);
});

test('title', macro, '3 * 3', 9);

Or with a title function:

js
import test from 'ava';

const macro = test.macro({
	exec(t, input, expected) {
		t.is(eval(input), expected);
	},
	title(providedTitle = '', input, expected) {
		return `${providedTitle} ${input} = ${expected}`.trim();
	},
});

test(macro, '2 + 2', 4);
test(macro, '2 * 3', 6);
test('providedTitle', macro, '3 * 3', 9);

The providedTitle argument defaults to undefined if the user does not supply a string title. This means you can use a parameter assignment to set the default value. The example above uses the empty string as the default.


02 Execution Context

Execution Context (`t` argument)

Translations: Français

Each test or hook is called with an execution context. By convention it's named t.

js
import test from 'ava';

test('my passing test', t => {
	t.pass();
});

Each test or hook receives a different object. It contains the assertions as well as the methods and properties listed below.

`t.title`

The test title.

`t.context`

Contains shared state from hooks.

`t.passed`

When used in test.afterEach() or test.afterEach.always() hooks this tells you whether the test has passed. When used in a test itself (including teardown functions) this remains true until an assertion fails, the test has ended with an error, or a teardown function caused an error. This value has no meaning in other hooks.

`t.log(...values)`

Log values contextually alongside the test result instead of immediately printing them to stdout. Behaves somewhat like console.log, but without support for placeholder tokens.

`t.plan(count)`

Plan how many assertions there are in the test. The test will fail if the actual assertion count doesn't match the number of planned assertions. See assertion planning.

`t.teardown(fn)`

Registers the fn function to be run after the test has finished. You can register multiple functions. They'll run in reverse order, so the last registered function is run first. You can use asynchronous functions: only one will run at a time.

You cannot perform assertions using the t object or register additional functions from inside fn.

You cannot use t.teardown() in hooks either.

`t.timeout(ms)`

Set a timeout for the test, in milliseconds. The test will fail if this timeout is exceeded. The timeout is reset each time an assertion is made.

Use t.timeout.clear() to clear the timeout and restore the default behavior.


03 Assertions

Assertions

Translations: Français

Assertions are mixed into the execution object provided to each test implementation:

js
test('unicorns are truthy', t => {
	t.truthy('unicorn'); // Assertion
});

Assertions are bound to their test so you can assign them to a variable or pass them around:

js
test('unicorns are truthy', t => {
	const truthy = t.truthy;
	truthy('unicorn');
});

If multiple assertion failures are encountered within a single test, AVA will only display the first one.

Assertions return true if they've passed and throw otherwise. Catching this error does not cause the test to pass. The error value is undocumented.

If you use TypeScript you can use some assertions as type guards.

Note that the "throws" assertions return the error that was thrown (provided the assertion passed).

Assertion planning

Assertion plans ensure tests only pass when a specific number of assertions have been executed. They'll help you catch cases where tests exit too early. They'll also cause tests to fail if too many assertions are executed, which can be useful if you have assertions inside callbacks or loops.

If you do not specify an assertion plan, your test will still fail if no assertions are executed. Set the failWithoutAssertions option to false in AVA's package.json configuration to disable this behavior.

Note that, unlike tap and tape, AVA does not automatically end a test when the planned assertion count is reached.

These examples will result in a passed test:

js
test('resolves with 3', t => {
	t.plan(1);

	return Promise.resolve(3).then(n => {
		t.is(n, 3);
	});
});

These won't:

js
test('loops twice', t => {
	t.plan(2);

	for (let i = 0; i < 3; i++) {
		t.true(i < 3);
	}
}); // Fails, 3 assertions are executed which is too many

test('invokes callback synchronously', t => {
	t.plan(1);

	someAsyncFunction(() => {
		t.pass();
	});
}); // Fails, the test ends synchronously before the assertion is executed

Skipping assertions

Any assertion can be skipped using the skip modifier. Skipped assertions are still counted, so there is no need to change your planned assertion count.

js
test('skip assertion', t => {
	t.plan(2);
	t.is.skip(foo(), 5); // No need to change your plan count when skipping
	t.is(1, 1);
});

Custom assertions

You can use any assertion library instead of or in addition to the built-in one, provided it throws exceptions when the assertion fails.

This won't give you as nice an experience as you'd get with the built-in assertions though, and you won't be able to use the assertion planning (see #25).

You'll have to configure AVA to not fail tests if no assertions are executed, because AVA can't tell if custom assertions pass. Set the failWithoutAssertions option to false in AVA's package.json configuration.

js
import assert from 'assert';

test('custom assertion', t => {
	assert(true);
});

Built-in assertions

`.pass(message?)`

Passing assertion.

`.fail(message?)`

Failing assertion.

`.assert(actual, message?)`

Asserts that actual is truthy.

`.truthy(actual, message?)`

Assert that actual is truthy.

`.falsy(actual, message?)`

Assert that actual is falsy.

`.true(actual, message?)`

Assert that actual is true.

`.false(actual, message?)`

Assert that actual is false.

`.is(actual, expected, message?)`

Assert that actual is the same as expected. This is based on Object.is().

`.not(actual, expected, message?)`

Assert that actual is not the same as expected. This is based on Object.is().

`.deepEqual(actual, expected, message?)`

Assert that actual is deeply equal to expected. See Concordance for details.

`.notDeepEqual(actual, expected, message?)`

Assert that actual is not deeply equal to expected. The inverse of .deepEqual().

`.like(actual, selector, message?)`

Assert that actual is like selector. This is a variant of .deepEqual(), however selector does not need to have the same enumerable properties as actual does.

Instead AVA derives a comparable value from actual, recursively based on the enumerable shape of selector. This value is then compared to selector using .deepEqual().

Any values in selector that are not arrays or regular objects should be deeply equal to the corresponding values in actual.

In the following example, the map property of actual must be deeply equal to that of selector. However nested.qux is ignored, because it's not in selector.

js
t.like({
	map: new Map([['foo', 'bar']]),
	nested: {
		baz: 'thud',
		qux: 'quux'
	}
}, {
	map: new Map([['foo', 'bar']]),
	nested: {
		baz: 'thud',
	}
})

You can also use arrays, but note that any indices in actual that are not in selector are ignored:

js
t.like([1, 2, 3, 4], [1, , 3])

`.throws(fn, expectation?, message?)`

Assert that an error is thrown. fn must be a function which should throw. By default, the thrown value must be an error. It is returned so you can run more assertions against it.
expectation can be an object with one or more of the following properties:

  • any: a boolean, if true then the thrown value does not need to be an error. Defaults to false
  • instanceOf: a constructor, the thrown error must be an instance of
  • is: the thrown error must be strictly equal to expectation.is
  • message: the following types are valid:
    • string - it is compared against the thrown error's message
    • regular expression - it is matched against this message
    • function - it is passed the thrown error message and must return a boolean for whether the assertion passed
  • name: the expected .name value of the thrown error
  • code: the expected .code value of the thrown error

expectation does not need to be specified. If you don't need it but do want to set an assertion message you have to specify undefined.

Example:

js
const fn = () => {
	throw new TypeError('🦄');
};

test('throws', t => {
	const error = t.throws(() => {
		fn();
	}, {instanceOf: TypeError});

	t.is(error.message, '🦄');
});

`.throwsAsync(thrower, expectation?, message?)`

Assert that an error is thrown. thrower can be an async function which should throw, or a promise that should reject. This assertion must be awaited.

By default, the thrown value must be an error. It is returned so you can run more assertions against it.
expectation can be an object with one or more of the following properties:

  • any: a boolean, if true then the thrown value does not need to be an error. Defaults to false
  • instanceOf: a constructor, the thrown error must be an instance of
  • is: the thrown error must be strictly equal to expectation.is
  • message: the following types are valid:
    • string - it is compared against the thrown error's message
    • regular expression - it is matched against this message
    • function - it is passed the thrown error message and must return a boolean for whether the assertion passed
  • name: the expected .name value of the thrown error
  • code: the expected .code value of the thrown error

expectation does not need to be specified. If you don't need it but do want to set an assertion message you have to specify undefined.

Example:

js
test('throws', async t => {
	await t.throwsAsync(async () => {
		throw new TypeError('🦄');
	}, {instanceOf: TypeError, message: '🦄'});
});
js
const promise = Promise.reject(new TypeError('🦄'));

test('rejects', async t => {
	const error = await t.throwsAsync(promise);
	t.is(error.message, '🦄');
});

`.notThrows(fn, message?)`

Assert that no error is thrown. fn must be a function which shouldn't throw.

`.notThrowsAsync(nonThrower, message?)`

Assert that no error is thrown. nonThrower can be an async function which shouldn't throw, or a promise that should resolve.

Like the .throwsAsync() assertion, you must wait for the assertion to complete:

js
test('resolves', async t => {
	await t.notThrowsAsync(promise);
});

`.regex(contents, regex, message?)`

Assert that contents matches regex.

`.notRegex(contents, regex, message?)`

Assert that contents does not match regex.

`.snapshot(expected, message?)`

Compares the expected value with a previously recorded snapshot. Snapshots are stored for each test, so ensure you give your tests unique titles.

`.try(title?, implementation | macro, ...args?)`

.try() allows you to try assertions without causing the test to fail.

The implementation function behaves the same as any other test function. You can even use macros. The first title argument is always optional. Additional arguments are passed to the implementation or macro function.

.try() is an asynchronous function. You must await it. The result object has commit() and discard() methods. You must decide whether to commit or discard the result. If you commit a failed result, your test will fail. Calling commit() on a failed result will throw an error.

You can check whether the attempt passed using the passed property. Any assertion errors are available through the errors property. The attempt title is available through the title property.

Logs from t.log() are available through the logs property. You can choose to retain these logs as part of your test by passing {retainLogs: true} to the commit() and discard() methods.

The implementation function receives its own execution context, just like a test function. You must be careful to only perform assertions using the attempt's execution context. At least one assertion must pass for your attempt to pass.

You may run multiple attempts concurrently, within a single test. However you can't use snapshots when you do so.

Example:

js
const twoRandomIntegers = () => {
	const rnd = Math.round(Math.random() * 100);
	const x = rnd % 10;
	const y = Math.floor(rnd / 10);
	return [x, y];
};

test('flaky macro', async t => {
	const firstTry = await t.try((tt, a, b) => {
		tt.is(a, b);
	}, ...twoRandomIntegers());

	if (firstTry.passed) {
		firstTry.commit();
		return;
	}

	firstTry.discard();
	t.log(firstTry.errors);

	const secondTry = await t.try((tt, a, b) => {
		tt.is(a, b);
	}, ...twoRandomIntegers());
	secondTry.commit();
});

04 Snapshot Testing

Snapshot testing

Translations: Français

AVA supports snapshot testing, as introduced by Jest, through its Assertions interface. You can snapshot any value.

Snapshots are stored alongside your test files. If your tests are in a test or tests folder the snapshots will be stored in a snapshots folder. If your tests are in a __tests__ folder then they they'll be stored in a __snapshots__ folder.

Say you have ~/project/test/main.js which contains snapshot assertions. AVA will create two files:

  • ~/project/test/snapshots/main.js.snap
  • ~/project/test/snapshots/main.js.md

The first file contains the actual snapshot and is required for future comparisons. The second file contains your snapshot report. It's regenerated when you update your snapshots. If you commit it to source control you can diff it to see the changes to your snapshot.

AVA will show why your snapshot assertion failed:

You can then check your code. If the change was intentional you can use the --update-snapshots (or -u) flag to update the snapshots:

console
$ ava --update-snapshots

If you need to update snapshots for only a particular test, you can use --update-snapshots together with e.g. --match or .only() to select the test.

You can specify a fixed location for storing the snapshot files in AVA's package.json configuration:

package.json:

json
{
	"ava": {
		"snapshotDir": "custom-directory"
	}
}

The snapshot files will be saved in a directory structure that mirrors that of your test files.

If you are running AVA against precompiled test files, AVA will try and use source maps to determine the location of the original files. Snapshots will be stored next to these files, following the same rules as if AVA had executed the original files directly. This is great if you're writing your tests in TypeScript (see our TypeScript recipe).


05 Command Line

CLI

Translations: Français

text
/* Detailed source-code truncated for AI context efficiency. */

AVA searches for test files using the following patterns:

  • test.js
  • src/test.js
  • source/test.js
  • **/test-*.js
  • **/*.spec.js
  • **/*.test.js
  • **/test/**/*.js
  • **/tests/**/*.js
  • **/__tests__/**/*.js

Files inside node_modules are always ignored. So are files starting with _ or inside of directories that start with a single _. Additionally, files matching these patterns are ignored by default, unless different patterns are configured:

  • **/__tests__/**/__helper__/**/*
  • **/__tests__/**/__helpers__/**/*
  • **/__tests__/**/__fixture__/**/*
  • **/__tests__/**/__fixtures__/**/*
  • **/test/**/helper/**/*
  • **/test/**/helpers/**/*
  • **/test/**/fixture/**/*
  • **/test/**/fixtures/**/*
  • **/tests/**/helper/**/*
  • **/tests/**/helpers/**/*
  • **/tests/**/fixture/**/*
  • **/tests/**/fixtures/**/*

When using npm test, you can pass positional arguments directly npm test test2.js, but flags needs to be passed like npm test -- --verbose.

Running tests with matching titles

The --match flag allows you to run just the tests that have a matching title. This is achieved with simple wildcard patterns. Patterns are case insensitive. See matcher for more details.

Match titles ending with foo:

console
npx ava --match='*foo'

Match titles starting with foo:

console
npx ava --match='foo*'

Match titles containing foo:

console
npx ava --match='*foo*'

Match titles that are exactly foo (albeit case insensitively):

console
npx ava --match='foo'

Match titles not containing foo:

console
npx ava --match='!*foo*'

Match titles starting with foo and ending with bar:

console
npx ava --match='foo*bar'

Match titles starting with foo or ending with bar:

console
npx ava --match='foo*' --match='*bar'

Note that a match pattern takes precedence over the .only modifier. Only tests with an explicit title are matched. Tests without titles or whose title is derived from the implementation function will be skipped when --match is used.

Here's what happens when you run AVA with a match pattern of *oo* and the following tests:

js
test('foo will run', t => {
	t.pass();
});

test('moo will also run', t => {
	t.pass();
});

test.only('boo will run but not exclusively', t => {
	t.pass();
});

Running tests at specific line numbers

AVA lets you run tests exclusively by referring to their line numbers. Target a single line, a range of lines or both. You can select any line number of a test.

The format is a comma-separated list of [X|Y-Z] where X, Y and Z are integers between 1 and the last line number of the file.

This feature is only available from the command line.

Running a single test

To only run a particular test in a file, append the line number of the test to the path or pattern passed to AVA.

Given the following test file:

test.js

js
1: test('unicorn', t => {
2:   t.pass();
3: });
4:
5: test('rainbow', t => {
6:  t.fail();
7: });

Running npx ava test.js:2 for would run the unicorn test. In fact you could use any line number between 1 and 3.

Running multiple tests

To run multiple tests, either target them one by one or select a range of line numbers. As line numbers are given per file, you can run multiple files with different line numbers for each file. If the same file is provided multiple times, line numbers are merged and only run once.

Examples

Single line numbers:

console
npx ava test.js:2,9

Range:

console
npx ava test.js:4-7

Mix of single line number and range:

console
npx ava test.js:4,9-12

Different files:

console
npx ava test.js:3 test2.js:4,7-9

When running a file with and without line numbers, line numbers take precedence.

Resetting AVA's cache

AVA maintains some temporary state. You can clear this state by running:

console
npx ava reset-cache

This deletes all files in the node_modules/.cache/ava directory.

Reporters

AVA uses a human readable reporter by default:

TAP reporter

AVA supports the TAP format and thus is compatible with any TAP reporter. Use the --tap flag to enable TAP output.

console
$ npx ava --tap | npx tap-nyan

Please note that the TAP reporter is unavailable when using watch mode.

Node arguments

The --node-arguments argument may be used to specify additional arguments for launching worker processes. These are combined with the nodeArguments configuration and any arguments passed to the node binary when starting AVA.

Only pass trusted values.

Specify the arguments as a single string:

console
npx ava --node-arguments="--throw-deprecation --zero-fill-buffers"

Only pass trusted values.


06 Configuration

Configuration

Translations: Français

All of the CLI options can be configured in the ava section of either your package.json file, or an ava.config.* file. This allows you to modify the default behavior of the ava command, so you don't have to repeatedly type the same options on the command prompt.

To ignore files, prefix the pattern with an ! (exclamation mark).

package.json:

json
{
	"ava": {
		"files": [
			"test/**/*",
			"!test/exclude-files-in-this-directory",
			"!**/exclude-files-with-this-name.*"
		],
		"match": [
			"*oo",
			"!foo"
		],
		"concurrency": 5,
		"failFast": true,
		"failWithoutAssertions": false,
		"environmentVariables": {
			"MY_ENVIRONMENT_VARIABLE": "some value"
		},
		"verbose": true,
		"require": [
			"./my-helper-module.js"
		],
		"nodeArguments": [
			"--trace-deprecation",
			"--napi-modules"
		]
	}
}

Arguments passed to the CLI will always take precedence over the CLI options configured in package.json.

Options

  • files: an array of glob patterns to select test files. Files with an underscore prefix are ignored. By default only selects files with mjs & js extensions, even if the pattern matches other files. Specify extensions to allow other file extensions
  • watchMode: See the watch mode recipe for details
  • match: not typically useful in the package.json configuration, but equivalent to specifying --match on the CLI
  • cache: defaults to true to cache compiled files under node_modules/.cache/ava. If false, files are cached in a temporary directory instead
  • concurrency: max number of test files running at the same time (default: CPU cores)
  • workerThreads: use worker threads to run tests (enabled by default). If false, tests will run in child processes
  • failFast: stop running further tests once a test fails
  • failWithoutAssertions: if false, does not fail a test if it doesn't run assertions
  • environmentVariables: specifies environment variables to be made available to the tests. The environment variables defined here override the ones from process.env
  • serial: if true, prevents parallel execution of tests within a file
  • tap: if true, enables the TAP reporter
  • verbose: if true, enables verbose output (though there currently non-verbose output is not supported)
  • snapshotDir: specifies a fixed location for storing snapshot files. Use this if your snapshots are ending up in the wrong location
  • extensions: extensions of test files. Setting this overrides the default ["mjs", "js"] value, so make sure to include those extensions in the list.
  • require: extra modules to load before test files
  • timeout: Timeouts in AVA behave differently than in other test frameworks. AVA resets a timer after each test, forcing tests to quit if no new test results were received within the specified timeout. This can be used to handle stalled tests. See our timeout documentation for more options.
  • nodeArguments: Configure Node.js arguments used to launch worker processes.
  • sortTestFiles: A comparator function to sort test files with. Available only when using a ava.config.* file. See an example use case here.
  • utilizeParallelBuilds: If false, disable parallel builds (default: true)

Note that providing files on the CLI overrides the files option.

Provide the typescript option (and install @ava/typescript as an additional dependency) for AVA to run tests written in TypeScript.

Using `ava.config.*` files

Rather than specifying the configuration in the package.json file you can use an ava.config.js file. If your project does not already treat .js files as ES modules, use ava.config.mjs instead.

To use these files:

  1. Your package.json must not contain an ava property (or, if it does, it must be an empty object)
  2. You must only have one ava.config.* file in any directory, so don't mix ava.config.js and ava.config.mjs files

AVA searches your file system for ava.config.* files. First, when you run AVA, it finds the closest package.json. Starting in that directory it recursively checks the parent directories until it either reaches the file system root or encounters a .git file or directory. The first ava.config.* file found is selected. This allows you to use a single configuration file in a monorepo setup.

`ava.config.js`

Prefer ava.config.js when your project already treats .js files as ES modules, for example through "type": "module" in package.json.

The default export can either be a plain object or a factory function which returns a plain object. You can export or return a promise for a plain object:

js
export default {
	require: ['./_my-test-helper.js']
};
js
export default function factory() {
	return {
		require: ['./_my-test-helper.js']
	};
};

The factory function is called with an object containing a projectDir property, which you could use to change the returned configuration:

js
export default ({projectDir}) => {
	if (projectDir === '/Users/username/projects/my-project') {
		return {
			// Config A
		};
	}

	return {
		// Config B
	};
};

`ava.config.mjs`

Use ava.config.mjs when you need the filename itself to opt into ESM.

The default export can either be a plain object or a factory function which returns a plain object. You can export or return a promise for a plain object:

js
export default {
	require: ['./_my-test-helper.js']
};
js
export default function factory() {
	return {
		require: ['./_my-test-helper.js']
	};
};

The factory function is called with an object containing a projectDir property, which you could use to change the returned configuration:

js
export default ({projectDir}) => {
	if (projectDir === '/Users/username/projects/my-project') {
		return {
			// Config A
		};
	}

	return {
		// Config B
	};
};

Alternative configuration files

The CLI lets you specify a specific configuration file, using the --config flag. Prefer a .js file. Use .mjs only when your project does not already treat .js files as ESM.

When the --config flag is set, the provided file will override all configuration from the package.json and ava.config.js or ava.config.mjs files. The configuration is not merged.

You can use this to customize configuration for a specific test run. For instance, you may want to run unit tests separately from integration tests:

ava.config.js:

js
export default {
	files: ['unit-tests/**/*']
};

integration-tests.config.js:

js
import baseConfig from './ava.config.js';

export default {
	...baseConfig,
	files: ['integration-tests/**/*']
};

You can now run your unit tests through npx ava and the integration tests through npx ava --config integration-tests.config.js.

Object printing depth

By default, AVA prints nested objects to a depth of 3. However, when debugging tests with deeply nested objects, it can be useful to print with more detail. This can be done by setting util.inspect.defaultOptions.depth to the desired depth, before the test is executed:

js
import util from 'util';

import test from 'ava';

util.inspect.defaultOptions.depth = 5;  // Increase AVA's printing depth

test('My test', t => {
	t.deepEqual(someDeeplyNestedObject, theExpectedValue);
});

AVA has a minimum depth of 3.

Experiments

From time to time, AVA will implement experimental features. These may change or be removed at any time, not just when there's a new major version. You can opt in to such a feature by enabling it in the nonSemVerExperiments configuration.

ava.config.js:

js
export default {
	nonSemVerExperiments: {
		feature: true
	}
};

Requiring extra modules

Use the require configuration to load extra modules before test files are loaded. Relative paths are resolved against the project directory and can be loaded through @ava/typescript. Otherwise, modules are loaded from within the node_modules directory inside the project.

You may specify a single value, or an array of values:

ava.config.js:

js
export default {
	require: './_my-test-helper.js'
}
js
export default {
	require: ['./_my-test-helper.js']
}

If the module exports a function, it is called and awaited:

_my-test-helper.js:

js
export default function () {
	// Additional setup
}

You can provide arguments:

ava.config.js:

js
export default {
	require: [
		['./_my-test-helper.js', 'my', 'arguments']
	]
}

_my-test-helper.js:

js
export default function (first, second) { // 'my', 'arguments'
	// Additional setup
}

Arguments are copied using the structured clone algorithm. This means Map values survive, but a Buffer will come out as a Uint8Array.

You can load dependencies installed in your project:

ava.config.js:

js
export default {
	require: '@babel/register'
}

These may also export a function which is then invoked, and can receive arguments.

Node arguments

The nodeArguments configuration may be used to specify additional arguments for launching worker processes. These are combined with --node-arguments passed on the CLI and any arguments passed to the node binary when starting AVA.

Node arguments filter for worker threads

In a config file only, filterNodeArgumentsForWorkerThreads may provide a function used for filtering nodeArguments sent to worker threads. This enables excluding arguments that throw if sent to a thread. The filter is ignored by worker processes.

ava.config.js:

js
const processOnly = new Set([
	'--allow-natives-syntax',
	'--expose-gc'
]);

export default {
	filterNodeArgumentsForWorkerThreads: argument => !processOnly.has(argument)
}

07 Test Timeouts

Test timeouts

Translations: Français

Timeouts in AVA behave differently than in other test frameworks. AVA resets a timer after each test, forcing tests to quit if no new test results were received within the specified timeout. This can be used to handle stalled tests. This same mechanism is used to determine when a test file is preventing a clean exit.

The default timeout is 10 seconds.

You can configure timeouts using the --timeout command line option, or in the configuration. They can be set in a human-readable way:

console
npx ava --timeout=10s # 10 seconds
npx ava --timeout=2m # 2 minutes
npx ava --timeout=100 # 100 milliseconds

`t.timeout(ms, message?)`

Timeouts can also be set individually for each test. These timeouts are reset each time an assertion is made. The test fails if it takes more than ms for an assertion to be made or the test to complete.

js
test('foo', t => {
	t.timeout(100); // 100 milliseconds
	// Write your assertions here
});

An optional message string can be provided. This can be useful if your test depends on some other setup that may not have been completed:

js
test('foo', t => {
	t.timeout(100, 'make sure database has started'); // 100 milliseconds
	// Write your assertions here
});

08 Common Pitfalls

Common Pitfalls

Translations: Français

If you use ESLint, you can install eslint-plugin-ava. It will help you use AVA correctly and avoid some common pitfalls.

Error edge cases

The throws() and throwsAsync() assertions use the Node.js built-in isNativeError() to determine whether something is an error. This only recognizes actual instances of Error (and subclasses).

Note that the following is not a native error:

js
const error = Object.create(Error.prototype);

This can be surprising, since error instanceof Error returns true. You can set any: true in the expectations to handle these values:

js
const error = Object.create(Error.prototype);
t.throws(() => { throw error }, {any: true});

AVA in Docker

If you run AVA in Docker as part of your CI, you need to fix the appropriate environment variables. Specifically, adding -e CI=true in the docker exec command. See #751.

AVA uses is-ci to decide if it's in a CI environment or not using these variables.

AVA and connected client limits

You may be using a service that only allows a limited number of concurrent connections. For example, many database-as-a-service businesses offer a free plan with a limit on how many clients can be using it at the same time. AVA can hit those limits as it runs multiple processes, but well-written services should emit an error or throttle in those cases. If the one you're using doesn't, the tests will hang.

By default, AVA will use as many processes as there are logical cores on your machine. This is capped at two in a CI environment.

Use the concurrency flag to limit the number of processes ran. For example, if your service plan allows 5 clients, you should run AVA with concurrency=5 or less.

Asynchronous operations

You may be running an asynchronous operation inside a test and wondering why it's not finishing. If your asynchronous operation uses promises, you should return the promise:

js
test('fetches foo', t => {
	return fetch().then(data => {
		t.is(data, 'foo');
	});
});

Better yet, use async / await:

js
test('fetches foo', async t => {
	const data = await fetch();
	t.is(data, 'foo');
});

If you're using callbacks, promisify the callback function using something like util.promisify():

js
import {promisify} from 'util';

test('fetches foo', async t => {
	const data = await promisify(fetch)();
	t.is(data, 'foo');
});

Attributing uncaught exceptions to tests

AVA can't trace uncaught exceptions back to the test that triggered them. Callback-taking functions may lead to uncaught exceptions that can then be hard to debug. Consider promisifying and using async/await, as in the above example. This should allow AVA to catch the exception and attribute it to the correct test.

Node.js command line options, child processes and worker threads

By default AVA runs test files in worker threads. However, not all Node.js command line options (those that end up in process.execArgv) are compatible with worker threads. You may get an error like this:

text
Error [ERR_WORKER_INVALID_EXEC_ARGV]: Initiated Worker with invalid execArgv flags: --title

If possible don't specify the command line option when running AVA. Alternatively you could disable worker threads in AVA.

Timeouts because a file failed to exit

You may get a "Timed out while running tests" error because AVA failed to exit when running a particular file.

AVA waits for Node.js to exit the worker thread or child process. If this takes too long, AVA counts it as a timeout.

It is best practice to make sure your code exits cleanly. We've also seen occurrences where an explicit process.exit() call inside a worker thread could not be observed in AVA's main process.

For these reasons we're not providing an option to disable this timeout behavior. However, it is possible to register a callback for when AVA has completed the test run without uncaught exceptions or unhandled rejections. From inside this callback you can do whatever you need to do, including calling process.exit().

Create a _force-exit.js file:

js
import process from 'node:process';
import { registerCompletionHandler } from 'ava';

registerCompletionHandler(() => {
	process.exit();
});

Completion handlers are invoked in order of registration. Results are not awaited.

Load it for all test files through AVA's require option:

js
export default {
	require: ['./_force-exit.js'],
};

Sharing variables between asynchronous tests

By default AVA executes tests concurrently. This can cause problems if your tests are asynchronous and share variables.

Take this contrived example:

js
import test from 'ava';

let count = 0;
const incr = async () => {
	await true;
	count = count + 1;
};

test.beforeEach('reset the count', () => {
	count = 0;
});

test('increment once', async t => {
	await incr();
	t.is(count, 1);
});

test('increment twice', async t => {
	await incr();
	await incr();
	t.is(count, 2);
});

Concurrent tests allow for asynchronous tests to execute more quickly, but if they rely on shared state this may lead to unexpected test failures. If the shared state cannot be avoided, you can execute your tests serially:

js
import test from 'ava';

let count = 0;
const incr = async () => {
	await true;
	count = count + 1;
};

test.beforeEach('reset the count', () => {
	count = 0;
});

test.serial('increment once', async t => {
	await incr();
	t.is(count, 1);
});

test.serial('increment twice', async t => {
	await incr();
	await incr();
	t.is(count, 2);
});

Is your problem not listed here? Submit a pull request or comment on this issue.


Support Statement

Supported Node.js versions

Translations: Français

AVA supports the latest release of any major version that is supported by Node.js itself.

Support here means that we run our test suite under the given Node.js versions and will accept pull requests to fix any bugs (provided they're not known bugs in Node.js itself that will be fixed imminently). Consequently, dropping support means we'll remove those Node.js versions from our test matrix and will no longer accept specific pull requests to fix bugs under those versions.

When we drop support for an LTS-covered major version we will bump AVA's major version number.

We try to avoid accidentally dropping support for non-latest Node.js releases. If such breakage does occur we'll accept pull requests to restore functionality. We might decide to deprecate the offending AVA release and bump AVA's major version number instead.

Whenever we bump AVA's major version number, we will explicitly drop support for non-latest Node.js releases. This ensures we can rely on backported APIs or the availability of newer V8 releases in later Node.js versions, either in AVA itself or one of our dependencies.

We may drop support for a Node.js version, in a major-version-bumping-pre-release, if that new AVA version is expected to become stable around or after the end-of-life date of the Node.js version in question.

Experimental features opted into through the nonSemVerExperiments configuration may be changed or removed at any time.


Readme

Please support our friend Vadim Demedes and the people in Ukraine.


AVA is a test runner for Node.js with a concise API, detailed error output, embrace of new language features and thread isolation that lets you develop with confidence 🚀

Watch this repository and follow the Discussions for updates.

Read our contributing guide if you're looking to contribute (issues / PRs / etc).

Translations: Español, Français, Italiano, 日本語, 한국어, Português, Русский, 简体中文

Why AVA?

Usage

To install and set up AVA, run:

console
npm init ava

Your package.json will then look like this (exact version notwithstanding):

json
{
	"name": "awesome-package",
	"type": "module",
	"scripts": {
		"test": "ava"
	},
	"devDependencies": {
		"ava": "^5.0.0"
	}
}

Or if you prefer using Yarn:

console
yarn add ava --dev

Alternatively you can install ava manually:

console
npm install --save-dev ava

Make sure to install AVA locally. AVA cannot be run globally.

Don't forget to configure the test script in your package.json as per above.

Create your test file

Create a file named test.js in the project root directory.

Note that AVA's documentation assumes you're using ES modules.

js
import test from 'ava';

test('foo', t => {
	t.pass();
});

test('bar', async t => {
	const bar = Promise.resolve('bar');
	t.is(await bar, 'bar');
});

Running your tests

console
npm test

Or with npx:

console
npx ava

Run with the --watch flag to enable AVA's watch mode:

console
npx ava --watch

Supported Node.js versions

AVA supports the latest release of any major version that is supported by Node.js itself. Read more in our support statement.

Highlights

Magic assert

AVA adds code excerpts and clean diffs for actual and expected values. If values in the assertion are objects or arrays, only a diff is displayed, to remove the noise and focus on the problem. The diff is syntax-highlighted too! If you are comparing strings, both single and multi line, AVA displays a different kind of output, highlighting the added or missing characters.

Clean stack traces

AVA automatically removes unrelated lines in stack traces, allowing you to find the source of an error much faster, as seen above.

Parallel runs in CI

AVA automatically detects whether your CI environment supports parallel builds. Each build will run a subset of all test files, while still making sure all tests get executed. See the ci-parallel-vars package for a list of supported CI environments.

Documentation

Please see the files in the docs directory:

Common pitfalls

We have a growing list of common pitfalls you may experience while using AVA. If you encounter any issues you think are common, comment in this issue.

Recipes

FAQ

How is the name written and pronounced?

AVA, not Ava or ava. Pronounced /ˈeɪvə/: Ay (face, made) V (vie, have) A (comma, ago)

What is the header background?

It's the Andromeda galaxy.

What is the difference between concurrency and parallelism?

Concurrency is not parallelism. It enables parallelism.

Support

Team

Mark Wubben Sindre Sorhus

Former