我在JS中有一个工厂方法,它为我的node.js app创建一个对象。这个工厂方法接收一些参数,我想测试我是否正确创建对象。
@Configuration
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();
函数很容易测试,因为我传递了一个对象中的所有依赖项,因此我可以存储和窥探我想要的所有内容。
问题是obj
,这个函数应该创建一个包含所有内容的objFactory
对象,为了做到这一点,我在那里使用obj
,这意味着我不能嘲笑它。我也无法测试new LibX
是否构建良好或是否正确传递。
这也违反了Law of Demeter,因为我的工厂需要知道不应该知道的事情。
没有传递phrase
作为参数(这意味着我需要工厂为我的工厂....混淆吧?)我不知道如何解决这个问题。
如何使LibX
易于测试?
答案 0 :(得分:2)
您需要问自己的第一个问题是您想要测试的内容。
您是否需要确保正确构建phrase
常量?如果是这样,您需要将其提取到单独的函数并单独测试。
或许你想要的是测试myObj.hello();
的效果。在这种情况下,我建议让hello()
返回一个字符串,而不是将其记录到控制台;这将使最终效果易于测试。
干净编写的代码将避免无法解决的依赖关系。您编写示例的方式libx
(外部依赖项)无法模拟。或者我应该说,不应该被嘲笑。技术上也可以嘲笑它,但我建议不要这样做,因为它会给图片带来自己的复杂性。
这非常简单。你的单元测试应该看起来像这样:
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!");
});
通过上述单元测试,您的生产代码需要看起来像这样:
const buildPhrase = function(input) {
const owner = input.owner || "max";
const animal = input.animal;
return `${owner} from ${animal} says hello!`;
};
你有它,测试短语构建。然后,您可以在buildPhrase
内使用objFactory
。
这也很简单。您为工厂提供输入并期望输出。输出将始终是输入的函数,即相同的输入将始终产生相同的输出。那么,如果你可以预测预期的结果,为什么还要测试一下发生了什么?
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!");
});
最终可能会引导您使用以下生产代码:
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 });
};
require("libx")
或者不要。如前所述,你真的不应该这样做。不过,如果你被迫这样做(并且我将这个决定留给你),你可以使用mock-require或类似的工具。
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!");
});
});
但请记住,这种方法比看起来更棘手。模拟require
依赖项会覆盖require
的缓存,因此您必须记住清除它以防其他测试不希望依赖项被模拟,而是依赖于它执行的操作。此外,您必须始终保持警惕,并确保代码的执行顺序(不是那么明显)是正确的。您必须首先模拟依赖关系,然后使用require()
,但确保这一点并不容易。
模拟依赖项的最简单方法是始终注入它。由于您在代码中使用new
,因此将它包装在一个可以随时模拟的简单函数中是有意义的:
const makeLibx = (phrase) => {
return new LibX(phrase);
};
如果你把它注入你的工厂,那么嘲弄将变得微不足道:
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!");
});
显然,这会导致你写这样的东西:
const objFactory = (makeLibx, {animal, owner = "max"}) => {
const phrase = `${owner} from ${animal} says hello!`;
const lib = makeLibx(phrase);
return obj({ colorLib: lib });
};
我的最后一条建议:始终提前规划您的代码并尽可能使用TDD。如果您编写生产代码然后考虑如何测试它,您会发现自己一遍又一遍地问同样的问题:我该如何测试它?我如何模拟这种依赖?这不违反得墨忒耳法吗?
虽然您应该问自己的问题是:我希望此代码能做什么?我该如何表现?它的效果应该是什么样的?