describe and it are aliases for suite and test, and they nest to any depth. before and after run once per suite; beforeEach and afterEach run around every test in the suite and in every suite nested inside it.
describe('cart totals', () => {
let cart;
before(() => console.log('opening the fixture'));
beforeEach(() => { cart = []; });
after(() => console.log('closing the fixture'));
describe('with one line', () => {
beforeEach(() => cart.push({ price: 4, qty: 3 }));
it('costs 12', () => assert.equal(cart[0].price * cart[0].qty, 12));
it.skip('handles gift wrap', () => {});
});
});opening the fixture
closing the fixture
▶ cart totals
▶ with one line
✔ costs 12 (1.6483ms)
﹣ handles gift wrap (0.1262ms) # SKIP
✔ with one line (2.3205ms)
✔ cart totals (3.6754ms)
ℹ tests 2
ℹ pass 1
ℹ skipped 1The inner beforeEach runs after the outer one, so cart is reset and then filled: outer hooks wrap inner ones. The console.log lines appear before the tree because the reporter buffers structured events and prints each suite when it completes, while raw stdout is forwarded immediately; use t.diagnostic() for a message that must land in place.
it.skip is not run and counts as skipped; it.todo runs but its failure does not fail the suite; it.only with --test-only runs just the marked tests. The options object takes those keys plus timeout, concurrency and signal. A hook returning a promise is awaited, and one that throws fails every test in its suite. t.after(fn) registers teardown from inside the test that needs it.
Tests in one file run sequentially unless you opt in with concurrency; files run in parallel, one child process each, capped at the CPU count. A test file binding a fixed port would otherwise collide with itself, and this is also why sharing state between test files is impossible. --test-concurrency=1 gives a suite the machine to itself; --test-isolation=none runs every file in one process, faster and without that protection.