Now Available React Programming: The Big Nerd Ranch Guide
Front-End ReactBased on our React Essentials course, this book uses hands-on examples to guide you step by step through building a starter app and a complete,...
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.
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.
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.
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!
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.
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.
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.
Based on our React Essentials course, this book uses hands-on examples to guide you step by step through building a starter app and a complete,...
Svelte is a great front-end Javascript framework that offers a unique approach to the complexity of front-end systems. It claims to differentiate itself from...
Large organizations with multiple software development departments may find themselves supporting multiple web frameworks across the organization. This can make it challenging to keep...