首页 > 解决方案 > 在 TypeScript 中进行单元测试时,存根依赖项的正确方法是什么?

问题描述

我正在将一组 Firebase 云函数从 Javascript 迁移到 Typescript。使用 JS,我能够使用 mocha、chai 和 sinon 进行单元测试,并存根各种数据库依赖项进行测试。使用 TS,我遇到了我不理解的问题和意外行为。

该模式类似于: mainFunction调用helperFunction哪个调用nestedHelperFunction

当我用一些测试数据nestedHelperFunction调用时,我想在我的测试中存根或监视。mainFunction

mainFunction在一个index.ts和中helperFunction,并且nestedHelperFunction在一个utils.ts文件中。

奇怪行为的例子

// 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***';
}

测试文件

//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);
    })
})

输出: 测试通过,我得到:[ 'One', 'Stubbed Function Results!', 'Three' ]

但是,如果我尝试存根nestedHelperFunction它不起作用。

// 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);
    })
})

输出 测试失败,我得到未修改的输出:[ 'One', 'helperFunction Result | ***Nested***', 'Three' ]

为什么它在存根时不起作用nestedHelperFunction但适用于 helperFunction

工作示例

一些有效的东西,但我不明白为什么,是在类的utils.ts创建helperFunctionnestedHelperFunction方法中,而不是“顶级”函数。

// 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***';
    }
}

测试文件

// 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;
}

输出 测试通过,我得到所需/预期的输出:[ 'One', 'helperFunction Result | Stubbed Function Results!', 'Three' ]

我读过的材料向我暗示了导入 es6 模块或 Typescript 如何编译并可以更改导入项目的名称。在 Javascript 中,我使用 Rewire 来设置存根,其中一些是私有函数,但我在 Typescript 中遇到了问题。

谢谢你的帮助。

标签: typescriptunit-testingsinon

解决方案


您是正确的,它与导入有关:如果您查看生成的代码,您会看到在 内util.ts,对本地函数的调用nestedHelperFunction是对本地函数的直接引用(即nestedHelperFunction(),不是类似的东西util.nestedHelperFunction())。

您的存根当前仅替换模块对象 ( util.nestedHelperFunction) 上的引用,而不是本地的。

您确实需要使用Rewire之类的东西来将其剔除。


你提到你有 Rewire 和 TS 的问题。我最近也对此进行了一些实验,但它的打字似乎无法正常工作(不再?)。主要问题是它的类型不会导出与导出的“主”符号同名的命名空间。

作为临时解决方法(直到“官方”类型被修复),您可以将以下内容保存到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;
}

然后(使用未修改的utils.tsand index.ts)使用以下内容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);
    });
});

这正是您想要的输出:

  Test Suite
Starting mainFunction...
[ 'One',
  'helperFunction Result | Stubbed Function Results!',
  'Three' ]
    ✓ should resolve and call the correct functions.


  1 passing (31ms)

推荐阅读