Протестируйте функцию React Component с помощью Jest
оригинал
Прежде всего, я слежу за архитектурой Flux.
У меня есть индикатор, который показывает количество секунд, например: 30 секунд. Каждую секунду он показывает на 1 секунду меньше, поэтому 29, 28, 27 до 0. Когда прибывает в 0, я очищаю интервал, чтобы он перестал повторяться. Более того, я запускаю действие. Когда это действие отправляется, мой магазин уведомляет меня. Поэтому, когда это происходит, я сбрасываю интервал до 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;
Компонент работает как положено. Теперь я хочу проверить это с Джестом. Я знаю, что могу использовать renderIntoDocument, затем я могу установить значение 30 секунд и проверить, равен ли мой component.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, основанное на ответе Яна
Это тест, который я пытаюсь запустить (см. Комментарии в некоторых строках):
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)
});
});
Я все еще что-то недопонимаю...
2 ответа
Похоже, вы на правильном пути. Просто чтобы убедиться, что все на одной странице с этим ответом, давайте разберемся с терминологией.
Макет: функция, поведение которой контролируется модульным тестом. Обычно вы заменяете реальные функции на некотором объекте фиктивной функцией, чтобы убедиться, что фиктивная функция вызывается правильно. Jest обеспечивает макеты для каждой функции в модуле автоматически, если вы не вызываете jest.dontMock
на имя этого модуля.
Класс компонента: это вещь, возвращаемая React.createClass
, Вы используете его для создания экземпляров компонентов (это сложнее, но этого достаточно для наших целей).
Компонентный экземпляр: фактический визуализированный экземпляр класса компонента. Это то, что вы получите после звонка TestUtils.renderIntoDocument
или многие другие TestUtils
функции.
В обновленном примере из вашего вопроса вы генерируете макеты и присоединяете их к классу компонента, а не к экземпляру компонента. Кроме того, вы хотите только макетировать функции, которые вы хотите отслеживать или иным образом изменять; например, вы издеваетесь _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's 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();
});
});
Я думаю, что я понимаю, что вы спрашиваете, по крайней мере, часть этого!
Начиная с ошибки, причина, по которой вы видите это, состоит в том, что вы дали команду jest не издеваться над модулем индикатора, чтобы все внутренние компоненты были такими, как вы их написали. Если вы хотите проверить, как вызывается эта конкретная функция, я бы предложил вам создать фиктивную функцию и использовать ее вместо этого...
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, чтобы игнорировать реагирующие модули, иначе они также будут автоматически подвергаться насмешкам.
Полный предлагаемый тест
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);
});
});