The State of JavaScript 2017 survey says that the two most popular JavaScript testing frameworks are Mocha and Jasmine. They both use the same peculiar API: describe()
, beforeEach()
, and it()
. Whatās up with this? Why not just call your tests test()
?
The describe()
/beforeEach()
/it()
convention originated with the Ruby testing library RSpec, and is often referred to as spec-style. Just like in the JavaScript test libraries above, RSpec lets you declare tests by calls to it()
, nested inside describe()
s and with optional beforeEach()
calls. The other major convention is test-style and is inspired by JUnit, the Java library that first popularized unit testing. In JUnit, tests are just plain methods that have a test
prefix or a @Test
annotation.
Spec-style testing is falling into disfavor lately, and I think thatās largely due to a lack of understanding. So Iād like to offer my take on what the point of describe()
/beforeEach()
/it()
is. Understanding their intent can help improve the quality of your tests.
Explicit method calls
Itās often said that JUnit-style tests are simpler because theyāre just plain methods, but Iād disagree. With RSpec-style tests, you have an explicit API of methods/functions that you use to define tests, groups, and setup blocks. With JUnit, you have conventions you need to know to follow (test
prefixes or @Test
annotations), and if you forget to follow that convention, your test method may be silently skipped even though youāve defined a method that looks almost exactly like a correct one.
String descriptions
Another benefit of a spec-style API is the fact that tests are named with strings, not method names. This encourages using natural language for your test case names (it('throws an exception when invalid input is provided')
), rather than an abbreviated method-name style (function testInvalidInput
).
Languages donāt generally put a length limit on method names, so itās still possible to write descriptive test names in JUnit. Iāve even seen people in languages that use camelCase
method names advocate snake_case
for test method names, to make them more readable when they get long (function throws_an_exception_when_invalid_input_is_provided
). But as developers, itās hard to break the habit of writing succinct method names, even for test methods.
One of the main reasons to keep production method names short is to make them easier to remember, type, and read at the point of use. But none of those apply to test methods; the test runner calls them for you. All those names are used for is displaying output, so itās in your best interests to make them as descriptive as possible. And using strings for test names makes it easier to do so.
Interestingly, JavaScript doesnāt have a popular testing framework that plain function names for test cases. Jest, Ava, and QUnit all provide a test()
function that you pass a string name to. In JavaScript string descriptions for tests are pretty much universal.
Nesting
describe()
allows you to gather your tests into separate groupings within the same file, even multiple nested levels. Now, nesting is one of the most-maligned features of RSpec, because itās easy to take it too far. When you have three or four levels of nesting, and each level runs setup code in its own beforeEach()
, you have to look at many places throughout the file just to understand whatās going on in one test.
However, the lack of a nesting feature in some libraries this serves as a bit of friction against making robust test suites. For unit tests itās most common to have one test file per production class. But without the option of nesting, itās very tempting to write just one test case for each method of the class under test. This leads to one of two outcomes: either the test becomes very complicated, or, more commonly, the test covers only the most basic scenarios for the method under test.
For your tests to provide reliable protection against regressions, you want to cover all the scenarios a method handles. And for helpful error test failure messages, itās a good idea to keep your tests short. Writing tests that meet these criteria is easier with a grouping feature.
For example, take a look at this test for the push()
method of a stack.
describe('Stack.prototype.push()', () => {
let stack;
let result;
describe('when not full', () => {
beforeEach(() => {
stack = new Stack({ capacity: 5, contents: [1, 2, 3] });
result = stack.push(4);
});
it('returns true', () => {
expect(result).to.eql(true);
});
it('adds the new item to the stack', () => {
expect(stack.contents).to.eql([1, 2, 3, 4]);
});
});
describe('when full', () => {
beforeEach(() => {
stack = new Stack({ capacity: 3, contents: [1, 2, 3] });
result = stack.push(4);
});
it('returns false', () => {
expect(result).to.eql(false);
});
it('does not add the new item to the stack', () => {
expect(stack.contents).to.eql([1, 2, 3]);
});
})
});
Instead of just a single method, we have two nested describes covering two different scenarios. And each one has one assertion per test, so that when we run it we get a nice detailed outline:
# yarn test StackTest.js
Stack.prototype.push
when not full
ā returns true
ā adds the new item to the stack
when full
ā returns false
ā does not add the new item to the stack
4 passing (359ms)
Discover why a code audit is essential to your application’s success!
Required Setup
One downside of beforeEach()
is that it moves the setup of data for the test away from the body of the test itself. You have an alternative, though: manually calling setup methods within the body of a test. Hereās an example:
function setUp() {
const input = 'Hello world';
const thinger = new Thinger({ style: 'succinct' });
const flinger = new Flinger({ format: 'yyyy-mm-dd' });
const temp = thinger.thing(input);
const result = flinger.fling(temp);
result.processMore();
return result;
};
it('should have a success status', () => {
const result = setUp();
expect(result.success).to.be.true;
});
it('should have the correct message', () => {
const result = setUp();
expect(result.message).to.eq('Thinged and flinged.');
});
it('should have two floobles', () => {
const result = setUp();
expect(result.floobles.length).to.eq(2);
});
Since explicit setup function calls are an option, is there ever a reason to prefer beforeEach()
? Letās look at the differences:
let result;
beforeEach(() => {
const input = 'Hello world';
const thinger = new Thinger({ style: 'succinct' });
const flinger = new Flinger({ format: 'yyyy-mm-dd' });
const temp = thinger.thing(input);
result = flinger.fling(temp);
result.processMore();
});
it('should have a success status', () => {
expect(result.success).to.be.true;
});
it('should have the correct message', () => {
expect(result.message).to.eql('Thinged and flinged.');
});
it('should have two floobles', () => {
expect(result.floobles.length).to.eql(2);
});
This change has a few implications. First, it makes it clear that your intent is to run exactly the same setup before each of the tests, instead of (for example) calling a different setup method in some cases. It ensures that you wonāt accidentally forget to call setUp()
in future tests you write. And it removes duplication from the tests, shrinking their bodies from two to one line and making the assertion more visually prominent.
This is certainly an area where thereās a tradeoff; I wouldnāt advocate always using beforeEach()
blocks. But having the option is convenient when itās used discriminately.
Specification, not testing
This last point gets a little more abstract. Spec-style testing originated from a movement called Behavior-Driven Development (BDD). If you read Dave Astelsā and Dan Northās original blog posts about BDD, youāll see that spec-style testing began as a way to improve the language of Test-Driven Development. The word ātestingā suggests that youāre trying out something that already exists, but they wanted to emphasize āspecifyingā the behavior of something that doesnāt yet exist. RSpec followed this direction by moving away from using test
in method names entirely, using it
instead. Instead of declaring that you will test
something, you just declare what it
should do.
In one sense, this is only a matter of word choice: you can still do test-after development with describe()
and it()
, and you can do test-driven development with test()
. But word choice does make a difference. If youāre doing test-after development the words describe()
and it()
might feel unnecessarily abstract. And Astels and North argue that describe()
and it()
make it easier to teach new TDD practitioners to think about specifying code that doesnāt already exist.
Conclusion
Spec-style and test-style libraries correspond to two different approaches to testing. I prefer spec-style testing, but Iām glad that there are some good options for people who prefer test style, and I hope they gain more of an equal footing in the JavaScript world.
For anyone using a spec-style API, you can improve your tests by understanding how those APIs are intended to be used and the tradeoffs of the features they provide. Whether youāre using JavaScript or any other language, I highly recommend reading Effective Testing with RSpec 3 ā it lays out a mature spec-style testing approach thatās directly applicable to any programming environment.