Back to Testing guides

A Beginner's Guide to Unit Testing with AVA

Stanley Ulili
Updated on April 24, 2025

AVA is a fast, simple JavaScript test runner. It runs tests concurrently, supports modern JavaScript, and keeps testing simple with a clean, focused API.

AVA gives you isolated test environments, snapshot testing, and built-in assertions. This makes it great for testing both front-end and back-end JavaScript code.

This guide shows you how to use AVA's core features to write and run tests effectively.

Prerequisites

You need Node.js version 20.0 or higher.

You should also be familiar with JavaScript basics and understand basic testing concepts.

Step 1 — Setting up the project

Before you can write and run any tests with AVA, you need to set up a basic Node.js project. This step will guide you through creating a new folder, setting up your package.json file, and installing all the necessary dependencies to get started with testing.

First, create a new directory and go into it:

 
mkdir ava-testing && cd ava-testing

Set up a new npm project:

 
npm init -y

This creates a package.json file for your project's settings and dependencies.

Enable ES Modules support:

 
npm pkg set type="module"

Install AVA as a development dependency:

 
npm install --save-dev ava

Add a test script to your package.json:

package.json
...
{
  "scripts": {
"test": "ava"
} }

AVA doesn't look for test files in any specific folder by default. Tell AVA where to find your tests by adding this to your package.json:

package.json
{
  "scripts": {
    "test": "ava"
  },
"ava": {
"files": [
"test/**/*.js"
]
}
... }

Now let's create a simple function to test. Add this to a new file called utils.js:

utils.js
export function capitalize(string) {
  if (typeof string !== 'string') return '';
  return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
}

This function takes a string, capitalizes its first letter, and makes all other letters lowercase. Now let's write a test for it.

Step 2 — Writing your first test

Unit tests verify that your functions operate correctly in various scenarios. Instead of testing manually, AVA helps you automate this process.

Create a test folder:

 
mkdir test

Inside this folder, create a file called utils.test.js:

test/utils.test.js
import test from 'ava';
import { capitalize } from '../utils.js';

test('capitalize should transform the first character to uppercase', t => {
  t.is(capitalize('hello'), 'Hello');
});

In this file, you:

  • Import the test function from AVA
  • Import your capitalize function
  • Create a test with a clear description
  • Use t.is() to check if the function returns the expected result

The t object gives you methods to make assertions about your code.

Step 3 — Running your tests

Now run your test with this command:

 
npm test

This works because your package.json file has' "test": "ava" '. It's the same as typing:

 
npx ava

AVA finds and runs your test files based on your package.json settings. You'll see output like this:

Screenshot of AVA output after running tests

This means your test ran and passed.

Unlike some testing tools, AVA does not automatically monitor file changes. To enable this, add a watch script to your package.json:

 
{
  "scripts": {
    "test": "ava",
"test:watch": "ava --watch"
} }

Now you can run tests in watch mode:

 
npm run test:watch
Output
> ava-testing@1.0.0 test:watch
> ava --watch


  ✔ utils › capitalize should transform the first character to uppercase
  ─

  1 test passed [09:52:57]

  Type `r` and press enter to rerun tests
  Type `u` and press enter to update snapshots

With watch mode enabled, AVA reruns your tests whenever you modify your code, providing instant feedback.

Step 4 — Writing multiple test cases

Let's make our tests more complete by checking different situations:

test/utils.test.js
import test from 'ava';
import { capitalize } from '../utils.js';

test('capitalize should transform the first character to uppercase', t => {
  t.is(capitalize('hello'), 'Hello');
});

test('capitalize should transform the rest of the string to lowercase', t => {
t.is(capitalize('HELLO'), 'Hello');
});
test('capitalize should handle empty strings', t => {
t.is(capitalize(''), '');
});
test('capitalize should handle non-string inputs', t => {
t.is(capitalize(null), '');
t.is(capitalize(undefined), '');
t.is(capitalize(123), '');
});

Each test focuses on a specific behavior of the capitalize function. One test checks that the first letter of the string is correctly transformed to uppercase.

Another ensures that the rest of the string is converted to lowercase. There's also a test to verify how the function handles empty strings, and finally, a test confirms that non-string inputs like null, undefined, or numbers return an empty string.

Writing small, focused tests makes your test suite easier to maintain. When something breaks, you'll know exactly what failed.

Run all the tests:

 
npm test

You'll see all tests pass:

Screenshot of the four tests passing

Step 5 — Running specific tests

As your test suite grows, you won’t always want to run every single test. Sometimes, you’ll just want to focus on a specific test while fixing a bug or trying out a new feature. AVA makes this easy with a couple of handy tools.

Using test.only to focus on specific tests

If you’re working on a specific issue, you can use test.only() to run just that one test. This helps you stay focused and avoid noise from unrelated tests.

Add the highlighted code below:

test/utils.test.js
import test from 'ava';
import { capitalize } from '../utils.js';

test('capitalize should transform the first character to uppercase', t => {
  t.is(capitalize('hello'), 'Hello');
});

test('capitalize should transform the rest of the string to lowercase', t => {
  t.is(capitalize('HELLO'), 'Hello');
});

test.only('capitalize should handle empty strings', t => {
t.is(capitalize(''), ''); }); test('capitalize should handle non-string inputs', t => { t.is(capitalize(null), ''); t.is(capitalize(undefined), ''); t.is(capitalize(123), ''); });

Now when you run tests, only the test with .only runs:

Output
> ava-testing@1.0.0 test
> ava


  ✔ capitalize should handle empty strings
  ─

  1 test passed

This is extremely helpful when you need fast feedback while working on a single piece of logic.

Using test.skip to exclude tests temporarily

Let’s say one of your tests is broken or not relevant right now, and you want to ignore it for the moment. You can use test.skip() to instruct AVA to skip the test:

test/utils.test.js
import test from 'ava';
import { capitalize } from '../utils.js';

test('capitalize should transform the first character to uppercase', t => {
  t.is(capitalize('hello'), 'Hello');
});

test('capitalize should transform the rest of the string to lowercase', t => {
  t.is(capitalize('HELLO'), 'Hello');
});

test('capitalize should handle empty strings', t => {
t.is(capitalize(''), ''); });
test.skip('capitalize should handle non-string inputs', t => {
t.is(capitalize(null), ''); t.is(capitalize(undefined), ''); t.is(capitalize(123), ''); });

Now that test will be skipped:

Output
> ava-testing@1.0.0 test
> ava


  - [skip] capitalize should handle non-string inputs
  ✔ capitalize should transform the first character to uppercase
  ✔ capitalize should transform the rest of the string to lowercase
  ✔ capitalize should handle empty strings
  ─

  3 tests passed
  1 test skipped

Skipping a test like this is excellent for keeping your test suite green while you work on fixes or updates.

Filtering tests with command line arguments

You can also run tests by matching their names. This is useful when you don't want to change your test files:

 
npm test -- --match="*lowercase*"

This command runs only tests with "lowercase" in their name:

Output
  ✔ capitalize should transform the rest of the string to lowercase
  ─

  1 test passed

The double dash (--) separates npm's arguments from AVA's arguments. The stars (*) match any text before or after "lowercase".

Step 6 — Testing asynchronous code

In modern applications, asynchronous code is everywhere, whether you're fetching data from an API, reading a file, or querying a database. AVA is built to handle async code smoothly, so you can write tests that wait for promises to resolve or reject without extra setup.

In this step, you'll create a simple async function and test it. Let's simulate a user data fetch with a fake delay.

In your project’s root directory, create a new file called users.js and add the following code:

users.js
export async function fetchUserData(id) {
  // Fake delay like a real API call
  await new Promise(resolve => setTimeout(resolve, 100));

  if (!id) {
    throw new Error('User ID is required');
  }

  // Return fake user data
  return {
    id,
    name: `User ${id}`,
    email: `user${id}@example.com`
  };
}

This function mimics a typical async operation: it waits for a short delay and either returns fake user data or throws an error if no ID is provided.

Now let’s test this function. Create a new file called users.test.js inside your test folder and add the following:

test/users.test.js
import test from 'ava';
import { fetchUserData } from '../users.js';

test('fetchUserData returns user data for valid ID', async t => {
  const user = await fetchUserData(1);

  t.is(user.id, 1);
  t.is(user.name, 'User 1');
  t.is(user.email, 'user1@example.com');
});

test('fetchUserData throws error for missing ID', async t => {
  await t.throwsAsync(async () => {
    await fetchUserData();
  }, { message: 'User ID is required' });
});

The first test waits for fetchUserData(1) to return and then checks if the data is correct. The second test uses t.throwsAsync() to make sure the function throws an error when no ID is provided. Since AVA understands how to handle promises, it automatically waits for async functions to finish before running assertions.

If you only want to run the tests from this file, you can use one of the following command:

 
npm test -- test/users.test.js

If everything works, your test output will look something like this:

Output

  ✔ fetchUserData returns user data for valid ID (103ms)
  ✔ fetchUserData throws error for missing ID (102ms)
  ─

  2 tests passed

You're now successfully testing asynchronous code. In the next section, you'll learn how to mock your code using AVA.

Step 7 — Mocking with AVA

When testing your code, it's often a good idea to mock external dependencies, such as APIs, databases, or file systems. Mocking means replacing those real components with fake ones so you can test your code’s behavior in isolation.

AVA doesn’t include mocking out of the box, but it works smoothly with libraries like Sinon.js.

 
npm install --save-dev sinon

Let’s say you have a service that uses an API client to fetch a user’s name:

userService.js
export class UserService {
  constructor(client) {
    this.client = client;
  }

  async getUserName(id) {
    try {
      const user = await this.client.fetchUser(id);
      return user.name;
    } catch (error) {
      throw new Error(`Failed to get user: ${error.message}`);
    }
  }
}

This method fetches a user and returns their name. If something goes wrong, it throws a custom error.

Now create a test file at test/userService.test.js:

test/userService.test.js
import test from 'ava';
import sinon from 'sinon';
import { UserService } from '../userService.js';

test('getUserName returns user name for valid ID', async t => {
  // Create a fake client
  const mockClient = {
    fetchUser: sinon.stub().resolves({ id: 1, name: 'John Doe' })
  };

  // Use the fake client
  const userService = new UserService(mockClient);

  // Test the method
  const name = await userService.getUserName(1);

  // Check the result
  t.is(name, 'John Doe');

  // Verify the client was called correctly
  t.true(mockClient.fetchUser.calledOnceWith(1));
});

test('getUserName handles errors properly', async t => {
  // Create a fake client that causes an error
  const mockClient = {
    fetchUser: sinon.stub().rejects(new Error('Network error'))
  };

  // Use the fake client
  const userService = new UserService(mockClient);

  // Check that we get the right error
  const error = await t.throwsAsync(async () => {
    await userService.getUserName(1);
  });

  t.is(error.message, 'Failed to get user: Network error');
});

These tests simulate both success and error cases using mocked behavior. You’re not calling any real APIs; instead, you're controlling the behavior of the fetchUser method using sinon.stub().

Now run the userService.test.js file:

 
npm test -- test/userService.test.js

Both commands will run only the tests in this file. This is super helpful when you're working on one specific part of your app.

If all goes well, you should see something like:

Output

  ✔ getUserName returns user name for valid ID
  ✔ getUserName handles errors properly
  ─

  2 tests passed

With this, you've learned how to mock dependencies in AVA and isolate your logic for accurate, reliable testing.

Final thoughts

This guide walked you through AVA’s key features, including setting up a project, writing tests, handling async code, and using mocks. AVA is simple yet powerful.

For more advanced use, visit the official docs. Most importantly, testing helps you build confidence in your code so you can make changes without worry.

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Next article
A Beginner's Guide to Unit Testing with Pytest
Learn how to write clean, concise, and effective Python tests using Pytest's intuitive syntax, fixtures, parametrization, and rich plugin ecosystem
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github