Spying module and mocking module function

Viewed 1408

I'm writing some tests for my component but I'm facing some troubles here...

Here is my Game component:

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;

My Board class:

export default class Board {
  //some code
}

My Deck class:

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
}

My User class:

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;

Now, at my tests I'm trying to test if the Board and User are called and if the pickCards() is called too.

Here are my tests:

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

I don't want to mock Board and User because I need everything work normally on it. But I want to spy them to check if they were really called. And I want to mock pickCard() from User.

I already tried to use jest.mock('./board'); and require('board') (for example) only inside my it() test but it didn't work. And now I'm trying to spy the components constructors.

But the expect(boardSpy).toHaveBeenCalledTimes(1) fails saying that was called 0 times and not 1 time.

And the pickCardMock seems not being linked to the User module because when debugging pickCard is a normal function and not a mock function and expect(pickCardMock).toHaveBeenCalledTimes(1) receives 0 too instead of 1.

Anyone knows how to solve these two problems (spy a module and mock a function in another module)?

Again:

I don't want to mock things for the whole test suite, I just want to mock for a single test (and I want to be able to spy a module call).

You can find all this code here.

3 Answers

You can use jest.mock(moduleName, factory, options) to mock User and Board components.

E.g.

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

You can reset all mocks after running all test cases.

Unit test result:

 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

In case you haven't tried this yet:

You can spy on User.pickCard with jest.spyOn(User.prototype, 'pickCard').

Another option for spying on Board and User while keeping the original implementation in place is with 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();
});

It looks like you are trying to do both unit testing and integration testing within the same test file - best practice recommends that you write separate files for them. That alone would solve your problem.

Also, in your initializeUser method, you are creating a User, calling pickCards, and then... It doesn't look like you are storing the variable anywhere. Do you not need access to that user variable again?

EDIT: removed mistake in answer.

Related