@hapi/lab Documentation
repository·master·Indexed 20 days ago
https://github.com/hapijs/labA specialized, framework-agnostic test utility for Node.js. Part of the hapi ecosystem, @hapi/lab supports BDD and TDD patterns, TypeScript execution, and integrates with assertion libraries like @hapi/code. It features a CLI for test execution, coverage reporting (HTML, JSON, JUnit, etc.), and lifecycle hooks (before, after, beforeEach, afterEach) for managing test state and setup.
What's inside @hapi/lab
- @hapi/lab is a Node.js test utility designed for testing. While it is part of the hapi ecosystem and integrates seamlessly with the hapi web framework, it is a standalone tool that can be used with any web framework or in any Node.js project.
Organize tests using experiments and lifecycle methods
masterTests can be grouped into
experiment()blocks. Within an experiment, you can define lifecycle hooks to run setup or teardown logic:before(): Runs once before the experiment starts.after(): Runs once after the experiment finishes.beforeEach(): Runs before every individual test in the experiment.afterEach(): Runs after every individual test in the experiment.
All lifecycle methods support returning a
Promiseto handle asynchronous setup/teardown.const Code = require('@hapi/code'); const Lab = require('@hapi/lab'); const { expect } = Code; const lab = exports.lab = Lab.script(); lab.experiment('math', () => { lab.before(() => { return new Promise((resolve) => { setTimeout(resolve, 1000); }); }); lab.beforeEach(() => { // Run before every single test }); lab.test('returns true when 1 + 1 equals 2', () => { expect(1 + 1).to.equal(2); }); });Manage shared state using context
masterThe
contextobject is passed tobefore,after,beforeEach, andafterEachhooks, as well as to the tests themselves. It is used to share properties between hooks and tests without relying on module-level variables.Key Behavior:
- The
contextobject is shallow cloned when passed to tests and child experiments. - Modifications made to the context inside a test or a nested experiment will not affect the context of other tests or the parent experiment.
lab.experiment('my experiment', () => { lab.before(({ context }) => { context.foo = 'bar'; }) lab.test('contains context', ({ context }) => { expect(context.foo).to.equal('bar'); }); lab.experiment('a nested experiment', () => { lab.before(({ context }) => { context.foo = 'baz'; }); lab.test('has the correct context', ({ context }) => { expect(context.foo).to.equal('baz'); context.foo = 'fizzbuzz'; // This change stays within this test/experiment }); lab.test('receives a clean context', ({ context }) => { expect(context.foo).to.equal('baz'); }); }); });- The
Integrate an assertion library with Lab
masterYou can integrate Lab with any assertion library using the
--assertargument. When an assertion library is specified, it is imported and assigned to theLab.assertionsproperty.If you use
@hapi/code, Lab provides additional features:- Missing Assertion Detection: Lab will report incomplete assertions (e.g., if you reference a property instead of calling a method) and return a failure.
- Verbosity Reporting: Lab calculates the ratio of assertions to tests and outputs this value when using the
consolereporter.
lab --assert @hapi/codeManage coverage with inline comments and a bypass stack
masterYou can control which lines are included in coverage reports using special comments.
Inline enabling/disabling
Use
$lab:coverage:off$to exclude a block and$lab:coverage:on$to resume coverage.Coverage bypass stack
To handle complex scenarios like transpiled code (e.g., via Babel) where simple on/off toggles might overwrite each other, use a stack-based approach:
$lab:coverage:push$: Copies the current skip state to the top of the stack and keeps it as the current state.$lab:coverage:pop$: Replaces the current skip state with the top of the stack and removes that top entry.
Note: If you attempt to pop an empty stack, Lab will throw the error
"unable to pop coverage bypass stack".// Example of using the stack for transpiled code /* $lab:coverage:off$ */ const { types } = /*$lab:coverage:push$/ /*$lab:coverage:off$*/ _util /*$lab:coverage:pop$/ . /*$lab:coverage:push$/ /*$lab:coverage:off$*/ default /*$lab:coverage:pop$/ ; /* $lab:coverage:on$ */Run TypeScript tests with Lab
masterTo run tests written in TypeScript, use the
--typescriptCLI option. Lab includes a TypeScript definition file to facilitate usage. If your project uses custom paths (e.g., viatsconfig-paths), you can pass--require 'tsconfig-paths/register'to the command.TypeScript Example:
import * as Lab from '@hapi/lab'; import { expect } from '@hapi/code'; const lab = Lab.script(); const { describe, it, before } = lab; export { lab }; describe('experiment', () => { before(() => {}); it('verifies 1 equals 1', () => { expect(1).to.equal(1); }); });Execution Commands:
# Basic TypeScript execution $ lab --typescript # TypeScript execution with custom path resolution $ lab --typescript --require 'tsconfig-paths/register'Debug Lab tests with V8 Inspector
masterTo debug your tests using the V8 Inspector, use the
--inspectflag. This will print a URL to the console that you can use with Chrome DevTools or other V8 inspector extensions.If your tests are mapped to
npm test, you can pass the flag through npm:npm test -- --inspect.You can also specify a custom port:
lab --inspect={port}.lab --inspectInstall and run lab via npm
masterAdd
@hapi/labas a development dependency in yourpackage.json. You can define scripts to run tests with specific configurations, such as setting a coverage threshold or generating HTML coverage reports.By default, running
labloads all*.js,*.cjs, or*.mjsfiles inside the localtestdirectory. To run specific files, pass them as arguments to the CLI.{ "devDependencies": { "@hapi/lab": "21.x.x" }, "scripts": { "test": "lab -t 100", "test-cov-html": "lab -r html -o coverage.html" } }# Run tests for a specific file $ lab unit.js # Run tests via npm $ npm testImplement BDD and TDD patterns in Lab
masterYou can configure your
Lab.script()export to follow Behavior Driven Development (BDD) or Test Driven Development (TDD) naming conventions by destructuring specific methods.// BDD Style const { describe, it, before, after } = exports.lab = Lab.script(); describe('math', () => { it('returns true when 1 + 1 equals 2', () => { expect(1 + 1).to.equal(2); }); }); // TDD Style const { suite, test } = exports.lab = Lab.script(); suite('math', () => { test('returns true when 1 + 1 equals 2', () => { expect(1 + 1).to.equal(2); }); });Create a test script with Lab.script()
masterTo use lab, you must require the module and export a test script using
Lab.script(). The exported object must be namedlabfor the runner to find it. You can use either theitpattern or thetestpattern.Note: lab works best with
@hapi/code, but is compatible with any assertion library that throws an error on failure. For asynchronous tests, useasync/awaitor return aPromise.const Code = require('@hapi/code'); const Lab = require('@hapi/lab'); const { expect } = Code; // Exporting the script as 'lab' is required const { it } = exports.lab = Lab.script(); it('returns true when 1 + 1 equals 2', () => { expect(1 + 1).to.equal(2); });Use code coverage with ES Modules (ESM)
masterLab does not natively support code coverage for ES modules. For ESM projects, it is recommended to use c8.
Setup with c8:
- Install c8:
npm install --save-dev c8. - Update your test script in
package.jsonto prefix the lab command withc8.
"scripts": { - "test": "lab -a @hapi/code -t 100" + "test": "c8 --100 lab -a @hapi/code" }- Install c8:
Configure ESLint with @hapi/eslint-plugin
masterTo use the recommended hapi-specific linting rules with lab, you must add
@hapi/eslint-pluginto your project and extend its configuration in your ESLint setup.- Add
@hapi/eslint-pluginas a dependency inpackage.json. - In your ESLint configuration file, add
"extends": "plugin:@hapi/recommended".
To ignore specific files in your ESLint configuration while preserving hapi rules, use the
ignoresproperty.import HapiPlugin from '@hapi/eslint-plugin'; export default [ { ignores: ['node_modules/*', '**/vendor/*.js'], }, ...HapiPlugin.configs.module, ];- Add