【问题标题】:Spying module and mocking module function间谍模块和模拟模块功能
【发布时间】:2020-07-27 22:49:24
【问题描述】:

我正在为我的组件编写一些测试,但我在这里遇到了一些麻烦......

这是我的游戏组件:

import React from 'react';
import User from './User';
import Board from './Board';
import Deck from './Deck';

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.board = new Board();
    this.deck = new Deck();

    //some more code
  }

  componentDidMount() {
    this.initializeUser("xpto");
    //some more code
  }

  //some more code

  initializeUser(name) {
    const user = new User(name, this.deck);
    //some more code

    user.pickCards();
    //some more code
  }

  //some more code

  render() {
    return (
      <div className="game-container">
          something to show
          <div id="deck"></div>
      </div>
    );
  }
}


Game.propTypes = {
};

Game.defaultProps = {
};

export default Game;

我的董事会课程:

export default class Board {
  //some code
}

我的甲板课:

export default class Deck {
  constructor(props) {
    //some more code

    this.cardsLeft = 52;
    this.lastPick = 0;

    //some more code
  }

  pickCards() {
    this.lastPick = 4;
    this.cardsLeft -= this.lastPick;
    const deckElem = document.getElementById("deck");
    deckElem.innerHTML = this.cardsLeft;
    return this.lastPick;
  }

  //some more code
}

我的用户类:

class User {
  constructor(name, deck) {
    this.name = name;
    this.tableDeck = deck;
    this.cards = 0;
    //some more code
  }

  //some more code

  pickCards() {
    const newCards = this.tableDeck.pickCards();
    this.cards += newCards;
    //some code
  }

  //some more code
}

export default User;

现在,在我的测试中,我正在尝试测试是否调用了 BoardUser 以及是否也调用了 pickCards()

这是我的测试:

import React from 'react';
import { mount } from 'enzyme';
import Game from './Game';
import User from './User';
import Board from './Board';

describe('Game start', () => {
  let container;

  beforeEach(() => {
    container = document.createElement('div');
    document.body.appendChild(container);
  });

  afterEach(() => {
    document.body.removeChild(container);
    container = null;
  });

  it("test where I'm having problems", () => {
    const boardSpy = jest.spyOn(Board, 'constructor'),
      userSpy = jest.spyOn(User, 'constructor'),
      pickCardMock = jest.fn();

    User.pickCard = pickCardMock;

    const wrapper = mount(<Game />, { attachTo: container });

    expect(boardSpy).toHaveBeenCalledTimes(1);
    expect(userSpy).toHaveBeenCalledTimes(1);
    expect(pickCardMock).toHaveBeenCalledTimes(1);

    //some more code
  });

  it("example test where I need to test everything without mocks", () => {
    const wrapper = mount(<Game />, { attachTo: container });

    expect(wrapper.find("#deck").text()).toEqual('48');

    //some code
  });

  //some more tests
});

我不想模拟BoardUser,因为我需要一切正常工作。但我想监视他们以检查他们是否真的被召唤。我想从User 模拟pickCard()

我已经尝试在我的it() 测试中使用jest.mock('./board');require('board')(例如),但它不起作用。现在我正在尝试监视组件的构造函数。

但是 expect(boardSpy).toHaveBeenCalledTimes(1) 没有说被调用了 0 次而不是 1 次。

pickCardMock 似乎没有链接到User 模块,因为在调试时pickCard 是一个普通函数而不是模拟函数,expect(pickCardMock).toHaveBeenCalledTimes(1) 也收到 0 而不是 1。

有谁知道如何解决这两个问题(窥探一个模块并在另一个模块中模拟一个函数)?

再说一遍:

我不想模拟整个测试套件的东西,我只想 模拟单个测试(我希望能够监视模块调用)。

你可以找到所有这些代码here

【问题讨论】:

  • 不想模拟 Board 和 User,因为我需要一切正常工作。 - 这是集成测试,它不是独立单元测试的适当替代品. 但我想监视他们以检查他们是否真的被召唤 - 为什么?如果您的意图是保留原始实现,您只需检查 board 是 Board 的一个实例。 我已经尝试过使用 jest.mock('./board');和 require('board') - 你到底做了什么?您需要jest.resetModules(),然后重新导入整个模块层次结构,即require 依赖于Board 的组件,而不仅仅是Board。
  • @EstusFlask 我可以尝试以instanceof Boad 测试板。但是我怎样才能在包装器成为mount() 之前模拟User.pickCards
  • 一种简单的方法是在顶层使用jest.mock('./user') 并自动模拟这些类。可以监视构造函数而不是模拟它们,但它是不可靠的 - 构造函数本身就是一个类,而不仅仅是 constructor 属性,因此 spy 函数应该以一种棘手的方式从原始类继承,并且不能保证按预期工作,这违背了让它“正常工作”的目的。

标签: reactjs testing mocking jestjs enzyme


【解决方案1】:

您可以使用jest.mock(moduleName, factory, options) 模拟UserBoard 组件。

例如

Game.jsx:

import React, { Component } from 'react';
import User from './User';
import Board from './Board';

class Game extends Component {
  constructor(props) {
    super(props);
    this.board = new Board({});
  }

  initializeUser(name) {
    const user = new User(name);
    user.pickCards();
  }

  render() {
    return <div className="game-container"></div>;
  }
}

export default Game;

Board.jsx:

import React, { Component } from 'react';

class Board extends Component {
  constructor(props) {
    super(props);
  }
  render() {
    return 'Board';
  }
}

export default Board;

User.jsx:

import React, { Component } from 'react';

class User extends Component {
  constructor(props) {
    super(props);
    this.name = this.props.name;
  }

  pickCards() {
    console.log('pick cards real implementation');
  }

  render() {
    return 'User';
  }
}

export default User;

Game.test.jsx:

import React from 'react';
import { mount } from 'enzyme';
import Game from './Game';
import BoardMock from './Board';
import UserMock from './User';

jest.mock('./User', () => {
  const mUser = { pickCards: jest.fn() };
  return jest.fn(() => mUser);
});

jest.mock('./Board', () => jest.fn());

describe('62199135', () => {
  afterAll(() => {
    jest.resetAllMocks();
  });
  it('should pass', () => {
    const userMock = new UserMock();
    const wrapper = mount(<Game />);
    const gameInstance = wrapper.instance();
    gameInstance.initializeUser('some name');

    expect(BoardMock).toHaveBeenCalledTimes(1);
    expect(UserMock).toHaveBeenCalledWith('some name');
    expect(userMock.pickCards).toHaveBeenCalledTimes(1);
  });
});

您可以在运行所有测试用例后重置所有模拟。

单元测试结果:

 PASS  stackoverflow/62199135/Game.test.jsx (10.16s)
  62199135
    ✓ should pass (33ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |     100 |      100 |     100 |     100 |                   
 Game.jsx |     100 |      100 |     100 |     100 |                   
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        11.474s, estimated 12s

【讨论】:

  • 感谢您的回答,这是一个关于如何模拟模块和函数的好例子。但是我不能模拟 BoardUser,因为我需要在另一个测试中使用它们,这就是为什么我试图间谍而不是模拟。所以我正在寻找一种方法来监视它们或仅在该测试中模拟它们,或者如果存在另一种更好的方法。
  • @Ninita 你可以重置所有模拟
  • jest.resetAllMocks() 不是解决方案,因为对于您的代码,BoardUser 仍然是整个测试套件的模拟,我只需要模拟它们进行一次测试。换句话说,此方法仅将所有模拟重置为原始状态,不要将它们还原为模块实例。
【解决方案2】:

如果您还没有尝试过:

您可以使用jest.spyOn(User.prototype, 'pickCard') 监视User.pickCard

另一种监视BoardUser 同时保持原始实现的选项是jest.requireActual

import Board from './Board';

jest.mock('./Board', () => {
  const BoardSpy = jest.requireActual('./Board').default;
  return {
    default: jest.fn((...args) => new BoardSpy(...args)),
    __esModule: true,
  };
});

it('instantiates a Board', () => {
  const board = new Board();
  expect(Board).toBeCalled();
});

【讨论】:

  • 不幸的是,这并没有解决 OP 只想模拟测试子集的方法的问题。
  • 它通过使该要求变得不必要来解决它(通过从 jest.mock 返回实际实现,不需要模拟某些测试而不是其他测试)。该示例创建了一个间谍的等价物,如果需要,它还可以为单个测试提供一个模拟实现。
  • 够近了!非常感谢@helloitsjoe。不需要const board = new Board(); 行。我只是将jest.resetAllMocks(); 添加到我的afterEach(),因为我正在做expect(Board).toHaveBeenCalledTimes(1);,现在它就像一个魅力!真的谢谢
  • 太好了,很高兴它有帮助!
【解决方案3】:

您似乎正在尝试在同一个测试文件中进行单元测试和集成测试 - 最佳实践建议您为它们编写单独的文件。仅此一项就可以解决您的问题。

另外,在您的initializeUser 方法中,您正在创建一个User,调用pickCards,然后......看起来您没有将变量存储在任何地方。您不需要再次访问该user 变量吗?

编辑:删除答案中的错误。

【讨论】:

  • initializeUser在componentDidMount期间被调用,所以它会作为mount(&lt;Game /&gt;)的结果被调用
  • 谢谢乔。因为我不熟悉 React,所以我不得不查一下,没有意识到它是一个生命周期钩子。我已经更新了答案。
猜你喜欢
  • 2020-10-26
  • 2019-07-16
  • 1970-01-01
  • 2019-08-28
  • 1970-01-01
  • 1970-01-01
  • 2018-08-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多