Подтвердить что ты не робот

Тестировать функцию React Component с помощью Jest

Оригинальные

Прежде всего, я следую за Flux.

У меня есть индикатор, который показывает несколько секунд, например: 30 секунд. Каждые одну секунду он показывает на 1 секунду меньше, поэтому 29, 28, 27 до 0. Когда доходит до 0, я очищаю интервал, чтобы он переставал повторять. Более того, я запускаю действие. Когда это действие будет отправлено, мой магазин уведомляет меня. Поэтому, когда это происходит, я reset интервал до 30 секунд и т.д. Компонент выглядит следующим образом:

var Indicator = React.createClass({

  mixins: [SetIntervalMixin],

  getInitialState: function(){
    return{
      elapsed: this.props.rate
    };
  },

  getDefaultProps: function() {
    return {
      rate: 30
    };
  },

  propTypes: {
    rate: React.PropTypes.number.isRequired
  },

  componentDidMount: function() {
    MyStore.addChangeListener(this._onChange);
  },

  componentWillUnmount: function() {
    MyStore.removeChangeListener(this._onChange);
  },

  refresh: function(){
    this.setState({elapsed: this.state.elapsed-1})

    if(this.state.elapsed == 0){
      this.clearInterval();
      TriggerAnAction();
    }
  },

  render: function() {
    return (
      <p>{this.state.elapsed}s</p>
    );
  },

  /**
   * Event handler for 'change' events coming from MyStore
   */
  _onChange: function() {
    this.setState({elapsed: this.props.rate}
    this.setInterval(this.refresh, 1000);
  }

});

module.exports = Indicator;

Компонент работает как ожидалось. Теперь я хочу проверить его с помощью Jest. Я знаю, что могу использовать renderIntoDocument, тогда я могу установить Timeout из 30-х и проверить, равен ли мой компонент .state.elapsed равным 0 (например).

Но, что я хочу проверить здесь, это разные вещи. Я хочу проверить , если функция обновления называется. Более того, я бы хотел проверить, что, когда мое прошедшее состояние равно 0, , он запускает мой TriggerAnAction(). Хорошо, в первую очередь я пытался:

jest.dontMock('../Indicator');

describe('Indicator', function() {
  it('waits 1 second foreach tick', function() {

    var React = require('react/addons');
    var Indicator = require('../Indicator.js');
    var TestUtils = React.addons.TestUtils;

    var Indicator = TestUtils.renderIntoDocument(
      <Indicator />
    );

    expect(Indicator.refresh).toBeCalled();

  });
});

Но при записи теста npm я получаю следующую ошибку:

Throws: Error: toBeCalled() should be used on a mock function

Я видел из ReactTestUtils функцию mockComponent, но, учитывая ее объяснения, я не уверен, что это то, что мне нужно.

Хорошо, в этом пункте я застрял. Может ли кто-нибудь дать мне некоторый свет о том, как проверить, что две вещи, о которых я упоминал выше?


Обновление 1, на основе ответа Ian

Это тест, который я пытаюсь запустить (см. комментарии в некоторых строках):

jest.dontMock('../Indicator');

describe('Indicator', function() {
  it('waits 1 second foreach tick', function() {

    var React = require('react/addons');
    var Indicator = require('../Indicator.js');
    var TestUtils = React.addons.TestUtils;

    var refresh = jest.genMockFunction();
    Indicator.refresh = refresh;

    var onChange = jest.genMockFunction();
    Indicator._onChange = onChange;

    onChange(); //Is that the way to call it?

    expect(refresh).toBeCalled(); //Fails
    expect(setInterval.mock.calls.length).toBe(1); //Fails

    // I am trying to execute the 1 second timer till finishes (would be 60 seconds)
    jest.runAllTimers();

    expect(Indicator.state.elapsed).toBe(0); //Fails (I know is wrong but this is the idea)
    expect(clearInterval.mock.calls.length).toBe(1); //Fails (should call this function when time elapsed is 0)

  });
});

Я все еще что-то недопонимаю...

4b9b3361

Ответ 1

Похоже, ты на правильном пути. Чтобы убедиться, что все на одной странице для этого ответа, позвольте получить некоторую терминологию в сторону.

Mock: функция с поведением, управляемая unit test. Вы обычно заменяете реальные функции на каком-либо объекте с помощью макетной функции, чтобы убедиться, что функция mock правильно вызвана. Jest предоставляет mocks для каждой функции на модуле автоматически, если вы не назовете jest.dontMock на имя этого модуля.

Класс компонента. Это свойство возвращается React.createClass. Вы используете его для создания экземпляров компонентов (это сложнее, но это достаточно для наших целей).

Экземпляр компонента: фактический визуализированный экземпляр класса компонента. Это то, что вы получили после вызова TestUtils.renderIntoDocument или многих других функций TestUtils.


В обновленном примере из вашего вопроса вы генерируете mocks и привязываете их к классу компонентов вместо экземпляра компонента. Кроме того, вы хотите только высмеять функции, которые вы хотите контролировать или иным образом изменить; например, вы издеваетесь _onChange, но на самом деле вы этого не хотите, потому что вы хотите, чтобы он вел себя нормально - это только refresh, который вы хотите высмеять.

Вот предлагаемый набор тестов, которые я написал для этого компонента; комментарии встроены, поэтому оставляйте комментарии, если у вас есть какие-либо вопросы. Полный рабочий источник для этого примера и набора тестов находится в https://github.com/BinaryMuse/so-jest-react-mock-example/tree/master; вы должны иметь возможность клонировать его и запускать без проблем. Обратите внимание, что мне пришлось внести некоторые незначительные догадки и изменения в компонент, поскольку не все ссылочные модули были в вашем исходном вопросе.

/** @jsx React.DOM */

jest.dontMock('../indicator');
// any other modules `../indicator` uses that shouldn't
// be mocked should also be passed to `jest.dontMock`

var React, IndicatorComponent, Indicator, TestUtils;

describe('Indicator', function() {
  beforeEach(function() {
    React = require('react/addons');
    TestUtils = React.addons.TestUtils;
    // Notice this is the Indicator *class*...
    IndicatorComponent = require('../indicator.js');
    // ...and this is an Indicator *instance* (rendered into the DOM).
    Indicator = TestUtils.renderIntoDocument(<IndicatorComponent />);
    // Jest will mock the functions on this module automatically for us.
    TriggerAnAction = require('../action');
  });

  it('waits 1 second foreach tick', function() {
    // Replace the `refresh` method on our component instance
    // with a mock that we can use to make sure it was called.
    // The mock function will not actually do anything by default.
    Indicator.refresh = jest.genMockFunction();

    // Manually call the real `_onChange`, which is supposed to set some
    // state and start the interval for `refresh` on a 1000ms interval.
    Indicator._onChange();
    expect(Indicator.state.elapsed).toBe(30);
    expect(setInterval.mock.calls.length).toBe(1);
    expect(setInterval.mock.calls[0][1]).toBe(1000);

    // Now we make sure `refresh` hasn't been called yet.
    expect(Indicator.refresh).not.toBeCalled();
    // However, we do expect it to be called on the next interval tick.
    jest.runOnlyPendingTimers();
    expect(Indicator.refresh).toBeCalled();
  });

  it('decrements elapsed by one each time refresh is called', function() {
    // We've already determined that `refresh` gets called correctly; now
    // let make sure it does the right thing.
    Indicator._onChange();
    expect(Indicator.state.elapsed).toBe(30);
    Indicator.refresh();
    expect(Indicator.state.elapsed).toBe(29);
    Indicator.refresh();
    expect(Indicator.state.elapsed).toBe(28);
  });

  it('calls TriggerAnAction when elapsed reaches zero', function() {
    Indicator.setState({elapsed: 1});
    Indicator.refresh();
    // We can use `toBeCalled` here because Jest automatically mocks any
    // modules you don't call `dontMock` on.
    expect(TriggerAnAction).toBeCalled();
  });
});

Ответ 2

Думаю, я понимаю, о чем вы спрашиваете, хотя бы часть этого!

Начиная с ошибки, причина, по которой вы видите это, заключается в том, что вы указали, что не шутите над модулем индикатора, чтобы все внутренние элементы были такими, как вы их написали. Если вы хотите проверить, какая именно функция вызвана, я бы предложил вам создать функцию mock и использовать это вместо этого...

var React = require('react/addons');
var Indicator = require('../Indicator.js');
var TestUtils = React.addons.TestUtils;

var refresh = jest.genMockFunction();
Indicator.refresh = refresh; // this gives you a mock function to query

Следующее, что нужно отметить, это на самом деле переназначение переменной Indicator в вашем примере кода, поэтому для правильного поведения я бы переименовал вторую переменную (например, ниже)

var indicatorComp = TestUtils.renderIntoDocument(<Indicator />);

Наконец, если вы хотите протестировать что-то, что меняется со временем, используйте функции TestUtils вокруг манипуляции с таймером (http://facebook.github.io/jest/docs/timer-mocks.html). В вашем случае я думаю, что вы можете сделать:

jest.runAllTimers();

expect(refresh).toBeCalled();

В качестве альтернативы, и, возможно, немного менее суетливым является использование ложных реализаций setTimeout и setInterval для определения вашего компонента:

expect(setInterval.mock.calls.length).toBe(1);
expect(setInterval.mock.calls[0][1]).toBe(1000);

Другое дело, что для любого из вышеперечисленных изменений для работы я думаю, вам нужно вручную запустить метод onChange, поскольку ваш компонент изначально будет работать с издетой версией вашего магазина, поэтому никаких изменений не произойдет. Вам также необходимо убедиться, что вы задали шутки, чтобы игнорировать реагирующие модули, иначе они будут автоматически высмеиваться.

Полный предлагаемый тест

jest.dontMock('../Indicator');

describe('Indicator', function() {
  it('waits 1 second for each tick', function() {
    var React = require('react/addons');
    var TestUtils = React.addons.TestUtils;

    var Indicator = require('../Indicator.js');
    var refresh = jest.genMockFunction();
    Indicator.refresh = refresh;

    // trigger the store change event somehow

    expect(setInterval.mock.calls.length).toBe(1);
    expect(setInterval.mock.calls[0][1]).toBe(1000);

  });

});