SkillAgentSearch skills...

Introscope

Automated mocking and spying tool for delightful ES6 modules testing

Install / Use

/learn @peter-leonov/Introscope
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Introscope

A babel plugin and a set of tools for delightful unit testing of modern ES6 modules. It allows you to override imports, locals, globals and built-ins (like Date or Math) independently for each unit test by instrumenting your ES6 modules on the fly.

Scope example

scope example

inc.js

const ONE = 1; // notice, not exported
export const inc = a => a + ONE;

// @introscope "enable": true

inc.test.js

import { introscope } from './inc';

test('inc', () => {
    const scope = introscope();

    expect(scope.inc(1)).toBe(2);
    scope.ONE = 100; // just for lulz
    expect(scope.inc(1)).toBe(101);
});

Effects example

effects example

abc.js

const a = () => {};
const b = () => {};
const c = () => {};

export const abc = () => {
    a(1);
    b(2);
    c(3);
};

// @introscope "enable": true

abc.test.js

import { effectsLogger, SPY } from 'introscope/logger';
import { introscope } from './abc';

const loggedScope = effectsLogger(introscope);

test('abc', () => {
    const { scope, effects } = loggedScope({
        a: SPY,
        b: SPY,
        c: SPY,
    });

    scope.abc();

    expect(effects()).toMatchSnapshot();
});

Recorder example

recorder example

tempfile.js

const now = () => Date.now();
const rand = () => Math.random();

export const tempfile = () => `/var/tmp/${now()}-${rand()}`;

// @introscope "enable": true

tempfile.test.js

import { effectsLogger, RECORD } from 'introscope/logger';
import { introscope } from './tempfile';

const recordedScope = effectsLogger(introscope);

test('tempfile', () => {
    const { scope, recorder } = recordedScope({
        now: RECORD,
        rand: RECORD,
    });

    expect(scope.tempfile()).toMatchSnapshot();

    recorder.save();
});

tempfile.test.js.record

[['now', 1539533182792], ['rand', 0.20456280736087873]];

What so special?

Intoscope is yet another mocking tool, but with much higher level of control, isolation and performance:

  • easily test any stateful module: on every run you get a fresh module scope;
  • test faster with a fresh module in each test: no need to reset mocks, spies, logs, etc;
  • faster module loading: remove or mock any heavy import on the fly;
  • intercept any top level variable definition: crucial for higher order functions;
  • spy or mock with any tool: introscope() returns a plain JS object;
  • easy to use: optimized for Jest and provides well fitting tooling;
  • type safe: full support for Flow in your tests;
  • simple to hack: just compose the factory function with your plugin.

See what Introscope does with code in playground.

Known issues

Support for TypeScript using Babel 7 is planned.

Please, see a short ☺️ list here: issues labeled as bug

Longer description

TL;DR; no need to export all the functions/variables of your module just to make it testable, Introscope does it automatically by changing the module source on the fly in testing environment.

Introscope is (mostly) a babel plugin which allows a unit test code look inside an ES module without rewriting the code of the module. Introscope does it by transpiling the module source to a factory function which exposes the full internal scope of a module on the fly. This helps separate how the actual application consumes the module via it's exported API and how it gets tested using Introscope with all functions/variables visible, mockable and spy-able.

It has handy integration with Jest (tested with Jest v24.5.0 and Babel v7.4.0) and Proxy based robust spies. Support for more popular unit testing tools to come soon.

Usage

Installation from scratch looks like the following.

First, install the Jest and Babel powered test environment together with Introscope:

yarn add -D jest babel-jest @babel/core @babel/preset-env introscope
# or
npm install -D jest babel-jest @babel/core @babel/preset-env introscope

Second, edit .babelrc like this:

{
    "presets": [["@babel/preset-env", { "targets": { "node": "current" } }]],
    "plugins": ["introscope/babel-plugin"]
}

The perameters to the @babel/preset-env preset are needed to make async/await syntax work and are not relevant to Introscope, it's just to make the modern JS code running.

Third, add this magic comment to the end of the module (or beginning, or anywhere you like) you are going to test:

// @introscope "enable": true

There is a way to avoid adding the magic comment, but it's fairly unstable and works only for older versions of Jest. If you badly hate adding little cure magic comments to your modules, please, help Introscope with making Jest team to get #6282 merged.

Done! You're ready to run some test (if you have any 😅):

yarn jest
# or
npm run env -- jest

Start using Introscope in tests:

import { introscope } from './tested-module';

// or using common modules
const { introscope } = require('./tested-module');

For safety reasons this plugin does nothing in non test environments, e.g. in production or development environment it's a no-op. Jest sets NODE_ENV to 'test' automatically. Please, see your favirite test runner docs for more.

Introscope supports all the new ES features including type annotations (if not, create an issue 🙏). That means, if Babel supports some new fancy syntax, Introscope should do too.

Detailed example

What Introscope does is just wraping a whole module code in a function that accepts one object argument scope and returns it with all module internals (variables, functions, classes and imports) exposed as properties. Here is a little example. Introscope takes module like this:

// api.js
import httpGet from 'some-http-library';

const ensureOkStatus = response => {
    if (response.status !== 200) {
        throw new Error('Non OK status');
    }
    return response;
};

export const getTodos = httpGet('/todos').then(ensureOkStatus);

// @introscope "enable": true

and transpiles it's code on the fly to this (comments added manually):

// api.js
import httpGet from 'some-http-library';

// wrapps all the module source in a single "factory" function
export const introscope = function(_scope = {}) {
    // assigns all imports to a `scope` object
    _scope.httpGet = httpGet;

    // also assigns all locals to the `scope` object
    const ensureOkStatus = (_scope.ensureOkStatus = response => {
        if (response.status !== 200) {
            // built-ins are ignored by default (as `Error` here),
            // but can be configured to be also transpiled
            throw new Error('Non OK status');
        }
        return response;
    });

    // all the accesses to locals get transpiled
    // to property accesses on the `scope` object
    const getTodos = (_scope.getTodos = (0, _scope.httpGet)('/todos').then(
        (0, _scope.ensureOkStatus),
    ));

    // return the new frehly created module scope
    return _scope;
};

You can play with the transpilation in this AST explorer example.

The resulting code you can then import in your Babel powered test environment and examine like this:

// api.spec.js

import { introscope as apiScope } from './api.js';
// Introscope exports a factory function for module scope,
// it creates a new module scope on each call,
// so that it's easier to test the code of a module
// with different mocks and spies.

describe('ensureOkStatus', () => {
    it('throws on non 200 status', () => {
        // apiScope() creates a new unaltered scope
        const { ensureOkStatus } = apiScope();

        expect(() => {
            ensureOkStatus({ status: 500 });
        }).toThrowError('Non OK status');
    });
    it('passes response 200 status', () => {
        // apiScope() creates a new unaltered scope
        const { ensureOkStatus } = apiScope();

        expect(ensureOkStatus({ status: 200 })).toBe(okResponse);
    });
});

describe('getTodos', () => {
    it('calls httpGet() and ensureOkStatus()', async () => {
        // here we save scope to a variable to tweak it
        const scope = apiScope();
        // mock the local module functions
        // this part can be vastly automated, see Effects Logger below
        scope.httpGet = jest.fn(() => Promise.resolve());
        scope.ensureOkStatus = jest.fn();

        // call with altered environment
        await scope.getTodos();
        expect(scope.httpGet).toBeCalled();
        expect(scope.ensureOkStatus).toBeCalled();
    });
});

Effects Logger

This module saves 90% of time you spend writing boiler plate code in tests.

Effects Logger is a nice helping tool which utilises the power of module scope introspection for side effects logging and DI mocking. It reduces the repetitive code in tests by auto mocking simple side effects and logging inputs and outputs of the tested function with support of a nicely looking custom Jest Snapshot serializer.

Example:

// todo.js
const log = (...args) => console.log(...args);
let count = 0;
const newTodo = (id, title) => {
    log('new todo created', id);
    return {
        id,
        title,
    };
};
const addTodo = (title, cb) => {
    cb(newTodo(++count, title));
};
// @introscope "enable": true

// todo.spec.js
import { introscope } from './increment.js';
import { effectsLogger, SPY, KEEP } from 'introscope/logger';

// decorate introscope with effectsLogger
const effectsScope = effectsLogger(introscope);

describe('todos', () => {
    it('addTodo

Related Skills

View on GitHub
GitHub Stars97
CategoryDevelopment
Updated4mo ago
Forks1

Languages

JavaScript

Security Score

92/100

Audited on Nov 11, 2025

No findings