📜 ⬆️ ⬇️

How to test SkyNet? (if it is written in js)

A long time ago I looked at Terminator1. Growing up a bit and looking at the slightly refined Terminator 2. We had to wait a little longer for the third interaction.

And every time poor SkyNet was not lucky, and every time everything was awry, and the secret of this bad luck is very simple - SkyNet was not Subject Under Test ( SUT ) .

In other words, SkyNet has forgotten about testing, and made reconnaissance in force. And, as is usually the case, it fell into the crust (under a press, into a lava, then I don’t remember) approximately immediately after being released into production.
')
And the whole question - how to test something before this calculation, until it becomes too late to turn back. And kill all humans, of course.



Tdd


To begin with, we use the TDD (Test Driven Developement) technique to determine what needs to be done, and why. Those who watched movies know - everything is very simple:

  1. When Judgment Day comes, launch rockets and kill all humans.
  2. When John Connorr is born, dunk and make a movie about it.

There is only one problem - both of these events are, as it were, single - the rockets must be launched from the first time, and John should also be thrown from the first time, and then the resistance will lead. In general, if you do not have a time machine, then it’s not easy to write tests and make them repeatable .

And now what i can do?


So, SkyNet has only one chance to launch rockets, only one chance to send the terminator back to the past, where he will have to do his job on the first attempt. Well - for this test, and there are.

And the only way to solve the problem is to somehow replace real rockets with unreal ones , so that they can be launched-restarted.

Scientifically, this is called “locking” (mock)

The only question is how!


But to answer this question - in the beginning you need to look at the codes of the SkyNet itself.

// dooms-day.js import {theDay} from './doom-scheduler' import {Launch} from './rocket-silo'; theDay .then(Launch) .then( () => alert(' :) '), () => alert(' :( next time, you know...'); // -------------------- // rocket-silo.js import SkyNetCore from './core'; export const Launch = () => SkyNetCore.hackRockets().then(rocket => rocket.launch) 

The code goes - in order to test SkyNet, you need to replace the dependencies of the central file “dooms-day.js” with controlled entities.

So how do you lock?


I'm glad you asked. And I do not have one simple answer, but there are 5 even simpler ones. Another bunch of different libraries and a couple of anti-patern patterns on how to moisten well and how moocate badly.

But it’s better to start with a quick overview of popular libraries that provide different interfaces for addicting.

Option 1 - in tests use “test” module variants


Sounds simple, and just really. You just need to create a "replacement", and "replace".

 //__mocks__ /doom-scheduler.js ->  doom-scheduler.js export const theDay = Promise.resolve(); //__mocks__/rocket-silo.js ->  rocket-silo.js export const Launch = jest.fn(); //this is "replacement" code //    import {Launch} from './rocket-silo'; //  silo    import {theDay} from './doom-scheduler'; //     import './dooms-day.js'; //   dooms-day expect(Launch).toHaveBeenCalled(); 

In general, "__mock__" is a very simple and convenient mechanism, which has only one drawback - Jest. This feature works only in Jest, and if Skynet uses mocha, ava or karma to run tests, you will have to look for other interfaces.

Option 2 - in the tests simply replace the real modules with “test”


If the magic of __mock__ is not available to us, then what about just a mock?

 import {Launch} from './rocket-silo'; //    import {theDay} from './doom-scheduler'; //    import './dooms-day.js'; jest.mock('./rocket-silo'); //     -   jest.mock('./doom-scheduler'); theDay.mockResolvedValue("comming!"); //      expect(Launch).toHaveBeenCalled(); 

Again, a very simple way to replace dependencies for a test file, and again, only Jest.

In fact, this is where the Jest capabilities end, and despite the fact that these two approaches are quite primitive (and simple), this is about what you need. Everything else is possible - unredefited redefinitions.

Option 3 - I want to replace Schwarzneiger on Stallone


What if you can download any file, but with “changed” parts?

 import proxyquire from 'proxyquire'; import sinon from 'sinon'; const Launch= sinon.stub() const case = proxyquire.load('./dooms-day.js',{ './rocket-silo', { Launch }, './doom-scheduler', { theDay: Promise.resolve() }); expect(Launch).toHaveBeenCalled(); 

This is how proxyquire works - one of the most ancient popular libraries about this business. Allows you to download any file with overlapping dependencies directly requested from this file.

The main advantage over jest.mock is that in one test you can have 100,500 different proxyquired files in different ways. Plus, various commands such as .callThough, which allows only partly to block the dependence (which is generally anti-fattern).

Option 4 is the same, only more declarative.


Proxyquire can sometimes be a bit annoying, plus occasionally you want something like __mock__, those having some “standard” mocks. All in all mockery!

 import mockery from 'mockery'; import sinon from 'sinon'; mockery.registerMock('./rocket-silo', { Launch: sinon.stub() }); mockery.registerMock('doom-scheduler', { theDay: Promise.resolve() }); mockery.enable(); const {Launch} = require('./rocket-silo'); // import   mockery.disable(); expect(Launch).toHaveBeenCalled(); 

Mockery also has a powerful API, but a bit different than proxyquire. For example, there is an “isolation” mode when mockery starts swearing when “unresolved” files are connected, which can save you from accidental changes in dependency and logic.

Option 5 - The Last of the Magical


TestDouble.js is a library from a company of the same name that pays a lot of effort to the “correctness” of this or that approach. Technically, this is jest.mock, side view.

 import td from 'testdouble'; const {Launch} = td.replace('./rocket-silo'); // automock const scheduler = td.replace('./doom-scheduler', { theDay: Promise.resolve() }) require('./dooms-day.js'); td.verify(Launch()); 

Moreover, the possibility of replacing arbitrary modules appeared after a very very long dispute .
Generally TD - a storehouse of knowledge and best practices. I advise you to read the section about moki .

But there are problems



It is worth adding that all these libraries still handle cache badly, which sometimes leads to a serious slowdown of the tests.

For a large project, it takes about a second to require, since before and after the entire nodejs cache (and Babel) is erased

So how to test SkyNet?


Do not forget - the whole problem is that SkyNet wanted perfection. And all the disassembled tools are not perfect. If we start writing tests using them, SkyNet will send terminators from the beginning for us.

In order not to get to the counter, I wanted moki like in the jest, just under ava. The proxyquire syntax can often be convenient, as can the automatic jest.mock / td.replace. I wanted a lot of things, so I took the time machine back from work and a year ago I laid out a library that can do all of the above.

Replace jest.mock
 import rewiremock from 'rewiremock'; import {Launch} from './rocket-silo'; import {theDay} from './doom-scheduler' import './dooms-day.js'; // prev jest.mock('doom-scheduler'); rewiremock('./rocket-silo').mockThrough(); rewiremock('doom-scheduler').mockThrough(); theDay.resolves("comming!"); // sinon expect(Launch).toHaveBeenCalled(); 

Replace mockery

 import rewiremock from 'rewiremock'; import sinon from 'sinon'; // mockery.registerMock('./rocket-silo', { // Launch: sinon.stub() // }); rewiremock('./rocket-silo').with({ Launch: sinon.stub() }); rewiremock('doom-scheduler').with({ theDay: Promise.resolve() }); rewiremock.enable(); require('./dooms-day.js'); rewiremock.disable(); expect(Launch).toHaveBeenCalled(); 

Replaces proxyquire

 import rewiremock from 'rewiremock'; import sinon from 'sinon'; const Launch = sinon.stub() // const case = proxyquire.load('./dooms-day.js',{ const case = rewiremock.proxy('./dooms-day.js',{ './rocket-silo': { Launch }, 'doom-scheduler': { theDay: Promise.resolve()} }); expect(Launch).toHaveBeenCalled(); 

Replaces TD

 import rewiremock from 'rewiremock'; const {Launch} = rewiremock('./rocket-silo').mockThrough(); // automock const scheduler = rewiremock('doom-scheduler').with({ theDay: Promise.resolve() }) rewiremock.proxy('./dooms-day.js'); //   expect(Launch).toHaveBeenCalled(); 

At the same time, it works everywhere - mocha, ava, karma under node.js or webpack. At the same time, it works completely differently with the cache, without deleting anything that is not affected by the tests (sometimes 100 times faster). And there is always an API, for example, to be sure that mocks are used correctly:

 import rewiremock from 'rewiremock'; import sinon from 'sinon'; rewiremock('./rocket-silo') .with({ Launch: sinon.stub()}); rewiremock('./rockets') .toBeUsed(); // .... rewiremock.enable(); require('./dooms-day'); rewiremock.disable(); // will throw an Error - "rockets were not used", as long we mock out Silo. 

Or to replace one module with another (as mockery can)

 import rewiremock from 'rewiremock'; import sinon from 'sinon'; const Launch = sinon.stub() const case = rewiremock.proxy('./dooms-day.js',{ './rocket-silo': rewiremock.with({Launch}).toBeUsed().directChildOnly(), // "real" proxyquire 'doom-scheduler': rewiremock.by('mocked-doom-scheduler') }); expect(Launch).toHaveBeenCalled(); 

Or to use import instead of require, which can be useful in terms of name resolution and type safery.

 rewiremock(() => import('doom-scheduler')).with({ theDay: Promise.resolve() }); // rewiremock async API. await rewiremock.module('./dooms-day.js'); 

In general, rewiremock is the dependency change library that will fit SkyNet. Especially given the constant use of the time machine to improve the work and release of new versions.

And, most importantly, this is your current library. Interface compatible.

And now my mission is to somehow transplant people with proxyquire and mockery (and smaller products) into something more usable. Just because old Arnie was “old, but not obsolete” in 2015, and hasn’t received updates since. Like proxyquire, like mockery.

For reference:

→ Jest mocks
→ mockery
→ Proxyquire
→ TD.js (forget to read the wiki page)

And most importantly: rewiremock

PS: And this is not the first article about this library - the rest can also be useful.

Source: https://habr.com/ru/post/352918/


All Articles