Top-Level function cannot be stubbed if it is exported by an asterisk

eye-catch JavaScript/TypeScript

A top-level function is not a part of an object. If we need to stub it, we need to have an object that has the target function. There are two patterns. The target function is defined in the same module or in a different module.

The way to stub the top-level function depends on it.

Sponsored links

The target functions to stub

The target functions that we want to stub are the following. I prepared 4 different patterns to check whether the behavior is the same or not. As a result of my test, there is no difference between the definitions.

// TopLevelFuncStub.ts
export interface Result {
    prop1: number;
    prop2: number;
}
export const constReturn1 = (): number => {
    return 1;
};

export const constReturnObj = (): Result => {
    return {
        prop1: 11,
        prop2: 22,
    };
};

export function functionReturn1(): number {
    return 1;
}

export function functionReturnObj(): Result {
    return {
        prop1: 11,
        prop2: 22,
    };
};

You can find the code above here.

Sponsored links

How to stub top-level function in the same module

Firstly, let’s stub the top-level functions in case it is defined in the same module. To stub a function, it must be a part of an object. The top-level function is not part of a function, so we need to put it into an object.

Let’s see the test code. The test code can be found here.

// This is wrong code

import "mocha";
import { expect } from "chai";
import * as sinon from "sinon";
import {
    constReturn1,
    constReturnObj, functionReturn1, functionReturnObj
} from "../lib/TopLevelFuncStub";

describe("TopLevelFuncStub", () => {
    afterEach(() => {
        sinon.restore();
    });

    let obj = {
        constReturn1: constReturn1,
        functionReturn1: functionReturn1,
        constReturnObj: constReturnObj,
        functionReturnObj: functionReturnObj,
    };

    describe('constReturn1', () => {
        [
            () => sinon.stub(obj, "constReturn1").returns(55),
            () => sinon.stub(obj, "constReturn1").get(() => 55),
            () => sinon.stub(obj, "constReturn1").callsFake(() => 55),
        ].forEach((stubFunc) => {
            it("should not be able to stub", () => {
                stubFunc();
                // The behavior is replaced
                expect(obj.constReturn1()).to.equal(55);
                // But this one not
                expect(constReturn1()).to.equal(1);
            });
        });
    });
});

In this way, the function can be stubbed but it doesn’t make sense because obj is not used in the production code. It is just newly created in the test code to use in it.

To replace the actual behavior in the production code, we need to write in the following way.

import "mocha";
import { expect, use } from "chai";
import * as sinon from "sinon";
import * as TopLevel from "../lib/TopLevelFuncStub";

describe("TopLevelFuncStub", () => {
    afterEach(() => {
        sinon.restore();
    });

    describe('constReturn1', () => {
        [
            () => sinon.stub(TopLevel, "constReturn1").returns(55),
            () => sinon.stub(TopLevel, "constReturn1").get(() => sinon.stub().returns(55)),
            () => sinon.stub(TopLevel, "constReturn1").callsFake(() => 55),
        ].forEach((stubFunc) => {
            it("should not be able to stub", () => {
                stubFunc();
                expect(TopLevel.constReturn1()).to.equal(55);
            });
        });
    });
});

The key is to import all by an asterisk. Then, we can stub the target function with the object.

import * as TopLevel from "../lib/TopLevelFuncStub";
sinon.stub(TopLevel, "constReturn1").returns(55);

Let’s try to call the same function in a different way.

import * as TopLevel from "../lib/TopLevelFuncStub";
import { constReturn1 } from "../lib/TopLevelFuncStub";

describe("TopLevelFuncStub", () => {
    afterEach(() => {
        sinon.restore();
    });

    describe('constReturn1', () => {
        [
            () => sinon.stub(TopLevel, "constReturn1").returns(55),
            () => sinon.stub(TopLevel, "constReturn1").get(() => sinon.stub().returns(55)),
            () => sinon.stub(TopLevel, "constReturn1").callsFake(() => 55),
        ].forEach((stubFunc) => {
            it("should not be able to stub", () => {
                stubFunc();
                expect(constReturn1()).to.equal(55);
                expect(TopLevel.constReturn1()).to.equal(55);
            });
        });
    });
});

Both validations pass.

expect(constReturn1()).to.equal(55);
expect(TopLevel.constReturn1()).to.equal(55);

In this way, we can stub the top-level function.

Note that, we get the following error if we try to stub it in the following way.

// TypeError: TopLevel.constReturn1 is not a function
sinon.stub(TopLevel, "constReturn1").get(() => 55),

How to stub top-level function defined in a different module

You can find the code here.

To come to the point, the key is not to use an asterisk to export the top-level functions.

// index.ts

// Impossible to stub in this way
export * from "./lib/TopLevelFuncStub";

// Possible to stub
export {
    constReturn1,
    functionReturn1,
    constReturnObj,
    functionReturnObj,
} from "./lib/TopLevelFuncStub";

Firstly, let’s check the test code. The functions are defined in common module.

import "mocha";
import { expect, use } from "chai";
import * as sinon from "sinon";
// Import the target functions from another moudle
import * as TopLevel from "common";

describe("TopLevelFuncStub", () => {
    const result = {
        prop1: 99,
        prop2: 88,
    };

    afterEach(() => {
        sinon.restore();
    });

    describe('constReturn1', () => {
        it("should be able to stub", () => {
            sinon.stub(TopLevel, "constReturn1").get(() => sinon.stub().returns(55));
            expect(TopLevel.constReturn1()).to.equal(55);
        });

        [
            () => sinon.stub(TopLevel, "constReturn1").returns(55),
            () => sinon.stub(TopLevel, "constReturn1").callsFake(() => 55),
        ].forEach((stubFunc) => {
            it("should not be able to stub", () => {
                stubFunc();
                expect(TopLevel.constReturn1()).to.equal(1);
            });
        });
    });
});

I defined other functions’ tests as well. Let’s see the test result when exporting the functions by an asterisk. Namely, when the index.ts is the following.

export * from "./lib/TopLevelFuncStub";

The test result is the following.

  TopLevelFuncStub
    constReturn1
      1) should be able to stub
      √ should not be able to stub
      √ should not be able to stub
    functionReturn1
      2) should be able to stub
      √ should not be able to stub
      √ should not be able to stub
    constReturnObj
      3) should be able to stub
      √ should not be able to stub
      √ should not be able to stub
    functionReturnObj
      4) should be able to stub
      √ should not be able to stub
      √ should not be able to stub

  8 passing (108ms)
  4 failing

The functions should be able to stub but actually not.

Next, in the case when exporting the functions by name.

export {
    constReturn1,
    constReturnObj,
    functionReturn1,
    functionReturnObj
} from "./lib/TopLevelFuncStub";

The result is the following.

  TopLevelFuncStub
    constReturn1
      √ should be able to stub
      √ should not be able to stub
      √ should not be able to stub
    functionReturn1
      √ should be able to stub
      √ should not be able to stub
      √ should not be able to stub
    constReturnObj
      √ should be able to stub
      √ should not be able to stub
      √ should not be able to stub
    functionReturnObj
      √ should be able to stub
      √ should not be able to stub
      √ should not be able to stub


  12 passing (66ms)

All tests passed!!

The difference between asterisk and named export

We saw the test result was different. We tried the two ways to export the functions. But… what’s the difference between the two? Let’s check the transpiled code.

Asterisk export

This is the transpiled code of index.ts for asterisk export.

// index.js

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
    for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./lib/TopLevelFuncStub"), exports);

It’s hard to follow the asterisk one at first look. Let’s have a deeper look at it. It calls the binding function with the following arguments for one of the calls because the for-in loop provides the property names of the object.

var TopLevelFuncStub_1 = require("./lib/TopLevelFuncStub");
__createBinding(exports, TopLevelFuncStub_1, "constReturn1");

Then, k2 = k = "constReturn1". If Object.create is defined in Object, the following code is executed.

Object.defineProperty(exports, "constReturn1", { enumerable: true, get: function() { return TopLevelFuncStub_1["constReturn1"]; } });

Named export

This is for named export.

// index.js

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.functionReturnObj = exports.functionReturn1 = exports.constReturnObj = exports.constReturn1 = void 0;
var TopLevelFuncStub_1 = require("./lib/TopLevelFuncStub");
Object.defineProperty(exports, "constReturn1", { enumerable: true, get: function () { return TopLevelFuncStub_1.constReturn1; } });
Object.defineProperty(exports, "constReturnObj", { enumerable: true, get: function () { return TopLevelFuncStub_1.constReturnObj; } });
Object.defineProperty(exports, "functionReturn1", { enumerable: true, get: function () { return TopLevelFuncStub_1.functionReturn1; } });
Object.defineProperty(exports, "functionReturnObj", { enumerable: true, get: function () { return TopLevelFuncStub_1.functionReturnObj; } });

Basically, it’s the same as asterisk export. There is no function but the functions are processed one by one.

Let’s check the final code of the asterisk export again.

Object.defineProperty(exports, "constReturn1", { enumerable: true, get: function() { return TopLevelFuncStub_1["constReturn1"]; } });

It looks the same to me.

But wait!! What’s this?

exports.functionReturnObj = exports.functionReturn1 = exports.constReturnObj = exports.constReturn1 = void 0;

If I remove this line from the index.js, the test fails. The result was the same as the one for asterisk export.

When I searched for void 0, it turns out that it means undefined. If I replace the code with the following, the all test passed.

exports.functionReturnObj = undefined;
exports.functionReturn1 = undefined;
exports.constReturnObj = undefined;
exports.constReturn1 = undefined;

The code defines the properties in the object with undefined behavior.

Namely, the difference is whether or not the function’s behavior is defined in a property of the object.

That’s why the function can be stubbed when exporting the top-level functions by name.

Conclusion

Export the functions by name if you need to stub the functions. The format is the following.

// index.ts
export {
    function1,
    function2,
    ...
} from "ModuleName";

Comments

Copied title and URL