简体   繁体   English

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

[英]How to test factory method in JS?

Background 背景

I have a factory method in JS that creates an object for my node.js app. 我在JS中有一个工厂方法,可以为我的node.js应用程序创建一个对象。 This factory method receives some parameters and I want to test if I am creating the object properly. 此工厂方法接收一些参数,我想测试是否正确创建了对象。

Code

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();

Problem 问题

The obj function is easy to test because I pass all the dependencies in an object and thus I can stub and spy all I want. obj函数很容易测试,因为我将所有依赖项传递给对象,因此可以存根和监视所有想要的东西。

The problem is the objFactory , this function is supposed to create an obj object with everything included and in order to do this I use new LibX there, which means I can't mock it. 问题是objFactory ,该函数应该创建一个包含所有内容的obj对象,为了做到这一点,我在那里使用了new LibX ,这意味着我无法模拟它。 I also cannot test if phrase is well built or if it is being passed correctly. 我也无法测试该phrase是否构建良好或是否正确传递。

This also violates the Law of Demeter because my factory needs to know something that it shouldn't. 这也违反了得墨 the 耳定律,因为我的工厂需要知道一些本不应该知道的东西。

Without passing LibX as a parameter (which means I would need a Factory for my Factory .... confusing right?) I have not idea on how to fix this. 如果不传递LibX作为参数(这意味着我将需要一个Factory作为工厂....造成混淆了吗?),我不知道如何解决此问题。

Question

How can I make objFactory easily testable? 如何使objFactory易于测试?

The first question you need to ask yourself is what you want to test. 您需要问自己的第一个问题是您要测试什么。

Do you need to make sure the phrase constant is built correctly? 您是否需要确保正确构建phrase常量? If so, you need to extract that to a separate function and test it separately. 如果是这样,则需要将其提取到单独的函数并分别进行测试。

Or perhaps what you want is to test the effect of myObj.hello(); 也许您想要的是测试myObj.hello();的效果myObj.hello(); . In this case, I would suggest making hello() return a string instead of logging it to the console; 在这种情况下,我建议使hello()返回一个字符串,而不是将其记录到控制台。 this will make the final effect easily testable. 这将使最终效果易于测试。

Cleanly written code will avoid dependencies that are unmockable. 编写清晰的代码将避免产生不可模仿的依赖关系。 The way you wrote your example, libx , which is an external dependency, cannot be mocked. 不能嘲笑您编写示例的方法libx (它是一个外部依赖项)。 Or should I say, it should not be mocked. 或者我应该说,它不应该被嘲笑。 It is technically possible to mock it as well, but I'd advise against this as it brings its own complications into the picture. 从技术上讲,也可以对其进行模拟,但是我建议您不要这样做,因为它会给图片带来其自身的复杂性。

1. Make sure the phrase is built correctly 1.确保词组正确构建

This is pretty straightforward. 这很简单。 Your unit tests should look somewhat like this: 您的单元测试应该看起来像这样:

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!");
});

With the above unit tests, your production code will need to look somewhat like this: 通过上面的单元测试,您的生产代码将需要看起来像这样:

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

    return `${owner} from ${animal} says hello!`;
};

And there you have it, the phrase building is tested. 在这里,短语构建已经过测试。 You can then use buildPhrase inside your objFactory . 然后,您可以使用buildPhrase你里面objFactory

2. Test effects of returned method 2.测试返回方法的效果

This is also straightforward. 这也很简单。 You provide the factory with an input and expect an output. 您为工厂提供输入,并期望输出。 The output will always be a function of the input, ie same input will always produce the same output. 输出将始终是输入的函数,即相同的输入将始终产生相同的输出。 So why test what's going on under the hood if you can predict the expected outcome? 那么,如果可以预测预期的结果,为什么还要测试幕后情况呢?

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!");
});

Which might eventually lead you to the following production code: 最终可能会导致您进入以下生产代码:

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

3. Mock the output of require("libx") 3.模拟require("libx")的输出

Or don't. 还是不。 As stated before, you really shouldn't do this. 如前所述,您实际上不应该这样做。 Still, should you be forced to do so (and I leave the reasoning behind this decision to you), you can use a tool such as mock-require or a similar one. 不过,如果您被迫这样做(我将推理留给了这个决定),则可以使用模拟需求或类似的工具。

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!");
    });
});

Bear in mind, however, that this approach is trickier than it seems. 但是请记住,这种方法比看起来复杂。 Mocking the require dependency overwrites require 's cache, so you must remember to clear it in case there other tests that do not want the dependency mocked and rather depend on it doing what it does. 模拟require依赖项覆盖了require的缓存,因此您必须记住清除它,以防万一有其他不希望依赖项被嘲笑而是依赖于它执行操作的测试。 Also, you must always be vigilant and make sure the execution order of your code (which isn't always that obvious) is correct. 另外,您必须始终保持警惕,并确保代码的执行顺序(并不总是那么明显)是正确的。 You must mock the dependency first, then use require() , but it's not always easy to ensure that. 您必须先模拟依赖项,然后使用require() ,但是要确保这一点并不总是那么容易。

4. Just inject the dependency 4.注入依赖

The simplest way of mocking a dependency is always injecting it. 模拟依赖项的最简单方法是始终注入它。 Since you use new in your code, it might make sense to wrap this in a simple function that you can mock out at any time: 由于在代码中使用了new ,因此将其包装在一个可以随时模拟的简单函数中可能是有意义的:

const makeLibx = (phrase) => {
    return new LibX(phrase);
};

If you then inject this into your factory, mocking will become trivial: 如果您随后将其注入到工厂中,则嘲笑将变得微不足道:

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!");
});

Which will, obviously, lead to you writing something like this: 显然,这将导致您编写如下内容:

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

    return obj({ colorLib: lib });
};

One last piece of advice from me: always plan your code ahead and use TDD whenever possible. 我的最后一条建议是:始终提前计划代码,并尽可能使用TDD。 If you write production code and then think about how you can test it, you will find yourself asking the same questions over and over again: how do I test it? 如果您编写生产代码然后考虑如何进行测试,就会发现自己一遍又一遍地问同样的问题:我该如何测试它? How do I mock this dependency? 我该如何模拟这种依赖性? Doesn't this violate the Law of Demeter? 这不违反得墨meter耳定律吗?

While the questions you should be asking yourself are: what do I want this code to do? 虽然您应该问自己的问题是:我希望这段代码做什么? How do I want it to behave? 我要如何表现? What its effects should be like? 它的效果应该是什么样的?

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM