If you’ve ever written tests for a Node.js application, chances are you used an external library. However, you don’t need a library to run unit tests in Javascript.

This post is going to illustrate how to make your own simple testing framework using nothing but the standard library.

It’s actually much simpler than you may think.

If you want to skip the explanation and just see the code, you can find it here

Components of a Test Framework

Before we try to make it ourselves, let’s try to understand the components that make up a test suite:

Tests

A test is the most basic unit of any testing setup, and contains a piece of code that throws an exception if the code doesn’t behave like it’s supposed to.

Here’s an example from the Mocha home page:

it('should return -1 when the value is not present', function() {
  assert.equal([1, 2, 3].indexOf(4), -1);
});

The test here, described by the it function, contains a name (‘should return -1 when the value is not present’), and a piece of code contained in the function argument.

tests contain a name and a function to run

This piece of code throws an exception if the described conditions are not satisfied.

Assertions

Assertions are used to trigger exceptions for non-compliant code.

The assert.equal method throws an exception if its two arguments are not equal to each other, and it is this exception that indicates to a test if the code it is testing is working or not.

Assertion helper functions are already present in the assert standard library, which we can use for writing our own tests as well.

Test Runners

After all the tests are defined, you need a way to run them. This is where frameworks like Mocha come in: they collect all the tests that you defined across multiple files, and run them one after the other.

runners take tests, and run them one by one

The runner catches the exceptions thrown in a failing test, and formats it into a nice-looking output, so that developers can easily tell what went wrong. For example, Mocha gives a readable formatted output describing the tests that passed or failed:

Array
  #indexOf()
    ✓ should return -1 when the value is not present


1 passing (9ms)

Creating Your Own Testing Framework

Now that we know the components required for running tests, let’s create our own bare-minimum testing framework.

Lets create a new project folder, and add a file test.js that will contain functions to declare and run tests.

Let’s first declare the test function:

// `tests` is a singleton variable that will contain all our tests
let tests = []


// The test function accepts a name and a function
function test(name, fn) {
	// it pushes the name and function as an object to
	// the `tests` array
	tests.push({ name, fn })
}

Next, we will define a function that will run all the declared tests:

function run() {
	// `run` runs all the tests in the `tests` array
	tests.forEach(t => {
		// For each test, we try to execute the
		// provided function. 
		try {
			t.fn()
			// If there is no exception
			// that means it ran correctly
			console.log('✅', t.name)
		} catch (e) {
			// Exceptions, if any, are caught
			// and the test is considered failed
			console.log('❌', t.name)
			// log the stack of the error
			console.log(e.stack)
		}
	})
}

Finally, we will write some code that will expose the test function to all our test files, and run them one after the other:

// Get the list of files from the command line
// arguments
const files = process.argv.slice(2)

// expose the test function as a global variable
global.test = test

// Load each file using `require`
files.forEach(file => {
	// Once a file is loaded, it's tests are
	// added to the `tests` singleton variable
	require(file)
})

// Now that all the tests from all the files are
// added, run them one after the other
run()

Before we run this though, we will need to write some tests…

Writing Tests

So far, we have a single test.js file. Lets add some files to write our tests.

First, we add a new file calculator.js which has simple functions to add and subtract numbers:

const add = (a, b) => a + b

const subtract = (a, b) => a - b

module.exports = {
        add,
        subtract
}

Next, we add the file that contains our tests, calculator-test.js :

// We use the assert standard library to make assertions
const assert = require('assert')
const { add, subtract } = require('./calculator')

// We do not need to import the test functions since
// they are made global variables by test.js
test('should add two numbers', () => {
        assert.equal(add(1, 2), 3)
})

test('should subtract two numbers', () => {
        assert.equal(subtract(3, 2), 1)
})

To run the test, execute the command:

node test.js ./calculator-test.js

Which will run all the tests and give you the output:

✅ should add two numbers
✅ should subtract two numbers

Let’s see what happens if we change one of the assertions so that the test fails:

test('should add two numbers', () => {
        assert.equal(add(1, 2), 4)
})

Now, when we run the tests, we get a helpful error message about which test failed, along with its stack trace:

❌ should add two numbers
AssertionError [ERR_ASSERTION]: 3 == 4
    at Object.fn (/Users/soham/go/src/github.com/sohamkamani/nodejs-test-without-library/calculator-test.js:5:16)
    at /Users/soham/go/src/github.com/sohamkamani/nodejs-test-without-library/test.js:18:6
    at Array.forEach (<anonymous>)
    at run (/Users/soham/go/src/github.com/sohamkamani/nodejs-test-without-library/test.js:14:8)
    at Object.<anonymous> (/Users/soham/go/src/github.com/sohamkamani/nodejs-test-without-library/test.js:48:1)
    at Module._compile (internal/modules/cjs/loader.js:959:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:995:10)
    at Module.load (internal/modules/cjs/loader.js:815:32)
    at Function.Module._load (internal/modules/cjs/loader.js:727:14)
    at Function.Module.runMain (internal/modules/cjs/loader.js:1047:10)
✅ should subtract two numbers

Congrats! You have now created a minimal test framework with 0 external libraries!

You can even add the test command to your package.json scripts:

{
  "name": "nodejs-test-without-library",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "test": "node test.js ./*-test.js"
  },
  "author": "",
  "license": "ISC"
}

Now all you need to do is run npm test and your tests will run like before.

If you want to see the complete code for this example, you can find it here

But Why?

I didn’t write this post to discourage you from using existing test frameworks. I personally use Mocha for most of my projects, and find it incredibly useful.

However, I feel like you should always know the concepts behind the tools you use. And hey, if the method described here is good enough for your use case, it’s actually better to just use it and have one framework less to worry about 🤷‍♂️.

Adding a dependency literally means that your code is dependent on someone else’s library.

For something as basic as testing, which is natively provided by the standard library for most popular programming languages, we should at least know how to implement it in Node.js as well.