pH200 / cycle-react

Rx functional interface to Facebook's React
MIT License
370 stars 18 forks source link

Testing cycle-react components #30

Closed dotfold closed 5 years ago

dotfold commented 8 years ago

I'm looking for some guidance on how would be the best way to write tests against my own cycle components.

I am able to test simple props by rendering to static markup and asserting against the jsx:

(excluded imports)

// button.jsx
const Button = Cycle.component('Button', (interactions, props) => {
  const onButtonClick = interactions.get('onButtonClick')

  return {
    view: props.distinctUntilChanged().map(({ label }) => {
      return (
        <button
          className="button radius"
          onClick={interactions.listener('onButtonClick')}>
          {label}
        </button>)
    }),
    events: {
      onClick: onButtonClick
    }
  }
})

export default Button

// button.spec.js
t.test('it renders the correct label from props', (t) => {
  const output = renderToStaticMarkup(<Button label="fancy" />)
  t.plan(1)
  t.jsxEquals(output, '<button class="button radius">fancy</button>')
  t.end();
})

I can't seem to wrap my head around how to test the interactions / handlers - in this case the onClick. Obviously if I pass in a simple function as the onClick I can test this easily. But how to test the interactions is quite different.

Would we need to provide mocks for these - something similar to https://github.com/erykpiast/cyclejs-mock?

Any help / guidance is much appreciated.

corps commented 8 years ago

Well, for one, you can use all the normal slew of React test utils to simulate events and such: https://facebook.github.io/react/docs/test-utils.html

This has been my approach because most of my team is already familiar with React.

corps commented 8 years ago

Also, if you wish to run these tests within node, you can fake the dom with a library like https://github.com/tmpvar/jsdom

pH200 commented 8 years ago

Good question. There are two ways of testing your components. One is to test the result of your logics in the component. Like in https://github.com/pH200/cycle-react/blob/v4.0.0/examples/web/many/many-model.js You can mock the events (intentions as in the example code) by using Subject or controlled and test the result of the Observable.

A more direct way is to use test utils, as mentioned by corps. For example:

// button.js
const Button = component('Button', (interactions) => {
  const click$ = interactions.get('onButtonClick');
  return click$
    .map(() => 'clicked')
    .startWith('not clicked')
    .map(buttonStatus => (
      <button onClick={interactions.listener('onButtonClick')}>
        {buttonStatus}
      </button>
    ));
});

export default Button;

// test.js
// Both jsdom and jsdom-global are need to be installed
require('jsdom-global')();
import ReactTestUtils from 'react-addons-test-utils';
import Button from '../components/button';

t.test('clicking button should change the textContent', (t) => {
  t.plan(2);

  function test(button) {
    t.equals(button.textContent, 'not clicked');
    ReactTestUtils.Simulate.click(button);
    t.equals(button.textContent, 'clicked');
    t.end();
  }

  const TestRoot = React.createClass({
    componentDidMount() {
      const button = ReactDOM.findDOMNode(this.refs.button);
      test(button);
    },
    render() {
      return <Button ref="button" />
    }
  });

  ReactTestUtils.renderIntoDocument(<TestRoot />);
});
dotfold commented 8 years ago

Thanks guys for your responses. @pH200 that example code is very useful, cheers.

Can you elaborate a bit more on the first option you mentioned? One would pass in a controlled Subject to the model, and assert against the results of that.. but in this scenario, we are not actually involving the Button in the tests? How is the mocked intent(s) supplied to the component under test (the Button in our examples above)? Sorry, does that make sense?

I guess what I am after is kind of pushing the boundary of a unit test into integration territory?

pH200 commented 5 years ago

cycle-react 7.0 no longer support Observable that works as component.