Home

semtest 2.0.1

Build Status Coverage Status Code Climate Dependency Status devDependency Status

Semtest

A(nother ?) light unit test framework for nodejs

semtest: (javascript) => TAP

So what is this about ?

Concieved during the time I was writing Semverse, Semtest is a simple yet efficient unit test framework for nodejs apps. Its main goal (besides honing my skills on developing and maintaining an npm module) is to alleviate all the boilerplate around:

  • testing code with Tape (in its Blue-Tape version)
  • mocking external dependencies with Proxyquire
  • mocking internal dependencies with SinonJs
  • enforcing unit test best practices, such as no state sharing between tests, function focused tests, and self-documenting code

It is an essay on abstracting these three tools in a practical one, suited for my needs on Semverse.

I also decided that I would apply the same standards I use on Semverse:

  • 100 percent unit test coverage
  • JSLint compliance
  • Functional programming all the way !
  • Automatic semantic versioning

Installation

Installation is pretty straightforward :

$ npm install --save-dev semtest

Usage

$ node your_spec_file.js

or, since tape allows for globbing:

$ tape '**/*.spec.js'

or, since a lot of us uses build pipelines, you can pipe the sources with your favorite build tool. These specs files are autonomous javascript files so you can run them anyway you want. Hurray, separation of concerns !

Documentation

NOTE : What's described here is the final API Semtest will use. Until then please refer the code to know how to use it (hint: there is no global semtest function for the moment. This work is done by the first call of prepareForTests).

Using Semtest is a three step process:

  1. You create a unit test module wrapper for your project. A unit test module wrapper is simply a function that will allow you to get stubbed modules for unit testing. This is what the main Semtest function does: semtest: (options) => unitTestModuleWrapper

  2. You instanciate your stubbed module anytime by calling the newly created wrapper with the module name (with full path) and custom stubs related to the current unit test, if needed: unitTestModuleWrapper: (moduleName, customStubs) => stubbedModule

  3. You execute your unit tests with a clean, predefined structure that will help you both ensure your module works as expected regardless of its dependencies, and document your code behaviour way better than any comment could ever do: executeTests: (moduleDescription, functionAssertions) => TAP output

1. Creating your unit test module wrapper:

// Semtest unit test module wrapper is the function you get when requiring the
module:
const semtest = require("semtest");

/**
 * Semtest main function is called with an options hash map that will allow you to:
 * - optionnaly use aliases for your dependencies. Default is the classic
 *   name for node modules (like "fs", "lodash"), and full path + name
 *   for local project modules (like "/home/me/myProjects/myModule.js")
 * - optionnaly define default stubs you want to use for them. Default is no
 *   methods are available at all for each module: every "require" will give an
 *   empty object
 */
const myModuleWrapper = semtest({
    // For each dependency:
    ["lodash/fp"]: {
        // Aliases are defined as key "@alias":
        "@alias": "lodash",
        // And default stubs as "@stubs":
        "@stubs": {
            // Here we want to keep lodash functions untouched to use them in our
            // unit tests so instead of redefining them (remember that the
            // default disallows any method call !), we just use the Proxyquire way 
            // of stating "just let it as-is":
            "@noCallThru": false
        }
    },
    ["/home/me/myProject/myModule.js"]: {
        // Aliases for local modules are very convenient because we won't have
        // to specify the full path everytime
        "@alias": "myModule",
        "@stubs": {
            // Stubs can be anything: basic types like strings, numbers:
            dependencyA: "foo",
            // Or, more commonly, objects:
            dependencyB: {
                foo : "bar",
                baz : true
            }
            // or functions:
            dependencyC: () => true,
            // or, even more likely, object with methods:
            dependencyD: {
                methodA: (foo) => (foo.bar),
                methodB: (foo) => foo.then(() => true)
            }
            // In this "myModule" case, we don't use @noCallThru because the usual
            // best practice is to define stubs at unit test level. So @noCallThru
            // is true by default.
        }
    }
});

2. Using the wrapper to instanciate a module

/**

  • The wrapper is a function that you will call with a module full
  • path, that will output a "required" version of the module where all
  • dependencies will have been stubbed, either at the local level (with custom
  • stubs you want to use for the instanciation) or at wrapper level (stubs that
  • you've defined when you created the unit test module wrapper). */ const myStubbedModule = myModuleWrapper( '~/projects/myProject/myModule.js', {
     dependencyC: (foo) => (foo) ? "qux" : "quz"
    } );

/**

  • "myStubbedModule" is then like "myModule", except that its "dependencyC"
  • dependency (used in "myModule" as a 'require("dependencyC")' statement) will
  • just be the (foo) => ... function instead of () => true
  • (which was the stub that was specified earlier when the wrapper was created)
  • and, more important, instead of whatever it was originally, be it a node native
  • module (like "fs" or "path"), a classic node module (like "lodash") or a module
  • from the local project.. */ ```

3. Using your stubbed module to perform unit tests

Now that you have a stubbed module, you can unit test it very easily with one of Semtest methods: executeTests.

executeTests is a function that takes as input the module description (for documentation) and an array of function assertions blocks (more on that later) and output unit tests results in TAP format on stdout: executeTests: (moduleDescription, functionAssertionsBlocks) => TAP (on stdout)

// Let's consider a "myModule" method that uses "dependencyC":
myModule.myFunction = (a) => dependencyC(a);

// Then we can use the stubbed "myModule" we have created above to unit test it
// like this:
semtest.executeTests("My powerful module", [{
    name: "myFunction()",
    assertions: [{
        when: "it's called with a truthy value",
        should: "return 'qux'",
        test: function (t) {
            t.equal(
                myStubbedModule.myFunction(true),
                "qux"
            );
            t.end();
        }
    }]
}]);

// This will output a TAP stream that you can pipe into TAP readers like faucet
// or tap-spec

4. Assertions structure

Assertions are objects with three keys:

 - "when" and "should" are properties that will document your unit test
 - "test" is the actual unit test, written like a Blue-Tape function, which
   is a function that take the Blue-Tape assert object as parameter and
   should either output a promise (whose resolved/rejected status will
   pass or fail the test) or call the "plan"/"end" Tape methods.

Assertions for a function are bundled in a function assertion block, which is an object with two keys:

- "name" is the function name
- "assertions" is an array of assertions, defined above.

executeTests will then, take an array of function assertion blocks as its second input to, as you can expect it, run every unit test, of every function.

Every assertions will then be output on stdout as TAP with a description built like this:

"module" - "function": when "assertion.when", it should "assertion.should"

If we reuse our myModule example and pipe the result in faucet, we'll get:

$ node myModule.spec.js | faucet
myModule - myFunction: when it's called with a truthy value, it should return 'qux'
# tests 1
# pass 1
ok

The additional bonus of such structure is that you can fold your code in spec files to only show the documentation part, enabling your to truly document your code with specs, which they are for in the first place :

executeTests("Test helpers library", [{
    name: "rejectFnHO()",
    assertions: [{
        when: "its returned function is not given a rejection reason",
        should: "return a function that returns the base reason",
        test: (test) => test((t) =>
                [..............................................]
        )
    }, {
        when: "its returned function is given a rejection reason",
        should: "return a function that returns the given reason",
        test: (test) => test((t) =>
                [..............................................]
        )
    }]
}

Here, whatever the [.....] cover, we can focus on the litteral description of our code instead.

Since the spec files aren't meant to be delivered in production, we can make them as bloated as we want, for the sake of code literacy instead of performance.

Coverage

This module is fully compatible with any coverage tool as far as I know. I recommend nyc but you're free to use your favorite.

Utility functions

utilityFunctions is a collection of one-liners that are very useful to stub you dependencies with:

// Instead of defining the same stubs over and over again :
const myModule = myModuleWrapper(
    '~/projects/myProject/myModule.js',
    {
        foo: () => Promise.resolve(true),
        bar: () => Promise.reject(new Error("Oups")),
        baz: () => () => Promise.reject(new Error("Oups")),
        qux: () => () => null,
        derp: (a) => a
    }
);

// We can use utilityFunctions as quicker and more consistent way :
const u = require("semtest").utilityFunctions;

const myModule = myModuleWrapper(
    '~/projects/myProject/myModule.js',
    {
        foo: u.resolveFn(true),
        bar: u.rejectFn("Oups"),
        baz: u.rejectFnHO("Oups"),
        qux: u.nullFn,
        derp: u.idFn
    }
);

Licence

Do whatever you want with the code, just tell me about it so that I can know at least one person read it xD

Contributing

Open issues and send me PRs like there's no tomorrow, be my guest !