简体   繁体   中英

How to test factory method in JS?

Background

I have a factory method in JS that creates an object for my node.js app. 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.

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. I also cannot test if phrase is well built or if it is being passed correctly.

This also violates the Law of Demeter because my factory needs to know something that it shouldn't.

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.

Question

How can I make objFactory easily testable?

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? 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(); . In this case, I would suggest making hello() return a string instead of logging it to the console; 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. 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

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 .

2. Test effects of returned method

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")

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. 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.

4. Just inject the dependency

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:

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. 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?

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?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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