尝试模拟子方法,如果父模块使用对象解构导入它,则失败。

huangapple go评论83阅读模式
英文:

Attempting to mock child method, fails if parent module imports it using object destructuring

问题

我正在尝试进行一些基本的Jest单元测试,以更深入学习它。

我遇到了一个问题,我不知道如何解释。

这个文件有一个子函数add

// FileB.js 

const add = (a, b) => {
  return a + b;
}

module.exports = {
  add,
};

这个文件有一个父函数addTen

// FileA.js

const { add } = require('./FileB');

const addTen = num => {
  return add(10, num);
}

module.exports = {
  addTen,
};

这是我的测试文件,我试图检查<mockedFunction/spy>.mock.calls或者使用toHaveBeenCalledWith来查看当我调用addTen(10)时是否传递了内部子方法add的参数10,10。

这个文件没有在任何实际环境中使用,只是我尝试学习Jest和单元测试的工作。

// randomTest.js

const { addTen } = require('../src/FileA');
const fileB = require('../src/FileB'); 

describe('temp', () => {
  it('temp', () => {
    const addSpy = jest.spyOn(fileB, 'add');

    addTen(10);
    console.log(addSpy.mock.calls);

    expect(addSpy).toHaveBeenCalledWith(10, 10);
  });
});

现在问题是,测试失败,说add从未被调用,或者没有传入任何参数。我在FileB中记录了add函数。

但是,如果我以以下方式修改FileA,即导入整个模块而不是解构,测试就会通过,并且我可以记录下函数并查看它的工作方式。

这个方法有效:

// FileA.js

const fileB = require('./FileB');

const addTen = num => {
  return fileB.add(10, num);
}

module.exports = {
  addTen,
};

为什么这个轻微的更改有效?是否有一种方法可以避免这种情况并保留我的解构?

英文:

I was doing some basic jest unit testing in attempt to learn it more.

I have this issue I do not know how to explain

This file has the child function add

// FileB.js 

const add = (a, b) =&gt; {
  return a + b;
}

module.exports = {
  add,
};

This file has the parent function addTen

// FileA.js

const { add } = require(&#39;./FileB&#39;);

const addTen = num =&gt; {
  return add(10, num);
}

module.exports = {
  addTen,
};

this is my test file, where I am trying to either check &lt;mockedFunction/spy&gt;.mock.calls or do toHaveBeenCalledWith to see if the inner child method, add, is being passed in 10,10 when i call addTen(10);

This file is not used in any real env, its simply me trying to learn jest + unit testing more.

// randomTest.js

const { addTen } = require(&#39;../src/FileA&#39;);
const fileB = require(&#39;../src/FileB&#39;); 

describe(&#39;temp&#39;, () =&gt; {
  it(&#39;temp&#39;, () =&gt; {
    const addSpy = jest.spyOn(fileB, &#39;add&#39;);

    addTen(10);
    console.log(addSpy.mock.calls);

    expect(addSpy).toHaveBeenCalledWith(10,10)
  });
});

Now for the issue, the test fails, saying add was never called, or nothing passed in. I logged the add function within FileB

However, if I modify FileA in this way, by importing entore module instead of destructuring, the test passes and I cna log out the functions and everything and see it works

This is what works

// FileA.js

const fileB = require(&#39;./FileB&#39;);

const addTen = num =&gt; {
  return fileB.add(10, num);
}

module.exports = {
  addTen,
};

Why does this slight change work? And is there a way to avoid this and keep my destrcutoring?

答案1

得分: 2

你可以通过在从FileA导入addTen之前设置好你的间谍来实现这一点。

const fileB = require('./FileB');
const spy = jest.spyOn(fileB, 'add');

const { addTen } = require('./FileA');

describe('temp', () => {
  afterEach(() => {
    // 恢复使用spyOn创建的间谍
    jest.restoreAllMocks();
  });

  it('temp', () => {
    addTen(10);
    expect(spy).toHaveBeenCalledWith(10, 10);
  });
});

或者,你可以使用jest.mock来模拟./FileB

const mockAdd = jest.fn();
jest.mock('./FileB', () => {
  return {
    add: mockAdd
  };
});

const { addTen } = require('./FileA');

describe('temp', () => {
  it('temp', () => {
    addTen(10);
    expect(mockAdd).toHaveBeenCalledWith(10, 10);
  });
});

在这两种情况下,你希望FileAFileB导入一个模拟的函数,所以你应该在导入之前执行这个操作。

请注意,jest.mock会被提升,因此即使你不将其放在文件的开头,它也会在运行时移到那里。


现在回到你的第一个问题:“为什么解构导入的对象会导致测试失败?”

首先,让我们记住JavaScript通过引用传递对象。这意味着以下情况:

const a = {
  b: 1
}

const c = a;
c.b = 9;

console.log(a); // 将打印 { b: 9 }

这意味着当FileA导入const { add } = require("./FileB")时,你使用的是对函数add的引用(在JavaScript中,函数是对象)。

这导致addTen中已经具有真实add函数的引用,即使你使用spyOn来覆盖FileB.add,也不会改写addTen已经具有的内容。

现在,如果你在导入addTen之前运行spyOn,情况就不同了:

it("should pass", () => {
  const addSpy = jest.spyOn(fileB, "add");
  const { addTen } = require("../FileA");
  addTen(10);
  expect(addSpy).toHaveBeenCalledWith(10, 10);
});

FileB.add的引用将被新模拟的引用替换,你可以看到这是如何在底层发生的:https://github.com/facebook/jest/blob/e2196cab463c5a3e15668b1d7663058b09b7c0ed/packages/jest-mock/src/index.ts#L1256。

英文:

You can achieve this by setting up your spy before importing addTen from FileA

const fileB = require(&#39;./FileB&#39;); 
const spy = jest.spyOn(fileB, &#39;add&#39;);

const {addTen} = require(&#39;./FileA&#39;);

describe(&#39;temp&#39;, () =&gt; {
  afterEach(() =&gt; {
    // restore the spy created with spyOn
    jest.restoreAllMocks();
  });

  it(&#39;temp&#39;, () =&gt; {
    addTen(10);
    expect(spy).toHaveBeenCalledWith(10,10)
  });
});

Or, you can mock ./FileB with jest.mock

const mockAdd = jest.fn()
jest.mock(&#39;./FileB&#39;, ()=&gt; {
  return {
    add: mockAdd
  }
});

const {addTen} = require(&#39;./FileA&#39;);

describe(&#39;temp&#39;, () =&gt; {
  it(&#39;temp&#39;, () =&gt; {
    addTen(10);
    expect(mockAdd).toHaveBeenCalledWith(10,10)
  });
});

In both cases, you want your FileA to import a mocked function from FileB, so you would do that before importing it.

Not that jest.mock would be hoisted anyways, so even if you don't put it at the beginning of your file, it would be moved there on the runtime


Now back to your first question "Why deconstructing the imported object make the test fail?"

First, let's remember that JavaScript pass Objects by reference. Which means the following:

const a = {
  b: 1
}

const c = a;
c.b = 9;

console.log(a); // will print { b: 9 }

Which means when FileA imports const { add } = require(&quot;./FileB&quot;) you are using the reference to the function add (functions are objects in JavaScript).

This leads to having the reference of the real add function in addTen, and then even if you use spyOn to override FileB.add you won't rewrite what addTen already has.

Now in case you use const fileB = require(&quot;./FileB&quot;) in FileA, you get a reference of the object that has add, that same object that you override with jest.spyOn.

That also explains what happens when you run the spyOn before importing addTen:

it(&quot;should pass&quot;, () =&gt; {
  const addSpy = jest.spyOn(fileB, &quot;add&quot;);
  const { addTen } = require(&quot;../FileA&quot;);
  addTen(10);
  expect(addSpy).toHaveBeenCalledWith(10, 10);
});

FileB.add's reference will be replaced with the new mock's reference, see how this is happening under the hood: https://github.com/facebook/jest/blob/e2196cab463c5a3e15668b1d7663058b09b7c0ed/packages/jest-mock/src/index.ts#L1256.

huangapple
  • 本文由 发表于 2023年1月7日 05:59:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/75036458.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定