Skip to content

Instantly share code, notes, and snippets.

@shaohua
Forked from joshdover/README.md
Created July 13, 2016 16:55
Show Gist options
  • Save shaohua/e61661d4cf63e82e553ebd0f1d876086 to your computer and use it in GitHub Desktop.
Save shaohua/e61661d4cf63e82e553ebd0f1d876086 to your computer and use it in GitHub Desktop.

Revisions

  1. @joshdover joshdover revised this gist Jul 13, 2016. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -36,8 +36,8 @@ const { component } = getComponent({ onChange: someFunc });
    There are 4 primary patterns that I've identified, have more ideas? Provide examples and rationale in the comments!

    - [**Basic rendering**](#file-test_render-js): how props change render results
    - [**Stateless interactions**](#file-test_stateless_interactions-js): how interactions change render results or interact with callbacks supplied as props
    - [**Stateful interactions**](#file-test_stateful_interactions-js): how interactions change render results relying on `this.state` or how lifecycle hooks behave
    - [**Stateless interactions**](#file-test_stateless_interaction-js): how interactions change render results or interact with callbacks supplied as props
    - [**Stateful interactions**](#file-test_stateful_interaction-js): how interactions change render results relying on `this.state` or how lifecycle hooks behave
    - [**Instance methods**](#file-test_instance_methods-js): how the component interacts with external libraries or APIs (very rare)

    ## Disclaimer
  2. @joshdover joshdover revised this gist Jul 13, 2016. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -35,9 +35,9 @@ const { component } = getComponent({ onChange: someFunc });

    There are 4 primary patterns that I've identified, have more ideas? Provide examples and rationale in the comments!

    - [**Basic rendering**](test_render.js): how props change render results
    - [**Stateless interactions**](test_stateless_interactions.js): how interactions change render results or interact with callbacks supplied as props
    - [**Stateful interactions**](test_stateful_interactions.js): how interactions change render results relying on `this.state` or how lifecycle hooks behave
    - [**Basic rendering**](#file-test_render-js): how props change render results
    - [**Stateless interactions**](#file-test_stateless_interactions-js): how interactions change render results or interact with callbacks supplied as props
    - [**Stateful interactions**](#file-test_stateful_interactions-js): how interactions change render results relying on `this.state` or how lifecycle hooks behave
    - [**Instance methods**](#file-test_instance_methods-js): how the component interacts with external libraries or APIs (very rare)

    ## Disclaimer
  3. @joshdover joshdover revised this gist Jul 13, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -38,7 +38,7 @@ There are 4 primary patterns that I've identified, have more ideas? Provide exam
    - [**Basic rendering**](test_render.js): how props change render results
    - [**Stateless interactions**](test_stateless_interactions.js): how interactions change render results or interact with callbacks supplied as props
    - [**Stateful interactions**](test_stateful_interactions.js): how interactions change render results relying on `this.state` or how lifecycle hooks behave
    - [**Instance methods**](test_instance_methods.js): how the component interacts with external libraries or APIs (very rare)
    - [**Instance methods**](#file-test_instance_methods-js): how the component interacts with external libraries or APIs (very rare)

    ## Disclaimer

  4. @joshdover joshdover revised this gist Jul 13, 2016. 1 changed file with 4 additions and 4 deletions.
    8 changes: 4 additions & 4 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -35,10 +35,10 @@ const { component } = getComponent({ onChange: someFunc });

    There are 4 primary patterns that I've identified, have more ideas? Provide examples and rationale in the comments!

    - **Basic rendering**: how props change render results
    - **Stateless interactions**: how interactions change render results or interact with callbacks supplied as props
    - **Stateful interactions**: how interactions change render results relying on `this.state` or how lifecycle hooks behave
    - **Instance methods**: how the component interacts with external libraries or APIs (very rare)
    - [**Basic rendering**](test_render.js): how props change render results
    - [**Stateless interactions**](test_stateless_interactions.js): how interactions change render results or interact with callbacks supplied as props
    - [**Stateful interactions**](test_stateful_interactions.js): how interactions change render results relying on `this.state` or how lifecycle hooks behave
    - [**Instance methods**](test_instance_methods.js): how the component interacts with external libraries or APIs (very rare)

    ## Disclaimer

  5. @joshdover joshdover revised this gist Jul 13, 2016. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -42,8 +42,8 @@ There are 4 primary patterns that I've identified, have more ideas? Provide exam

    ## Disclaimer

    You may have a different testing stack and YMMV with how well these patterns work within that environment. For reference
    the stack we use at Cratejoy and the one I've had the most success with:
    You may have a different testing stack and YMMV with how well these patterns work within that environment. For reference,
    here is the stack we use at Cratejoy and the one I've had the most success with:

    - [Karma](https://github.com/karma-runner/karma) test runner (configured with browserify + babel)
    - [Mocha](https://github.com/mochajs/mocha) test framework
  6. @joshdover joshdover revised this gist Jul 13, 2016. No changes.
  7. @joshdover joshdover revised this gist Jul 13, 2016. 1 changed file with 51 additions and 0 deletions.
    51 changes: 51 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,51 @@
    # Idiomatic React Testing Patterns

    Testing React components seems simple _at first_. Then you need to test something that isn't a pure interaction and
    things seem to break down. These 4 patterns should help you use a pattern that is repeatable and readable for the type
    of test you need.

    ## Setup

    I recommend doing all setup in the most functional way possible. If you can avoid it, don't set variables in a
    `beforeEach`. This will help ensure tests are isolated and make things a bit easier to reason about. I use a pattern
    that gives great defaults for each test example but allows every example to override `props` when needed:

    ```js
    const getComponent = (props = {}) => {
    // Any test can override the default props by passing an object to the getComponent function
    props = Object.assign({
    onChange: sinon.spy(),
    title: 'Test Title',
    color: 'red'
    }, props);

    const component = ReactDOM.findDOMNode(TestUtils.renderIntoDocument(
    <MyComponent {...props} />
    ));

    return Object.assign(props, { component });
    };

    // Usage
    const { component, onChange } = getComponent();
    const { component } = getComponent({ onChange: someFunc });
    ```

    ## The Patterns

    There are 4 primary patterns that I've identified, have more ideas? Provide examples and rationale in the comments!

    - **Basic rendering**: how props change render results
    - **Stateless interactions**: how interactions change render results or interact with callbacks supplied as props
    - **Stateful interactions**: how interactions change render results relying on `this.state` or how lifecycle hooks behave
    - **Instance methods**: how the component interacts with external libraries or APIs (very rare)

    ## Disclaimer

    You may have a different testing stack and YMMV with how well these patterns work within that environment. For reference
    the stack we use at Cratejoy and the one I've had the most success with:

    - [Karma](https://github.com/karma-runner/karma) test runner (configured with browserify + babel)
    - [Mocha](https://github.com/mochajs/mocha) test framework
    - [Chai](https://github.com/chaijs/chai) expect library
    - [Sinon](https://github.com/sinonjs/sinon) mock library
  8. @joshdover joshdover created this gist Jul 13, 2016.
    38 changes: 38 additions & 0 deletions test_instance_methods.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,38 @@
    import { expect } from 'chai';
    import sinon from 'sinon';

    import React from 'react';
    import ReactDOM from 'react-dom';
    import TestUtils from 'react-addons-test-utils';

    import MyComponent from './MyComponent';

    /*
    * Pattern to be used for testing instance methods of components. These should not be used to test impementation details.
    * Examples of good applications of this pattern:
    * - Testing interactions with a stateful DOM API (eg. iframe). NOTE: components should not interact with DOM APIs
    * that are not related to visual display.
    * - Test interactions with an external UI library (eg. an image editor like Aviary)
    */
    describe('MyComponent', () => {
    const getComponent = (props = {}) => {
    props = Object.assign({
    onChange: sinon.spy(),
    }, props);

    const node = document.createElement('div');
    // Notice the different rendering method here
    const component = ReactDOM.render(
    <MyComponent {...props} />
    ), node);

    return Object.assign(props, { component });
    };

    describe('myMethod', () => {
    it('returns some value', () => {
    const { component } = getComponent();
    expect(component.myMethod()).to.equal('some value');
    });
    });
    });
    31 changes: 31 additions & 0 deletions test_render.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,31 @@
    import { expect } from 'chai';
    import sinon from 'sinon';

    import React from 'react';
    import ReactDOM from 'react-dom';
    import TestUtils from 'react-addons-test-utils';

    import MyComponent from './MyComponent';

    /*
    * Pattern to be used to assert basic rendering expectations, including how props change the output.
    */
    describe('MyComponent', () => {
    const getComponent = (props = {}) => {
    // Any test can override the default props by passing an object to the getComponent function
    props = Object.assign({
    onChange: sinon.spy(),
    }, props);

    const component = ReactDOM.findDOMNode(TestUtils.renderIntoDocument(
    <MyComponent {...props} />
    ));

    return Object.assign(props, { component });
    };

    it('renders a h1 for title prop', () => {
    const { component } = getComponent({ title: 'My Label' });
    expect(component.querySelector('h1').innerText).to.equal('My Label');
    });
    });
    43 changes: 43 additions & 0 deletions test_stateful_interaction.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,43 @@
    import { expect } from 'chai';
    import sinon from 'sinon';

    import React from 'react';
    import ReactDOM from 'react-dom';
    import TestUtils from 'react-addons-test-utils';

    import MyComponent from './MyComponent';

    /*
    * To be used when testing a component that is _NOT_ pure (uses this.state) OR when testing lifecycle hooks
    * (eg. componentDidUpdate). This is accomplished by re-rendering the the component manually and then asserting
    * expectations.
    */
    describe('MyComponent', () => {
    const getComponent = (props = {}) => {
    props = Object.assign({
    onChange: sinon.spy(),
    }, props);

    const node = document.createElement('div');
    // Notice the different rendering method here
    const component = ReactDOM.render(
    <MyComponent {...props} />
    ), node);

    return Object.assign(props, { component, node });
    };

    context('when clicked', () => {
    it('adds some-class', () => {
    const props = getComponent();

    // Do some action that changes internal state
    TestUtils.Simulate.click(props.component);

    // Re-render (you can also change props here)
    ReactDOM.render(<MyComponent {...props} />, props.node);

    expect(component.className).includes('some-class');
    });
    });
    });
    34 changes: 34 additions & 0 deletions test_stateless_interaction.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,34 @@
    import { expect } from 'chai';
    import sinon from 'sinon';

    import React from 'react';
    import ReactDOM from 'react-dom';
    import TestUtils from 'react-addons-test-utils';

    import MyComponent from './MyComponent';

    /*
    * Pattern to be used to assert pure interaction expectations that do not require any lifecycle hooks or internal state.
    */
    describe('MyComponent', () => {
    const getComponent = (props = {}) => {
    props = Object.assign({
    onChange: sinon.spy(),
    }, props);

    const component = ReactDOM.findDOMNode(TestUtils.renderIntoDocument(
    <MyComponent {...props} />
    ));

    return Object.assign(props, { component });
    };

    context('when the component is changed', () => {
    it('calls onChange', () => {
    const { component, onChange } = getComponent();
    const inputNode = input.querySelector('input[type=text]');
    TestUtils.Simulate.change(inputNode, { target: { value: 'new' } });
    expect(onChange.calledWith('new')).to.be.true;
    });
    });
    });