📜 ⬆️ ⬇️

Mock dependency in node.js applications

Mocks, fakes, and stubs are the three pillars of unit testing. Of course everyone knows what it is, how to salt and when to eat. I honestly thought so too, until I was confronted with reality, under which I had to bend a little.


It all started very simply - I changed my place of work, and the first thing I saw in the new code base was the tests, there were a little more than code. And in the midst of these tests was a strange construction


Component = proxyquire.noCallThru().load('../Component', { '../../core/selectors/common': { getData } }).default; 

For reference:


proxyquire is one of the most popular, and one of the oldest libraries for addicting. In addition to it, inject-loader, rewire, mockery and others are now well known. By "old" is meant that it is designed for the "old" node.js code. No es6 imports.

For those who are in the tank, and do not fully understand what is happening here - everything is very simple. Proxyquire will load us with a modified ../Component, in which one of the imports will be blocked by our stub.


What for? This is a great way to make a unit test a little more unit, reducing the number of uncontrolled external dependencies to zero. Or simply to facilitate testing temporarily removing complex or unnecessary logic from the application.


And everything would be fine, only this particular code did not work. There was an error in the file name, it was necessary to shine one more directory upwards. And the mistake was made for an even more prosaic reason - the project itself used webpack-alias, addressing all files from the root, and the proxyquire worked with the files after passing the babel, where all the paths were relative.


Imagine that you have a code


  import something from 'something/else'; 

It seems to be very simple and understandable, only something is actually something2.default, and the file itself can be anything you like - Babel contains a lot of magic.


That is how this epic began. With attempts to add support for aliases in proxyquire.


1. Decision number 1. First meeting


The first solution that came to mind was to add a little brain to the old proxyquire. His problem is generally very simple.


 Proxyquire.prototype._require = function(module, stubs, path) { //…. if (hasOwnProperty.call(stubs, path)) { var stub = stubs[path]; 

It just needs an exact match between the name of the plug-in and the entries in the list for overload.


./foo, ./foo.js, ../common/foo - this may be one file, but three different lines.

In 5 minutes, some brains were added to this place, and after another 5 minutes one more PR on the githaba was added. After another 5 minutes, he was mercilessly closed, asking not to add unnecessary brains here, but to find another place for them.


2. Decision number 2. Real heroes always go around


In principle, not only the guys from Proxyquire were not happy with the decision. I received a couple of suggestions from the immediate environment for alternative solutions to the problem.


In short, the meaning was simple - it is enough to write a simple function that creates fileName2 from a certain fileName1, so that the first is with an alias, and the second is already in the “correct” form for proxyquire.


Do not try to change the proxyquire itself, but write something like


 proxyquire.load('../Component', addSomeMagic({ 'something/with/alias':{} })) 

so that the correct names come to the proxyquire itself, without aliases, but relative to the source file.


Here it is necessary to dwell on the little magic of proxyquire. Almost nobody wondered why it works.


 Component = proxyquire.noCallThru().load('../Component', { 

Where does proxyquire know the current directory and technically can download a file relative to it? And why in the tests you need to connect the proxyquire directly, or nothing will not work?


The answer is simple - proxyquire uses the information that node.js passes to it through the variable module. The module.parent stores information about who first opened this file. It only remains to remember to erase yourself from the cache of the modular system , so that each time was like ... the first time.


In general, developing a solution that would not require proxyquire modifications introduced me to many of the node.js news.


As a result, I could not do anything - a normal solution required modifications to the source library. Dead end.


3. Decision number 3. The world does not stand still


In parallel, the work was in full swing. And, in trying to make my life a little easier, I tried to improve the original proxyquire, because it was difficult to refuse to use it. I again began to generate pull requests.



In parallel with this, in order to test solutions in practice and to prove their effectiveness in general, work was carried out on



IMHO, I can both recommend you to use any of this list instead of proxyquire, and just get acquainted with the differences in implementation, for a better understanding of the issue.


4. Retrospective


As soon as I defended my version before my colleagues, and at the same time I cured not only my own, but also their long-standing pain , the question arose - what next? Although it is better to ask - what before?


I tried to remember how we mocked dependencies in my former place of work. It was difficult because they did not moss. And in general, a survey of the delta of the surroundings showed a very simple picture of the world.


No one is mocking. sinon, fetch-mock is not considered, it is not interception of dependencies, but local or global variables.

In normal people - normal DI.


Then I tried to remember how it was possible at all to get wet dependencies in the environment of js code execution at my former place of work, where I spent the best years of my youth,



For those who are not familiar with this very interesting CommonJS / AMD compatible solution, originally from Yandex.Maps, there are a couple of presentations about it.


In general, everything was very simple:


 //   ,     module.define('a',['b'], (provide,b) => {}); //   ,     module.require('a'); 

And if you need to block the dependence


  module.define('b',[], (provide) => {}); //   ,     module.define('a',['b'], (provide,b) => {}); //   ,   ,    module.require('a'); 

Simple, and you can even say - naturally.


How do different mocking solutions work on the modular system native to node.js?



The difference is very simple:



And everywhere there are some nuances, implementations that complicate life a little:



5. Final decision


So - I had my own proxyquire, knowledge of dozens of other libraries, analysis of the problem from different sides and a good understanding of the technical problem. The goal was simple - one ring to rule them all.


As the original version, the most satisfying feeling of beauty was taken by mockery .
Why:



The result was the library rewiremock , which satisfies all my Wishlist, and from the box can solve all problems.


 import rewiremock from 'rewiremock'; ... // totaly mock `fs` with your stub rewiremock('fs') .with({ readFile: yourFunction }); // replace path, by other module rewiremock('path') .by('path-mock'); // replace default export of ES6 module rewiremock('reactComponent') .withDefault(MockedComponent) // replace only part of some library and keep the rest rewiremock('someLibrary') .callThought() .with({ onlyOneMethod }) … rewiremock.enable(); rewiremock.isolation(); rewiremock.passBy(/node_modules/); // use native require const module = require('../core/somemodule'); // or add some magic…. const module = require(rewiremock.resolve('core/module')); 

It allows you to define mocks in separate files, cleverly cuts cache only for overlapped files (and the files that use them), supports RegEx in the passBy isolation setup, has an extremely simple api and will always work as required.


The whole secret is in the plugins that allow you to control the behavior of the library in the right places.


I think you shouldn’t describe the implementation details, everyone can dig into the code or the tests themselves, and at the same time express anything at all through comments.
Or pullrequests, I promise to listen.


→ https://github.com/theKashey/rewiremock


This is how one small splinter, simply not accepting the “crookedness” of an actively used tool, led to the discovery of micronish of mocks, a much deeper understanding of the issue, to the 4th new libraries. And maybe to a little bit better world.


PS: There are no complaints about the proxyquire authors. Why? https://habrahabr.ru/post/328412/

')

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


All Articles