Unit Tests Are Production Code

Anton Korzunov
Hackmamba
Published in
5 min readAug 6, 2017

--

But if it is so — Who will test your tests?

Who watches the watchmen?

Why we are writing tests?

It is ok not to write tests, it is bearable. But lets assume that we good programers and we are writing them. Unfortunately a lot of people do writing tests for writing tests, to obtain achievement called code coverage.

Some other people are writing tests in search of mysterious confidence. A lot of them never see that confidence. Or, by the time one need it — creators may already left the company.

Lets throw away theory and town legends, and recall the only thing tests are designed to do:

Tests are concreting actual realization,

usually — the right cases.

Nothing more.

Whats the difference between good and bad tests?

Good tests will fail as soon as possible. Bad tests can skip something.

Good tests will fail in case something goes in an unexpected way. If you expect few moments to be flexible — it is ok. But flexible tests — are bad(and flaky) tests.

Good tests will reveal the reson, the place where something enexpected occurs. Bad tests will only notify you — “test fails”. Which test, Why, Where?

So, good tests will stop in point, closest to the true root of the change.

Ok then?

In short, 99% tests, test libraries and test frameworks enable you to write good tests. Usually.

But testdoubles, especially mocks, a very important part of tests, would violate this proposition.

Mocking libraries — are bad libraries. From Good Tests point of view.

Good tests are very picky :(

Example

You have file A, which uses file B. You mocked file B and wrote a test:

// a.js
import MagicFunction from 'b.js';
export default () => MagicFunction() ? 1 : 2;// a.spec.jswhen(MagicFunction) // this function
.from('b.js') // from this file
.mockedBy(MyMock) // replaced by this mock
.then(a.js) // then default export from this file
.returns(1); // should return 1

Unfortunately this syntax does not exists, and you will have to overload b.js with something existing, like proxyquire, Jest, mockery or other library*.

And then (a week later)NOT-you will refactor a.js and add a new dependency — c.js

// a.js
import MagicFunction from 'b.js';
import MoreMagic from 'c.js'; <-- new
export default () => MoreMagic(MagicFunction()) ? 1 : 2;

50% — tests will NOT fall. Probably even 90% that test will be green, but not it’s not quite clear what you are testing now.

And then(a month later) somebody else, refactor a.js and replace b.js by d.js.

// a.js
import NewMagicFunction from 'd.js'; <-- replacement
import MoreMagic from 'c.js';
export default () => MoreMagic(NewMagicFunction())? 1 : 2;

This time test might probably fail, as long as we are not mocking anything. And if they do — you will see message about some expectation has fallen.

But this is not the root issue, the “source of the change”. This is a side effect.

The root issue— You’ve rewired sources. Mock is no longer in use.

No more. No less.

The 0.01%

To solve the first issue(adding new dep) mocking library must throw an exception, when something new was used. It is called an isolation.

To solve the second issue(removing a dep) mocking library must throw an exception, when something, assumed to be used, — was unused. It’s called reverse isolation.

To solve the third issue(which I didn’t name yet), you have to clearly understand what, when and where you are mocking.

  • If you are mocking b.js, you are mocking not the b.js, but a dependency of a.js with same name.
  • If you are mocking entire filesystem — you are mocking not dependency fs of a.js, but any fs, used anywhere. It’s good to mock-out disk and network during test, to make tests repeatable.
  • And if you are mocking a dependency of a dependency — it is better to clearly specify you wishes. Define what you actually want to mock.

That means that you have to have some control upon you wishes….

Mocking libraries

The mocking library must empower you with three tools:

  • The mocking. Give you ability to mock something. The “mock”.
  • The assurance. To do it with confidence. The “isolation”.
  • The control. Ability to be more “picky”.

Lets check popular ones:

  • Proxyquire: can mock only direct dependencies, with some control, but no assurance.
  • Jest: can mock any dependency, with no control and no assurance.
  • Rewire: can mock only direct dependencies, with no control, and no assurance.
  • Mockery: can mock any dependency, with no control and some assurance.

So — from the list of existing libraries — there is no “perfect” library. Proxyquire and mockery have got more “score” than Jest, but still not feature complete. And rewire, to be honest, is not a mocking library.

Mockery for each run will purge the whole node.js cache, and isolation mode(warnOnUnregistered) is actually not working. So, proxyquire is the best, but… no, it is not.

The Solution

I was barely surprised, when I found that existing libraries are not just far from ideal, but they are just wrong. I mean — “from a good tests prospective”.

So I had to fix it

For the first time I was trying to fix proxyquire, but it decided to be not fixed.

I have another story about it —

So I decided to create a new mocking library. The Right One. The solution.

Rewiremock

This is not an article about rewiremock, but this is the article about how rewiremock solves highlighted issues.

  1. To seal some module, and be informed when new dependency added:
rewiremock.enableIsolation();
// any unmocked dependency would throw

If you want some files to be transparent to isolation mode, ie dont have to be mocked:

rewiremock.passBy(/React/);

2. If you want to secure the usage of a dependency:

rewiremock('b.js')
.with(something)
.toBeUsed(); // will throw an Error if it is not
// or just enable this behaviour by default
import { usedByDefault } from 'rewiremock/plugins';
rewiremock.addPlugin(usedByDefault);

3. If you want to mock only dependencies of a.js, the entry point of a test.

rewiremock('b.js')
.directChildOnly();
// ^ would mock only `b.js` requested from `a.js`, if we are testing a.jsrewiremock('fs')
.atAnyPlace();
// or - "anywhere"

3.1 Or, a rare case to be honest, a dependency used from patially mocked, but still “controlled” location?

rewiremock('b.js').callThrough(); // "partial" mockrewiremock('fs')
.calledFromMock(); // or .calledFromAnywhere
// ^ would mock `fs` for `b.js`(mocked) only

So — rewiremock is not just capable to mock depencies — it is capable to do it in a right way.

Rewiremock: can mock any dependency, with full control and maximal assurance.

If you got the point — may be you should get rewiremock. Working with nodejs, webpack and ESM modules.

--

--