npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

mocha-chain

v0.0.4

Published

Utility methods to help manage and expose nx plugins

Downloads

16

Readme

mocha-chain

Chain mocha BDD methods together for determinstic and type safe tests.

npm package License

Table of Contents

Introduction

Mocha is a powerful and popular testing framework. Refer to the linked documentation for the best examples, but this is a simple demonstration:

import { suite, beforeEach, test } from 'mocha';
import { expect } from 'chai';

suite('Division.divide', () => {

    let division: Division | null = null;

    beforeEach(() => {
        division = new Division();
    });

    test('success', () => {
        expect(division!.divide(12, 3)).to.equal(4);
    });

    test('failure', () => {
        expect(() => division!.divide(12, 0)).to.throw(Error);
    });
});

In the example above, we can create a new instance of our Divison class before every test that runs in the Division.divide() suite. Then we can assert the expected behavior in each test.

Functionally, this works exactly as we want, but has a few drawbacks.

The first is that it is not type safe. We have to define the division variable in the shared scope, but only define it inside the beforeEach hook. While we know the hook will run before our test, typescript is not convinced, so we have to explicitly override the types at test time. If our tests are not type safe themselves, how can we be confident they are testing type safe interfaces?

What if we just created a single instance of Division and shared it across all tests?

const division = new Division();;

test('success', () => {
    expect(division.divide(12, 3)).to.equal(4);
});

Now it is type safe! But what if our class is mutable, or has some other internal state that individual tests can impact? This may be the case if we are working with an local database, or using a library like Sinon for setting up mocks per test.

So we could just ditch the beforeEach hook altogether, but this will begin to violate DRY, and we lose out on all the perks of mocha hooks, like teardowns of mocks.

Ideally we can still use these mocha hooks, but chain the results to be accessed in following hooks and tests.

Entry mocha-chain, which does exactly that.

Installation

npm i mocha-chain

Mocha-chain is an ESM package. It must be imported.

The type safety of mocha-chain is a major benefit, but does not require typescript or any further build step to support. It is not a replacement for mocha, but works alongside it. In fact there is a peer dependency on mocha and is completely interchangable.

Example

import { before, beforeEach, suite } from 'mocha-chain'; 

suite('Example Database Test', () => {

    // Optionally name hooks
    const withDb = before('Setup DB', async () => {
        const db = await connectToLocalDb();

        await db.createTables();

        return { db };
    });

    withDb.beforeEach(({ db }) => {
        // Both sync and async hooks supported!
        await db.populateData();
    });

    const withDbUser = withDb.beforeEach(() => {
        const user = createFakeUserData();
        // Merge `user` with existing `db`.
        return ({ user });
    });

    withDbUser.test('Can add user', async ({ db, user }) => {
        const success = await db.insert(user);
        expect(success).to.equal(true);
    });

    withDbUser.test('Cannot update user', ({ db, user }) => {
        // This test uses the same `db` as the previous test,
        // but a new `user`!
        const success = await db.update({
            ...user,
            name: 'Johnny B. Goode',
        });
        expect(success).to.equal(false);
    });

    suite('User already exists', () => {

        // Just like normal mocha, this only runs for tests inside the suite
        withDbUser.beforeEach(async ({ db, user }) => {
            await db.insert(user);
        });

        withDbUser.test('Cannot re-add user', ({ db, user }) => {
            const success = await db.insert(user);
            expect(success).to.equal(false);
        });

        withDbUser.test('Can update user', ({ db, user }) => {
            const success = await db.update({
                ...user,
                name: 'Johnny B. Goode',
            });
            expect(success).to.equal(true);
        });
    });

    withDb.test.skip('Have not implemented yet', ({ db, user }) => {
        await db.delete(user);
    });

    withDb.afterEach(({ db }) => {
        // Successfully tear down data in a type safe way.
        await db.clearData();
    });

    withDb.after(function ({ db }) {
        // Access the hook instance, just like mocha!
        this.timeout(1000);
        await db.disconnect();
    });
});

Usage

mocha-chain exports hook and test methods mirroring mocha. As stated above, this is not a replacement of mocha as these methods will invoke the native mocha methods under the hood.

However they provide the extra benefit of chaining the methods so any values returned by that hook (and any previous hooks) will be available to following hooks and tests. The values are returned as an object, with all keys merged with existing context object.

Besides the extra context as a parameter and the added data in the response, it otherwise maintains the API of mocha methods, including async and sync support, named hooks, and passing the test instance as this.

Similarly you can mark tests and suites with .only() and .skip().

Given that tests and hooks are just function calls, it is easy to accidentally perform the following:

suite('FooBar', () => {

    test('Does foo', () => {

        test('Does bar', () => {
            // This will silently never run!
        });
    });
});

mocha-chain will also enforce that the Does bar test above will immediately throw an error, causing Does foo to fail.

Native mocha behavior does not support embedding tests (or hooks, or suites...), but also does not enforce against it.

Chaining hooks

A before hook can have a context that is passed to a beforeEach hook. Similarly a beforeEach hook can have a context that is later referenced in both the test and the teardown afterEach hook.

But you can't access context of an afterEach hook in a test. Or before from an after.

The following table shows which hooks can be chained.

Note that suite is just used for setting up further hooks and tests, and does not return a chainable interface itself. It also runs before any hooks run, so it does not have any access to context itself.

Similarly test can chained from hooks, but does not return any context.

| chainable? | before | beforeEach | afterEach | after | |------------|:------:|:------:|:------:|:------:| | before | ✅ | ❌ | ❌ | ❌ | | beforeEach | ✅ | ✅ | ❌ | ❌ | | test | ✅ | ✅ | ✅ | ❌ | | afterEach | ✅ | ✅ | ✅ | ❌ | | after | ✅ | ❌ | ❌ | ✅ |

See the API section below for more details on the individual hooks, but the chaining behavior is the same for all.

The chained hook (or test) will accept a callback which will now be called with an additional parameter which is the context of previous hooks. It can further return more context which will be merged with the existing.

Note that the context object passed to further hooks is a shallow clone of the returned context, so it will be a new object on for every hook and test.

before(() => {
    return { abc: 123 };
}).beforeEach(({ abc }) => {
    return ({ efg: abc + 333 });
}).test('Chained', ({ abc, efg }) => {
    expect(abc).to.equal(123);
    expect(efg).to.equal(456);
});

API

suite

Aliases describe and context. Mirrors mocha method of the same name.

First parameter is the title of the suite. The second is a callback function which can set up hooks and tests internally to the context of this suite.

Callback must be synchronous, and async methods will result in a failure.

Calling this method inside another hook or test will result in an error.

Instead of using the method directly, may be modified with .skip() or .only() with the same parameters:

suite.only('Only this will run', () => {});
describe.skip('This does not run', () => {});

before

Alias suiteSetup. Mirrors mocha method of the same name.

A hook that runs at the very start of a suite. Optionally supports a title as the first argument, otherwise takes a callback that will be invoked for the hook. The callback may be async.

before('Optional title', () => {});
suiteSetup(async () => {});

If the callback returns an object, all keys are cloned over to chained context. This context is accessible in chained hooks before, beforeEach, test, afterEach, and after.

beforeEach

Alias setup. Mirrors mocha method of the same name.

A hook that runs before every test. Optionally supports a title as the first argument, otherwise takes a callback that will be invoked for the hook. The callback may be async.

If the callback returns an object, all keys are cloned over to chained context. This context is accessible in chained hooks beforeEach, test, afterEach.

test

Alias it, specify. Mirrors mocha method of the same name.

Runs the actual test, supporting both synchronous and asynchronous execution.

Instead of using the method directly, may be modified with .skip() or .only() with the same parameters:

test.only('Only this will run', () => {});
it.skip('This does not run', () => {});

afterEach

Alias teardown. Mirrors mocha method of the same name.

A hook that runs after every test. Optionally supports a title as the first argument, otherwise takes a callback that will be invoked for the hook. The callback may be async.

If the callback returns an object, all keys are cloned over to chained context. This context is accessible in chained hook afterEach.

before

Alias suiteTeardown. Mirrors mocha method of the same name.

A hook that runs at the very end of a suite. Optionally supports a title as the first argument, otherwise takes a callback that will be invoked for the hook. The callback may be async.

after('Optional title', () => {});
suiteTeardown(async () => {});

If the callback returns an object, all keys are cloned over to chained context. This context is accessible in chained hook after.