Annotation-based javascript unit tests with saul

From the saul demo.

There’s too much test boilerplate running around in Javascript projects IMHO. There are genuine cases in which some use-cases require well-thought out non-trivial test cases. But most test cases I see in the wild follow some variation of:

import Utils from 'utils';
import { expect } from 'chai';
import { myFunc, myOtherFunctionWhichCallsUtl } from 'myModule';

describe('My awesome suite', () => {
  it('should produce proper output', () => {
    const actual = myFunc(arg1, arg2, arg3);
    const expected = { foo: 'bar' }
    
    expect(actual).to.eql(expected);
  });
  
  it('should call the utilFunction', () => {
    Utils.utilFunction = sinon.spy(); // mocks the utilFunction
    myOtherFunctionWhichCallsUtl();
    
    expect(Utils.utilFunction.calledOnce).to.equal(true)
  });
});

What if we can reduce this effort with a custom DSL? Or better yet, with a DSL, that allows us to co-locate our tests with functions using annotations? Something like this:

// @t "produces output" myFunc('foo', 'bar', 'baz') ~equals {'foo': 'bar'}
export function myFunc(arg1, arg2, arg3) {
  /* ... function content ... */
}

// @t "calls util" myOtherFunctionWhichCallsUtil(spy('util')) ~expect spy('util').calledOnce
export function myOtherFunctionWhichCallsUtil(utilFn = Utils.utilFunction) {
  /* ... function content ... */
  utilFn();
}

This is what I tried to do with . It lets you write tests alongside your code for most of the cases, and it generates framework agnostic tests on the fly and feed it to your test runner — whether it’s mocha, jest or jasmine.

A few benefits I’ve noted in my own projects include:

  1. Self-documentation: Annotated tests provide some documentation as to what the tested artifact accomplishes — and it runs a valid test as well.

  2. Forces me to write more testable code: When I feel-like I can’t write a meaningful test for a function with saul, it indicates to me that maybe the function is doing “too much” or “too little to have a meaningful test”.

  3. Allows me to test domain-specific use-cases: If I have a bunch of tests that I need to write something very specific to my domain, I can easily use a custom-engine and assert that logic. Like:

// @t "foo" foo(1) ~does-my-domain-specific-stuff
export function foo(arg) {
  // does something domain specific
}

// @t "foo" foo(1) ~does-my-domain-specific-stuff
export function bar(arg) {
  // does something domain specific, similar to foo()
}

// @t "foo" foo(1) ~does-my-domain-specific-stuff
export function baz(arg) {
  // does something domain specific, similar to foo() and bar()
}

That’s the spiel! Check out . Happy testing!

Written on August 19, 2017