Introduction
AVA is a minimalistic JavaScript test runner designed for Node.js, focusing on speed, simplicity, and concurrent test execution. It provides a clean, distraction-free testing environment with isolated test files, modern JavaScript support, and no implicit globals.
Why AVA Matters
- Concurrency: Runs tests in parallel for faster execution
- Simplicity: Minimal API with focused functionality
- Isolation: Each test file runs in its own Node.js process
- Modern JS: Full ES6/ES2015+ support via Babel
- Clean Output: Simple, readable test output format
- Snapshot Testing: Built-in snapshot capability
Installation & Setup
Basic Installation
# Install as a dev dependency
npm install --save-dev ava
# Or using Yarn
yarn add --dev ava
Configuration in package.json
{
"scripts": {
"test": "ava"
},
"ava": {
"files": [
"test/**/*.test.js",
"!**/node_modules/**"
],
"concurrency": 5,
"failFast": true,
"failWithoutAssertions": false,
"verbose": true,
"timeout": "30s"
}
}
Configuration with ava.config.js
export default {
files: ['test/**/*.test.js'],
concurrency: 5,
failFast: true,
verbose: true
};
Test File Structure
Basic Test Structure
import test from 'ava';
test('my test name', t => {
t.pass();
});
Setup and Teardown
import test from 'ava';
test.before(t => {
// Runs before all tests
});
test.after(t => {
// Runs after all tests
});
test.beforeEach(t => {
// Runs before each test
});
test.afterEach(t => {
// Runs after each test
});
Core Assertions
| Assertion | Description | Example |
|---|
t.pass() | Test passes | t.pass() |
t.fail() | Test fails | t.fail() |
t.truthy() | Value is truthy | t.truthy(value) |
t.falsy() | Value is falsy | t.falsy(value) |
t.true() | Value is true | t.true(value) |
t.false() | Value is false | t.false(value) |
t.is() | Strict equality | t.is(value, expected) |
t.not() | Strict inequality | t.not(value, expected) |
t.deepEqual() | Deep equality | t.deepEqual(value, expected) |
t.notDeepEqual() | Not deep equal | t.notDeepEqual(value, expected) |
t.like() | Subset equality | t.like(value, subset) |
t.throws() | Function throws | t.throws(fn, [expected]) |
t.notThrows() | Function doesn’t throw | t.notThrows(fn) |
t.regex() | String matches regex | t.regex(string, regex) |
t.notRegex() | String doesn’t match regex | t.notRegex(string, regex) |
t.snapshot() | Match to snapshot | t.snapshot(value) |
Advanced Test Techniques
Async Testing
// Promises
test('async promise test', t => {
return somePromise().then(result => {
t.is(result, 'expected');
});
});
// Async/await
test('async/await test', async t => {
const result = await somePromise();
t.is(result, 'expected');
});
Test Modifiers
// Only run this test
test.only('will run', t => {
t.pass();
});
// Skip this test
test.skip('will not run', t => {
t.fail();
});
// Only run in specific environment
test.serial('runs serially', t => {
t.pass();
});
// Test runs but expected to fail
test.failing('expected to fail', t => {
t.fail();
});
// Todo test (reminder to implement)
test.todo('implement this test later');
Snapshot Testing
test('snapshot test', t => {
t.snapshot({
foo: 'bar',
baz: 'qux'
});
});
Macros (Reusable Tests)
import test from 'ava';
// Define a test macro
const macro = (t, input, expected) => {
t.is(input, expected);
};
// Use the macro with different inputs
test('macros-1', macro, 'foo', 'foo');
test('macros-2', macro, 'bar', 'bar');
// Macro with title function
macro.title = (providedTitle, input, expected) =>
`${providedTitle}: ${input} is ${expected}`;
Common Testing Patterns
Testing React Components with AVA
// Using with React Testing Library
import test from 'ava';
import { render, fireEvent } from '@testing-library/react';
import React from 'react';
import Button from '../components/Button';
test('Button renders correctly', t => {
const { getByText } = render(<Button>Click me</Button>);
t.truthy(getByText('Click me'));
});
test('Button fires onClick event', t => {
let clicked = false;
const { getByText } = render(
<Button onClick={() => { clicked = true; }}>Click me</Button>
);
fireEvent.click(getByText('Click me'));
t.true(clicked);
});
Testing APIs and HTTP Requests
// Using with Supertest
import test from 'ava';
import request from 'supertest';
import app from '../app';
test('GET /users returns 200', async t => {
const res = await request(app).get('/users');
t.is(res.status, 200);
});
// Using with Nock for mocking HTTP requests
import test from 'ava';
import nock from 'nock';
import { fetchUser } from '../api';
test('fetchUser makes correct API call', async t => {
nock('https://api.example.com')
.get('/users/1')
.reply(200, { id: 1, name: 'John' });
const user = await fetchUser(1);
t.is(user.name, 'John');
});
Testing with Mocks and Stubs
import test from 'ava';
import sinon from 'sinon';
import * as database from '../database';
import { saveUser } from '../users';
test('saveUser calls database correctly', async t => {
// Create a stub for database.save
const saveStub = sinon.stub(database, 'save').resolves({ success: true });
await saveUser({ id: 1, name: 'John' });
// Verify the stub was called with correct arguments
t.true(saveStub.calledWith({ id: 1, name: 'John' }));
// Restore the original function
saveStub.restore();
});
Common Challenges and Solutions
| Challenge | Solution |
|---|
| Tests too slow | Increase concurrency in AVA config; minimize setup/teardown operations |
| Test environment setup | Use test.before() hooks to initialize environment once |
| Flaky tests | Ensure proper isolation; avoid dependencies between tests; use timeouts |
| Database testing | Use test databases or in-memory databases; properly clean up after tests |
| Mocking dependencies | Use sinon, proxyquire, or mock-require to replace dependencies |
| Isolating tests | Leverage AVA’s process isolation; avoid shared state between tests |
| Coverage reporting | Integrate with NYC/Istanbul for coverage metrics |
Best Practices
Naming Conventions
- Use descriptive test names that explain the expected behavior
- Follow a consistent pattern like “should [expected behavior] when [condition]”
- Group related tests in separate files following the structure of your source code
Organizing Tests
- Keep test files close to the code being tested
- Split large test suites into multiple files
- One test file per module/component being tested
Test Structure
- Keep tests simple and focused on a single aspect
- Follow the Arrange-Act-Assert pattern
- Minimize dependencies between tests
Performance Tips
- Use
t.plan() to ensure all assertions run - Utilize AVA’s concurrency features
- Keep setup/teardown code minimal and fast
Debugging
- Use
t.log() to output debug information - Run specific tests with
npx ava test-file.js - Use
--verbose flag for detailed output
Command-Line Options
| Flag | Description |
|---|
--watch, -w | Re-run tests when files change |
--match, -m | Only run tests matching pattern |
--update-snapshots, -u | Update snapshots |
--fail-fast | Stop after first test failure |
--timeout, -T | Set global timeout |
--verbose, -v | Enable verbose output |
--serial, -s | Run tests serially |
--concurrency, -c | Max number of test files running at the same time |
AVA vs Other Testing Frameworks
| Feature | AVA | Jest | Mocha | Tape |
|---|
| Parallel Testing | ✅ Built-in | ✅ Built-in | ❌ Requires plugins | ❌ No |
| Assertions | ✅ Built-in | ✅ Built-in | ❌ Requires assertion lib | ✅ Built-in |
| Snapshot Testing | ✅ Built-in | ✅ Built-in | ❌ Requires plugins | ❌ No |
| Test Isolation | ✅ Process-level | ✅ Module-level | ❌ Limited | ❌ Limited |
| Configuration | ✅ Minimal | ✅ Extensive | ✅ Moderate | ✅ Minimal |
| Learning Curve | Moderate | Moderate | Low | Low |
| Community/Plugins | Moderate | Extensive | Extensive | Minimal |
| Built-in Mocking | ❌ No | ✅ Yes | ❌ No | ❌ No |
Resources for Further Learning
Official Resources
Tutorials and Guides
Complementary Tools