Probar una función de componente React con Jest


Original

En primer lugar, estoy siguiendo la arquitectura Flux.

Tengo un indicador que muestra un número de segundos, por ejemplo: 30 segundos. Cada segundo muestra 1 segundo menos, así que 29, 28, 27 hasta 0. Cuando llega a 0, borro el intervalo para que deje de repetirse. Además, desencadenaré una acción. Cuando esta acción se envía, mi tienda me notifica. Así que cuando esto sucede, restablezco el intervalo a 30s y así sucesivamente. El componente se parece a:

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;

Componente funciona como se esperaba. Ahora, quiero probarlo con Broma. Sé que puedo usar renderIntoDocument, luego puedo establecer Timeout de 30s y verificar si mi componente.estado.elapsed es igual a 0 (por ejemplo).

Pero, lo que quiero probar aquí son cosas diferentes. Quiero probar si la función refresh se llama. Además, me gustaría probar que cuando mi estado transcurrido es 0, activa mi TriggerAnAction(). Ok, para lo primero que traté de hacer:

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

  });
});

Pero recibo la siguiente error al escribir la prueba npm:

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

Vi de ReactTestUtils una función mockComponent pero dada su explicación, no estoy seguro si es lo que necesito.

Ok, en este punto, estoy atascado. ¿Puede alguien darme alguna luz sobre cómo probar esas dos cosas que mencioné anteriormente?


Actualización 1, basada en la respuesta de Ian

Esa es la prueba que estoy tratando de ejecutar (ver comentarios en algunas líneas):

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)

  });
});

Todavía estoy malinterpretando algo...

Author: James Montagne, 2014-08-27

2 answers

Parece que estás en el camino correcto. Solo para asegurarnos de que todos están en la misma página para esta respuesta, quitemos algo de terminología del camino.

Mock: Una función con comportamiento controlado por la prueba unitaria. Generalmente intercambiar funciones reales en algún objeto con un simulacro de función para asegurarse de que el simulacro de la función es correctamente llamado. Jest proporciona simulaciones para cada función en un módulo automáticamente a menos que llame a jest.dontMock en el módulo de nombre.

Component Class : Esta es la cosa devuelta por React.createClass. Se usa para crear instancias de componentes (es más complicado que eso, pero esto es suficiente para nuestros propósitos).

Instancia de componente : Una instancia renderizada real de una clase de componente. Esto es lo que obtendrías después de llamar TestUtils.renderIntoDocument o muchas de las otras funciones TestUtils.


En el ejemplo actualizado de tu pregunta, estás generando simulaciones y adjuntándolas al componente claseen lugar de una instancia del componente. Además, solo desea burlarse de las funciones que desea monitorear o cambiar de otra manera; por ejemplo, se burla de _onChange, pero realmente no lo desea, porque desea que se comporte normalmente; solo refresh quiere burlarse.

Aquí hay una propuesta de conjunto de pruebas que escribí para este componente; los comentarios están en línea, así que publique un comentario si tiene alguna pregunta. La fuente de trabajo completa para este ejemplo y el conjunto de pruebas está en https://github.com/BinaryMuse/so-jest-react-mock-example/tree/master ; debería ser capaz de clonarlo y ejecutarlo sin problemas. Tenga en cuenta que tuve que hacer algunas conjeturas menores y cambios en el componente ya que no todos los módulos referenciados estaban en su pregunta original.

/** @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();
  });
});
 46
Author: Michelle Tilley,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-30 02:47:10

Creo que entiendo lo que estás pidiendo, ¡al menos parte de ello!

Comenzando con el error, la razón por la que lo está viendo es porque le ha dado instrucciones a jest para que no se burle del módulo Indicador para que todas las partes internas estén como las ha escrito. Si quieres probar que se llama a esa función en particular, te sugiero que crees una función simulada y la uses en su lugar...

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

Lo siguiente a tener en cuenta es que en realidad está reasignando la variable indicadora en su código de ejemplo para un comportamiento adecuado cambiaría el nombre de la segunda variable (como a continuación)

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

Finalmente, si desea probar algo que cambia con el tiempo, use las características de TestUtils en torno a la manipulación del temporizador ( http://facebook.github.io/jest/docs/timer-mocks.html ). En tu caso creo que puedes hacer:

jest.runAllTimers();

expect(refresh).toBeCalled();

Alternativamente, y quizás un poco menos quisquilloso es confiar en las implementaciones simuladas de setTimeout y setInterval para razonar sobre tu componente:

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

Otro cosa, para que cualquiera de los cambios anteriores funcione, creo que necesitará activar manualmente el método onChange ya que su componente inicialmente funcionará con una versión burlada de su Tienda para que no se produzcan eventos de cambio. También tendrás que asegurarte de que has configurado jest para ignorar los módulos de react, de lo contrario también se burlarán automáticamente.

Prueba propuesta completa

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

  });

});
 6
Author: Ian Thomas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-28 05:55:47