AVA Test Runner

repository·main·Indexed 12 days ago

https://github.com/avajs/ava

A fast, minimal Node.js test runner emphasizing concurrency, thread isolation, and clean error reporting. Version 8.0.1 supports async/await, Observables, and TypeScript. Key features include test macros, custom TAP reporters, and modifiers like .serial, .only, .skip, and .failing for precise test control. It provides flexible hooks for setup and teardown and allows for shared context via t.context.

Tokens
31.8K
Snippets
139
Records
164
Agent score
96%

What's inside AVA

  1. How shared workers work in AVA

    main

    Shared workers allow a program to run in a worker thread within AVA's main process, enabling communication with code running in individual test workers. This is useful for managing shared resources (like database connections or port locking) across all test files in a run.

    Key characteristics:

    • Persistence: In watch mode, shared workers remain loaded across runs.
    • Resource Management: They provide opportunities to set up resources before tests start and clean them up after.
    • Communication: Test workers and the shared worker communicate via a versioned protocol using message passing.
  2. Share context between hooks and tests

    main

    You can share data between hooks and tests using t.context.

    • When a .before() hook modifies t.context, a shallow copy is passed to .beforeEach() hooks and tests.
    • For .beforeEach(), .afterEach(), and .afterEach.always(), the context is not shared between different tests, preventing data leakage.
    • .after() and .after.always() hooks receive the original context value.
    test.beforeEach(t => {
    	t.context.data = generateUniqueData();
    });
    
    test('context data is foo', t => {
    	t.is(t.context.data + 'bar', 'foobar');
    });
  3. Use `test.serial` for database tests

    main

    By default, AVA runs tests concurrently. If your tests modify a shared database (like Mongoose/MongoDB), concurrent execution can cause tests to interfere with each other, leading to unpredictable results.

    To ensure tests run one at a time and maintain a predictable state, use test.serial() instead of test().

    If you require concurrent testing with a database, you must configure separate Mongoose connections for each test, which is a more complex setup.

  4. Understand the Execution Context (`t` argument)

    main

    In AVA, every test and hook is provided with an execution context object, conventionally named t. This object is unique to each test or hook and provides access to assertions, metadata, and lifecycle management methods. It is the primary interface for interacting with the test runner during execution.

    import test from 'ava';
    
    test('my passing test', t => {
    	t.pass();
    });
  5. Support asynchronous tests with Promises, Async/Await, or Observables

    main

    AVA natively supports asynchronous testing patterns. A test is treated as asynchronous if you:

    1. Return a Promise.
    2. Use an async function.
    3. Return an Observable (from es-observable).

    AVA will wait for the promise to resolve or the observable to complete before ending the test. If a promise rejects, the test fails.

    // Promise support
    test('resolves with unicorn', t => {
    	return somePromise().then(result => {
    		t.is(result, 'unicorn');
    	});
    });
    
    // Async function support
    test(async function (t) {
    	const value = await promiseFn();
    	t.true(value);
    });
    
    // Observable support
    test('handles observables', t => {
    	t.plan(3);
    	return Observable.of(1, 2, 3, 4, 5, 6)
    		.filter(n => n % 2 === 0)
    		.map(() => t.pass());
    });
  6. How AVA splits tests in CI

    main

    AVA can automatically detect if your CI environment supports parallel builds using the ci-parallel-vars package. When detected, AVA sorts all test files by name and splits them into chunks. Each CI machine is assigned a specific chunk (subset) of tests to run in parallel, optimizing execution time.

    To disable this automatic detection and splitting, set utilizeParallelBuilds to false in your AVA configuration.

  7. Understand AVA snapshot reports

    main

    When using snapshot testing in AVA, the actual snapshot data is saved in a .snap file (e.g., try-snapshot.js.snap). The test report displays the snapshots captured during the run, categorized by how the tests were executed: serial or concurrent.

    Each snapshot in the report is identified by a number (e.g., > Snapshot 1) followed by the captured value. This allows you to verify that the expected output matches the recorded snapshot.

    ## serial
    
    > Snapshot 1
    
        'hello'
    
    > Snapshot 2
    
        true
  8. How t.try() works for testing flaky code

    main

    The t.try() method allows you to attempt assertions without immediately failing the test. This is useful for handling flaky logic or testing multiple possible outcomes.

    Workflow:

    1. Call await t.try(implementation, ...args). The implementation is an async function that receives its own execution context.
    2. The result object contains passed (boolean), errors (array), title (string), and logs (array).
    3. You must call either .commit() or .discard() on the result.
      • If you call .commit() on a failed attempt, the test will fail.
      • If you call .discard(), the failure is ignored.
    4. To retain logs from the attempt in your main test logs, pass {retainLogs: true} to commit() or discard().

    Note: You cannot use snapshots within t.try() if running multiple attempts concurrently.

    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();
    });
  9. How snapshot testing works in AVA

    main

    AVA supports snapshot testing for any value via its assertions interface. When a snapshot assertion is executed, AVA creates two files alongside your test file:

    1. .snap file: Contains the actual snapshot data and is required for future comparisons.
    2. .md file: A snapshot report that can be committed to source control to allow diffing changes.

    Storage Locations:

    • If tests are in test/ or tests/, snapshots are stored in test/snapshots/ or tests/snapshots/.
    • If tests are in __tests__/, snapshots are stored in __snapshots__/.
    • If running against precompiled files (e.g., TypeScript), AVA uses source maps to store snapshots next to the original source files.

    Example File Structure: If your test is at ~/project/test/main.js, AVA creates:

    • ~/project/test/snapshots/main.js.snap
    • ~/project/test/snapshots/main.js.md
  10. Choosing between `beforeEach()` and setup functions

    main

    When setting up test state in AVA, you can use the built-in beforeEach() hook or plain JavaScript setup functions.

    Use beforeEach() when:

    • You want the same setup applied to all tests in a file.
    • You need built-in support for observables.
    • You want failure output to be clearly associated with the hook rather than the test.
    • You want to use afterEach() or afterEach.always() for automatic cleanup.

    Use setup functions when:

    • You need different setup requirements for different tests.
    • You want to skip setup for specific tests.
    • You need to pass parameters to customize the setup (e.g., valid vs. invalid data).
    • You want to avoid the "magic" of hooks and keep setup logic explicit within the test body.
    • Note: Asynchronous setup functions must use Promises.
  11. Use assertion planning to ensure test coverage

    main

    Assertion planning via t.plan(n) ensures that a test only passes if exactly n assertions are executed. This prevents tests from exiting early (e.g., due to an unhandled promise or early return) without running all intended checks. It also prevents tests from passing if too many assertions are executed (e.g., inside a loop).

    Important Behaviors:

    • If you do not specify a plan, AVA will still fail if zero assertions are executed (unless failWithoutAssertions is set to false in your package.json configuration).
    • Unlike tap or tape, AVA does not automatically end a test when the planned assertion count is reached.
    test('resolves with 3', t => {
    	t.plan(1);
    
    	return Promise.resolve(3).then(n => {
    		t.is(n, 3);
    	});
    });
  12. How to use `t.plan()` in AVA

    main

    In AVA, t.plan(n) is used to assert that exactly n assertions are called during a test. Unlike tap or tape, calling t.plan() does not automatically end the test; it only validates the assertion count.

    When to use t.plan()

    You should use t.plan() when your test has non-straightforward code flow that makes it difficult to reason about how many assertions will actually execute. Good candidates include:

    • Tests with branching statements (if/else).
    • Tests with assertions inside callbacks.
    • Tests containing loops (for/while).
    • Tests using try/catch blocks (though t.throws() or t.throwsAsync() is often preferred).

    When NOT to use t.plan()

    Avoid using t.plan() in the following scenarios as it adds unnecessary maintenance overhead:

    • Sync tests with no branching: If the code path is linear, t.plan() is redundant.
    • Promises expected to resolve: If you are testing a successful promise resolution, use async/await instead of .then() callbacks. A rejected promise will fail the test automatically.
    • Promises with .catch() blocks: Instead of using t.plan() to ensure a .catch() block is hit, use t.throwsAsync() with async/await for flatter, more readable code.
    • Manual try/catch for errors: Use t.throws() or t.throwsAsync() instead of manually catching errors to assert on them.
    test('foo or bar', t => {
    	const result = functionUnderTest(testDefinition.input);
    
    	// Use t.plan(1) to ensure exactly one assertion runs despite the branching
    	t.plan(1);
    
    	if (testDefinition.foo) {
    		t.is(result.foo, testDefinition.foo);
    	}
    
    	if (testDefinition.bar) {
    		t.is(result.bar, testDefinition.foo);
    	}
    });