Reputation: 133
I am migrating a set of Firebase Cloud Functions from Javascript to Typescript. With JS I was able to unit test with mocha, chai, and sinon and stub out various database dependencies for testing. With TS I am running into problems and unexpected behavior I don't understand.
The pattern is something like:
mainFunction
calls helperFunction
which calls nestedHelperFunction
.
I want to stub or spy on nestedHelperFunction
in my tests when I call mainFunction
with some test data.
mainFunction
is in an index.ts
and helperFunction
and nestedHelperFunction
are in something like a utils.ts
file.
Example of odd behavior
// index.ts
import * as utils from './utils';
export async function mainFunction() {
console.log('Starting mainFunction...');
const promiseResults = await Promise.all([
Promise.resolve('One'),
utils.helperFunction(),
Promise.resolve('Three'),
]);
console.log(promiseResults);
return 1;
}
// utils.ts
export async function helperFunction() {
const newString = await nestedHelperFunction();
return 'helperFunction Result | ' + newString;
}
export async function nestedHelperFunction() {
return '***Nested***';
}
Test file
//index.test.ts
import * as myFunctions from '../index';
import * as utils from '../utils';
import * as sinon from 'sinon';
import * as chai from 'chai';
const expect = chai.expect;
describe("Test Suite", () => {
let functionSpy: sinon.SinonStub;
beforeEach(() => {
functionSpy = sinon.stub(utils, 'helperFunction');
functionSpy.returns(Promise.resolve('Stubbed Function Results!'))
});
afterEach(() => {
functionSpy.restore();
});
it('should resolve and call the correct functions.', async () => {
const returnValue = await myFunctions.mainFunction();
expect(returnValue).to.equal(1);
expect(functionSpy.callCount).to.equal(1);
})
})
Output:
Tests pass and I get : [ 'One', 'Stubbed Function Results!', 'Three' ]
However, If I try to stub the nestedHelperFunction
it does not work.
// index.test.js
import * as myFunctions from '../index';
import * as utils from '../utils';
import * as sinon from 'sinon';
import * as chai from 'chai';
const expect = chai.expect;
describe("Test Suite", () => {
let functionSpy: sinon.SinonStub;
beforeEach(() => {
functionSpy = sinon.stub(utils, 'nestedHelperFunction'); // Changed
functionSpy.returns(Promise.resolve('Stubbed Function Results!'))
});
afterEach(() => {
functionSpy.restore();
});
it('should resolve and call the correct functions.', async () => {
const returnValue = await myFunctions.mainFunction();
expect(returnValue).to.equal(1);
expect(functionSpy.callCount).to.equal(1);
})
})
Output
Test fails and I get unmodified output: [ 'One', 'helperFunction Result | ***Nested***', 'Three' ]
Why does it not work when stubbing nestedHelperFunction
but works for helperFunction
?
Working Example
Something that works, but I don't understand why, is within utils.ts
creating helperFunction
and nestedHelperFunction
as methods on a class instead of 'top level' functions.
// utils.ts
export class Utils {
static async helperFunction(): Promise<string> {
const newString = await this.nestedHelperFunction();
return 'helperFunction Result | ' + newString;
}
static async nestedHelperFunction (): Promise<string> {
return '***Nested Output***';
}
}
Tests File
// index.test.ts
import {mainFunction} from '../index';
import {Utils} from '../utils';
import sinon from 'sinon';
import * as chai from 'chai';
const expect = chai.expect;
describe("Test Suite", () => {
let functionSpy: sinon.SinonStub;
beforeEach(() => {
functionSpy = sinon.stub(Utils, 'nestedHelperFunction');
functionSpy.returns(Promise.resolve('Stubbed Function Results!'));
});
afterEach(() => {
functionSpy.restore();
});
it('should resolve and call the correct functions.', async () => {
const returnValue = await mainFunction();
expect(returnValue).to.equal(1);
expect(functionSpy.callCount).to.equal(1);
})
})
// index.ts
import {Utils} from './utils';
export async function mainFunction() {
console.log('Starting mainFunction...');
const promiseResults = await Promise.all([
Promise.resolve('One'),
Utils.helperFunction(),
Promise.resolve('Three'),
]);
console.log(promiseResults);
return 1;
}
Output
Tests pass and I get the desired/expect output: [ 'One',
'helperFunction Result | Stubbed Function Results!',
'Three' ]
Material I've read suggests to me something going on with importing es6 modules or how Typescript compiles and can change the names of imported items. In Javascript I was using Rewire to set stubs, some were private functions, but I had issues with that in Typescript.
Thanks for any help.
Upvotes: 2
Views: 2983
Reputation: 340
You are correct that it has to do with the imports: if you look at the generated code, you see that within util.ts
, the call to nestedHelperFunction
is a direct reference to the local function (i.e. nestedHelperFunction()
, not something like util.nestedHelperFunction()
).
Your stub is currently only replacing the reference on the module object (util.nestedHelperFunction
), not the local one.
You'll indeed need to use something like Rewire to stub it out.
You mentioned you have issues with Rewire and TS. I recently experimented a bit with that too, and it appears that the typings for it aren't working correctly (anymore?). The main problem is that its typings don't export a namespace with the same name as the exported 'main' symbol.
As a temporary workaround (until the 'official' typings are fixed), you can save the following to rewire.d.ts
:
declare module "rewire" {
// Type definitions for rewire 2.5
// Project: https://github.com/jhnns/rewire
// Definitions by: Borislav Zhivkov <https://github.com/borislavjivkov>
// Federico Caselli <https://github.com/CaselIT>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3
namespace rewire {
interface RewiredModule {
/**
* Takes all enumerable keys of obj as variable names and sets the values respectively. Returns a function which can be called to revert the change.
*/
__set__(obj: { [variable: string]: any }): () => void;
/**
* Sets the internal variable name to the given value. Returns a function which can be called to revert the change.
*/
__set__(name: string, value: any): () => void;
/**
* Returns the private variable with the given name.
*/
__get__<T = any>(name: string): T;
/**
* Returns a function which - when being called - sets obj, executes the given callback and reverts obj. If callback returns a promise, obj is only reverted after
* the promise has been resolved or rejected. For your convenience the returned function passes the received promise through.
*/
__with__(obj: { [variable: string]: any }): (callback: () => any) => any;
}
}
/**
* Returns a rewired version of the module found at filename. Use rewire() exactly like require().
*/
function rewire<T = { [key: string]: any }>(filename: string): rewire.RewiredModule & T;
export = rewire;
}
Then (with your unmodified utils.ts
and index.ts
) use the following index.tests.ts
:
import rewire = require("rewire");
import * as sinon from 'sinon';
import { expect } from 'chai';
const myFunctions = rewire<typeof import("../index")>("../index");
const utils = rewire<typeof import("../utils")>("../utils");
describe("Test Suite", () => {
let functionSpy: sinon.SinonStub;
let restoreRewires: Array<() => void>;
beforeEach(() => {
restoreRewires = [];
functionSpy = sinon.stub();
functionSpy.returns(Promise.resolve('Stubbed Function Results!'))
restoreRewires.push(
utils.__set__("nestedHelperFunction", functionSpy),
myFunctions.__set__("utils", utils),
);
});
afterEach(() => {
restoreRewires.forEach(restore => restore());
});
it('should resolve and call the correct functions.', async () => {
const returnValue = await myFunctions.mainFunction();
expect(returnValue).to.equal(1);
expect(functionSpy.callCount).to.equal(1);
});
});
This outputs exactly what you wanted:
Test Suite
Starting mainFunction...
[ 'One',
'helperFunction Result | Stubbed Function Results!',
'Three' ]
✓ should resolve and call the correct functions.
1 passing (31ms)
Upvotes: 2