如何在JS中测试工厂方法?

Fla*_*nix 1 javascript unit-testing mocha.js node.js sinon

背景

我在 JS 中有一个工厂方法,可以为我的 node.js 应用程序创建一个对象。这个工厂方法接收一些参数,我想测试我是否正确创建了对象。

代码

const LibX = require("libX");

const obj = deps => {

    const { colorLib } = deps;

    const hello = () => {
        console.log(colorLib.sayHello()); // prints a phrase with cool colors
    };

    return {
        hello
    };
};

//Here I return `obj` with all dependencies included. Ready to use!
const objFactory = ({animal, owner = "max"}) => {

    //For example,I need to know if phrase is being well constructed
    const phrase = `${owner} from ${animal} says hello!`;

    const lib = new LibX(phrase);
    return obj({ colorLib: lib });
};

const myObj = objFactory({animal: "cat"});
myObj.hello();
Run Code Online (Sandbox Code Playgroud)

问题

obj函数很容易测试,因为我传递了对象中的所有依赖项,因此我可以存根和监视我想要的所有内容。

问题是objFactory,这个函数应该创建一个obj包含所有内容的对象,为了做到这一点,我new LibX在那里使用,这意味着我无法模拟它。我也无法测试是否phrase构建良好或是否正确通过。

这也违反了德米特定律,因为我的工厂需要知道一些它不应该知道的信息。

如果不作为参数传递LibX(这意味着我的工厂需要一个工厂......令人困惑,对吧?)我不知道如何解决这个问题。

问题

我怎样才能objFactory轻松测试?

min*_*gos 5

您需要问自己的第一个问题是您想要测试什么。

您需要确保phrase常量构建正确吗?如果是这样,您需要将其提取到一个单独的函数中并单独进行测试。

或者也许你想要的是测试一下效果myObj.hello();。在这种情况下,我建议hello()返回一个字符串,而不是将其记录到控制台;这将使最终效果易于测试。

清晰编写的代码将避免不可模拟的依赖关系。您编写示例的方式libx是外部依赖项,无法被嘲笑。或者我应该说,它不应该被嘲笑。从技术上讲,模拟它也是可能的,但我建议不要这样做,因为它会带来自己的复杂性。

1. 确保短语构建正确

这非常简单。你的单元测试应该看起来像这样:

it("should build the phrase correctly using all params", () => {
    // given
    const input = {animal: "dog", owner: "joe"};

    // when
    const result = buildPhrase(input);

    // then
    expect(result).to.equal("joe from dog says hello!");
});

it("should build the phrase correctly using only required params", () => {
    // given
    const input = {animal: "cat"};

    // when
    const result = buildPhrase(input);

    // then
    expect(result).to.equal("max from cat says hello!");
});
Run Code Online (Sandbox Code Playgroud)

通过上述单元测试,您的生产代码将需要看起来像这样:

const buildPhrase = function(input) {
    const owner = input.owner || "max";
    const animal = input.animal;

    return `${owner} from ${animal} says hello!`;
};
Run Code Online (Sandbox Code Playgroud)

现在你已经完成了短语构建的测试。然后您可以buildPhrase在您的objFactory.

2、返回方法测试效果

这也很简单。您向工厂提供输入并期望输出。输出将始终是输入的函数,即相同的输入将始终产生相同的输出。那么,如果您可以预测预期结果,为什么要测试幕后发生的事情呢?

it("should produce a function that returns correct greeting", () => {
    // given
    const input = {animal: "cat"};
    const obj = objFactory(input);

    // when
    const result = obj.hello();

    // then
    expect(result).to.equal("max from cat says hello!");
});
Run Code Online (Sandbox Code Playgroud)

这最终可能会导致您得到以下生产代码:

const LibX = require("libX");

const obj = deps => {
    const { colorLib } = deps;
    const hello = () => {
        return colorLib.sayHello(); // note the change here
    };

    return {hello};
};

const objFactory = ({animal, owner = "max"}) => {
    const phrase = `${owner} from ${animal} says hello!`;
    const lib = new LibX(phrase);

    return obj({ colorLib: lib });
};
Run Code Online (Sandbox Code Playgroud)

3. 模拟输出require("libx")

或者不这样做。如前所述,您确实不应该这样做。不过,如果您被迫这样做(我将这个决定背后的原因留给您),您可以使用诸如模拟需求或类似工具之类的工具。

const mock = require("mock-require");

let currentPhrase;
mock("libx", function(phrase) {
    currentPhrase = phrase;
    this.sayHello = function() {};
});

const objFactory = require("./objFactory");

describe("objFactory", () => {
    it("should pass correct phrase to libx", () => {
        // given
        const input = {animal: "cat"};

        // when
        objFactory(input);

        // then
        expect(currentPhrase).to.be("max from cat says hello!");
    });
});
Run Code Online (Sandbox Code Playgroud)

但请记住,这种方法比看起来更棘手。模拟require依赖项会覆盖 的require缓存,因此您必须记住清除它,以防存在其他不希望模拟依赖项而是依赖于它执行其操作的测试。另外,您必须始终保持警惕并确保代码的执行顺序(并不总是那么明显)是正确的。您必须首先模拟依赖项,然后使用require(),但确保这一点并不总是那么容易。

4.只需注入依赖

模拟依赖项的最简单方法始终是注入它。由于您在代码中使用new,因此将其包装在一个可以随时模拟的简单函数中可能是有意义的:

const makeLibx = (phrase) => {
    return new LibX(phrase);
};
Run Code Online (Sandbox Code Playgroud)

如果您随后将其注入工厂,则模拟将变得微不足道:

it("should pass correct input to libx", () => {
    // given
    let phrase;
    const mockMakeLibx = function(_phrase) {
        phrase = _phrase;
        return {sayHello() {}};
    };
    const input = {animal: "cat"};

    // when
    objFactory(mockMakeLibx, input);

    // then
    expect(phrase).to.equal("max from cat says hello!");
});
Run Code Online (Sandbox Code Playgroud)

显然,这会导致你写出这样的东西:

const objFactory = (makeLibx, {animal, owner = "max"}) => {
    const phrase = `${owner} from ${animal} says hello!`;
    const lib = makeLibx(phrase);

    return obj({ colorLib: lib });
};
Run Code Online (Sandbox Code Playgroud)

我的最后一条建议:始终提前规划代码并尽可能使用 TDD。如果您编写生产代码,然后考虑如何测试它,您会发现自己一遍又一遍地问同样的问题:我如何测试它?我如何嘲笑这种依赖性?这不违反德墨忒尔法则吗?

虽然您应该问自己的问题是:我想要这段代码做什么?我希望它如何表现?它的效果应该是怎样的呢?