Register to get access to free programming courses with interactive exercises

Monkey patching JS: Advanced Testing

In the previous lesson, we tested the hypothetical getPrivateForksNames(org) function by applying dependency inversion. Let's remind ourselves of the contents of this function in its original form:

import Octokit from '@octokit/rest';

const getPrivateForksNames = async (org) => {
  const client = new Octokit();
  const { data } = await client.repos
    .listForOrg({ // There are side effects
      org,
      type: 'private',
    });
  return data.filter(repo => repo.fork).map(repo => repo.name);
};

In some situations, dependency inversion is a perfect solution, in others, it makes the code much more complicated and sometimes confusing, especially if the dependencies are needed somewhere deep in the call stack (since you have to pass the dependency through all intermediate functions). But there is a way to get to the right calls and change them, without even using dependency inversion.

The JS prototype model allows you to change the behavior of objects without directly accessing them. To do this, just replace the methods in the prototype. After that, any object that has this prototype in any part of the program will start using the new implementation of the method.

// Substituting repos so that the listForOrg method doesn't make a network request
// After executing this code, Octokit changes its behavior not only
// in this module, but also throughout the entire program
Octokit.prototype.repos = {
  listForOrg() {
    console.log('Nothing happens!');
  },
};

// Somewhere in another file
// Since the objects are passed by reference, this is the same Octokit
// as in the code above
import Octokit from '@octokit/rest';

const client = new Octokit();

// The substituted repos is called
client.repos.listForOrg(/* the arguments are unimportant, they're not used internally */);
// => 'Nothing happens!'

When an object (such as a constructor function) is used directly, it's even simpler than with a construction. It's sufficient to change the property of the object itself:

Array.isArray(''); // false

// This code can be called anywhere in the program
Array.isArray = () => true;

Array.isArray(''); // true

// The same applies to any imported object
import Octokit from '@octokit/rest';

// Now whenever Octokit is imported, it will be a modified Octokit
Octokit.boom = () => console.log('Hexlet Magic');

// In any other module
Octokit.boom(); // => 'Hexlet Magic'

This approach, where property values are globally swapped, is called (monkey patching). It's considered bad practice when writing regular code in JS, but it is very popular and handy in tests.

The most famous example in the JavaScript world is the nock library. It overrides real network requests made by the http module, which is included in the standard Node.js library.

// Example http-request using the http module
import http from 'http';

const options = {
  hostname: 'hexlet.io',
  port: 443,
  path: '/my',
  method: 'GET',
};

// request – asynchronous method
const req = http.request(options, (res) => {
  // Here is where the http-response is processed
});

Nock replaces some of the methods inside the http module that are used by different libraries to make HTTP requests.

// Roughly what the substitution looks like

import http from 'http';

// Keeping the old method
// This allows you to put it back.
const overriddenRequest = http.request

http.request = (/* the same parameters as the original method */) => {
  // here's the logic of the nock library

  // Return the original method!
  http.request = overriddenRequest;
}

And an example of how to use it:

import nock from 'nock';
import { getPrivateForkNames } from '../src.js';

test('getPrivateForkNames', async () => {
  nock(/api\.github\.com/) //  this is a regular expression so we don't have to specify the full address
    // get for GET requests, post for POST requests, and so on
    .get(/\/orgs\/hexlet\/repos/)
    .reply(200, [{ fork: true, name: 'one' }, { fork: false, name: 'two' }]);

  const names = await getPrivateForkNames('hexlet');
  expect(names).toEqual(['one']);
});

The chain nock(domain).get(urn) specifies the full address of the page you want to intercept. Nock analyzes all running requests and substitutes only the one that matches the given parameters. The domain and page address can be specified as a whole or through a regular expression, you don't have to write too much.

The method reply(code, body, headers) reply(code, body, headers) defines the response to be returned by the given request. In the simplest case, it's enough to specify the return code. In our situation, we need both code and data. This is the data we use to test the function getPrivateForkNames().

Here we've looked at only the most basic use of Nock. This library has a massive amount of documentation and many use cases. It's useful to review it periodically to find more elegant ways to solve testing problems.

What are the pros and cons of this way of working?

The main advantage is that this way of testing is almost universal. It can be used with any code, and you don't need to edit the code itself. The program won't even know it's being tested.

The disadvantage is that black box testing ends up more like “transparent box” testing. This means that the test knows about the structure of the code being tested and depends on the internals. Such knowledge makes the tests fragile. The function can be changed without losing performance, but the tests will have to be rewritten because they're tied to specific values of the domain, pages, and format of the returned data.

In most situations, this isn't so critical. So feel free to use Nock in your projects, but don't forget about other ways.


Recommended materials

  1. Cassettes for axios

Are there any more questions? Ask them in the Discussion section.

The Hexlet support team or other students will answer you.

About Hexlet learning process

For full access to the course you need a professional subscription.

A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.

Get access
130
courses
1000
exercises
2000+
hours of theory
3200
tests

Sign up

Programming courses for beginners and experienced developers. Start training for free

  • 130 courses, 2000+ hours of theory
  • 1000 practical tasks in a browser
  • 360 000 students
By sending this form, you agree to our Personal Policy and Service Conditions

Our graduates work in companies:

<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.bookmate">Bookmate</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.healthsamurai">Healthsamurai</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.dualboot">Dualboot</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.abbyy">Abbyy</span>
Suggested learning programs
profession
Development of front-end components for web applications
10 months
from scratch
Start at any time

Use Hexlet to the fullest extent!

  • Ask questions about the lesson
  • Test your knowledge in quizzes
  • Practice in your browser
  • Track your progress

Sign up or sign in

By sending this form, you agree to our Personal Policy and Service Conditions
Toto Image

Ask questions if you want to discuss a theory or an exercise. Hexlet Support Team and experienced community members can help find answers and solve a problem.