Here's a taste of what's been included in this version 2.0 release:
- A (breaking) change to how
td.replace()swaps out constructor functions. In 1.x,
td.replacewould inject an artificial constructor and return to the test a plain object of test double functions. Instead, 2.x returns the entire fake constructor to the test. User feedback tells us this behavior will be less surprising. The change also enables tests to stub & verify how constructors themselves are invoked for the first time. Moreover, "static" properties on the constructor are now also replaced with test doubles in addition to prototypal functions. Finally, this behavior is now exposed via a new top-level
td.constructor()API method (as opposed to only being accessible via
td.replace). More on constructor function changes here
td.function()method of creating a test double has now gained the ability to mimic an actual dependency the same way its siblings
td.constructor()imitate objects and constructors. When passed a function,
td.functionwill infer its name, copy its properties, and replace any function properties with test doubles (e.g. imagine a module that exports an async function but also exposes a
syncversion as a property of the primary one—now both functions will be placed under the test's control!). More on td.function's changes here
- Proxy test doubles are now a practical option for many teams. While not a new feature, many testdouble.js users are now running Node.js version 6 or higher, which includes support for ES Proxy. When we first shipped this feature, it only worked on prerelease versions of Firefox, so most users missed it. If you haven't tried this "blank slate" approach to designing your subject's dependencies, give it a try!
- Support for nested usage of argument
which allows for even more granular control over how a test double is invoked.
Now instead of specifying whether an entire argument satisfies
td.matchers.isA(Number), you can assign the matcher to a single property on an object or element of an array. There are now vanishingly few invocations that can't be specified by testdouble.js. More on improved nested matcher support here
- Asynchronous support for stubs that use
thenReject. Callback stubs can now be deferred to a later call stack by setting the
deferoption to true. Callback and promise stubs can also be configured with a
delay(in milliseconds) so tests can have fine-grained control over the order and timing of asynchronous events in cases significant to the subject. More on async stubbing here
- Improved messages when a
td.verify()call fails due to a test double not being invoked the right number of times in a way that satisfied the specified verification. (This gets really tricky when using argument matchers, trust us!) More on improved verification messages here
- Internal build improvements, notably the incorporation of babel and yarn into the project. Neither of these are user-facing changes, but we hope the conversion of the project to ECMAScript 201X will lower the barrier of entry for new contributors
These changes also set us up for a few exciting new ideas for the 2.0 series of the library. Just a sample of them include:
- Now that each type of test double can be created by imitating a production
dependency, test doubles are now primed to provide warnings when stubbed or
verified with an arity that doesn't match the defined function
length. This may be a useful way to combat so-called fantasy tests as test doubles slip out of alignment with their mirrored actual dependencies. More on the opportunity to warn about arity mismatches here
- While testdouble.js boasts a one-of-a-kind test double introspection API in
it still requires a bit of futzing when a test fails unexpectedly. The recent
improvements to how messages are factored will allow us to be more proactive
when a test fails. By passing failure status to
td.reset()(which you should be calling after each test!), testdouble.js could be improved to log out a state of its universe (e.g. what test doubles were created by the test, and how they were each stubbed, invoked, and verified). Our goal with this feature is to increase the odds that you'll recognize the cause of an error without having to debug or
console.logand re-run the test. The issue for this
td.resethealth check is here
- Currently, each mechanism available for creating test doubles performs a
shallow clone of the actual dependency being doubled. Now that the
td.constructor()API has been made to be symmetrical between the test and subject, we can consider implementing a "deep mock" feature that recursively replaces even deeply-nested functions in a dependency with test doubles. What remains to be seen is whether this is ever actualy a good idea, however. If you feel strongly about this, please let us know
There's never been a better time to take a second look at incorporating testdouble.js into the design of your unit tests. Our goal was to ship a 2.0 release that establishes solid footing on which the library can be maintained for years to come and we're confident this release has accomplished that goal.
Slow and steady adoption
In the 18 months since we first published our eponymous testdouble.js library, we've been reminded that it's pretty hard to rally a community around a mocking library, even if we're certain it's far-and-away the best one available for a given language.
Why it's important that testdouble.js exists
The first mocking library I encountered that featured a pleasant user experience was Java's Mockito, years ago. But even though it was vastly superior, it took many more years before its adoption rate (much less its userbase) began to rival other Java mocking libraries—despite their painfully clunky APIs. We're now seeing a similar curve to the adoption of testdouble.js, even as its chief alternative Sinon.js is now clearing an astonishing 3 million downloads per month.
But for the fact a lot of Sinon users aren't aware of its alternatives, the relatively gradual adoption of testdouble.js is not a grave concern to us. If anything, this presents an opportunity to explain why test doubles are emblematic of a cadre of low-priority-and-yet-fundamental aspects of software development.
First, consider just how distant test doubles can seem from the vantage point of a team trying to build and deploy an application:
- The primary concern of software development is the production code. Opinions vary wildly on the best way to write code. Even if two vastly different implementations both behave identically from the perspective of the user, developers regularly stake their reputations on passionately-held beliefs over how software should be made. Even when arguments over code devolve into absurdity, debate is at least governed by a clearly-defined limiting factor: an implementation has to actually work to be considered superior
- A secondary concern of development is automated testing. Here, opinions are even more diverse! Teams regularly litigate what types of tests to write and how many—they even disagree about the nature of the confidence they seek from their code's test suites (it's about design! Nay, integration!). The governing principle of debates over testing is therefore much looser than it is for code, but it still exists: the tests should pass, be reasonably fast, and give confidence that the production code probably works
- Mocking libraries represent a distant tertiary concern for developers: to the extent their tests can't (or shouldn't) perfectly replicate real-world conditions in which to exercise their production code, how should we go about faking out reality? Because "making test doubles" is in nobody's job description, most developers rarely evolve beyond intuition when it comes to gauging how much fakeness they should tolerate. Most of a team's (very limited) energy on this concern is spent arguing whether to fake something or to what extent, much less why or how. If a limiting factor exists to keep these debates grounded in some kind of value system, it's so hazy as to be imperceptible
If there's one principle that has guided my own open source, writing, and speaking, it's been that the industry doesn't give people enough time to consider secondary-but-still-important topics that impact the software we write. Having a consistent, comprehensible, and well-reasoned approach to defining reality is exactly one such topic. It's true: mocking libraries are a distant, seemingly-trivial concern from the perspective of the people cutting our paychecks. But that doesn't make it any less critical that teams maintain a clear understanding of the context under which their code is verified.
It's been my privilege to have taken so much time to consider this particular scruple over the years, and it's why this library is as opinionated and focused as it is. As our team has grown around it, I've seen our consultants use testdouble.js as an educational tool on their client teams, helping developers better express intention in their tests and in turn improve the design of their production code. One of the reasons I love that our company is named Test Double is that this seemingly minor aspect of our craft demonstrates the importance of sweating the small stuff if your aim is to write great software.
As a result of all this, it's no surprise that testdouble.js adoption has been slow and steady—99% of people using a mocking library are simply looking for a hammer to knock real things out with fake things in an effort to get a test to pass. This library was written to suit a much more conscientious workflow and as a result serves a smaller, more discriminating audience. If you haven't before had the chance, there's an incredible opportunity to be found in taking the time to dive in and better understand how thoughtful use of test doubles can improve the design of your code. Consider exploring our wiki on testing or the longer form screencast series on this blog on Discovery Testing or asking me something on twitter.
Most important of all, we hope you'll enjoy the improvements made in today's 2.0 release. If you find working with testdouble.js to be valuable, we'd love to hear about it!