34

I'm writing Jest tests for my React code and hoping to make use of/test the PropType checks. I am quite new to the Javascript universe. I'm using npm to install react-0.11.2 and have a simple:

var React = require('react/addons');

In my tests. My test looks quite similar to the jest/react tutorial example with code like:

var eventCell = TestUtils.renderIntoDocument(
  <EventCell
    slot={slot}
    weekId={weekId}
    day={day}
    eventTypes={eventTypes}
    />
);

var time = TestUtils.findRenderedDOMComponentWithClass(eventCell, 'time');
expect(time.getDOMNode().textContent).toEqual('19:00 ');

However it seems that the PropType checks in the EventCell component aren't being triggered. I understand that the checks are only run in Development mode but then I also thought that getting react through npm gave you the development version. The checks trigger in my browser when I build the component with watchify.

What am I missing?

machineghost
  • 28,573
  • 26
  • 128
  • 197
MichaelJones
  • 1,120
  • 1
  • 11
  • 21
  • 1
    Can you elaborate on the `PropType` you are using? I test mine by spying on `console.warn`. – srph Apr 26 '15 at 03:17
  • For EventCell, I'm using number.isRequired and object.isRequired. I've not looked at it for a long time and I'm not sure how to go about using console.warn. Is the console available in node.js which is running the jest test? – MichaelJones Apr 28 '15 at 17:34
  • Yes, it should available. – srph Apr 29 '15 at 05:55
  • Does running your tests with `NODE_ENV=development` solve the problem? – Patrick Jun 08 '15 at 19:26

6 Answers6

39

The underlying problem is How to test console.log?

The short answer is that you should replace the console.{method} for the duration of the test. The common approach is to use spies. In this particular case, you might want to use stubs to prevent the output.

Here is an example implementation using Sinon.js (Sinon.js provides standalone spies, stubs and mocks):

import {
    expect
} from 'chai';
import DateName from './../../src/app/components/DateName';
import createComponent from './create-component';
import sinon from 'sinon';

describe('DateName', () => {
    it('throws an error if date input does not represent 12:00:00 AM UTC', () => {
        let stub;

        stub = sinon.stub(console, 'error');

        createComponent(DateName, {date: 1470009600000});

        expect(stub.calledOnce).to.equal(true);
        expect(stub.calledWithExactly('Warning: Failed propType: Date unix timestamp must represent 00:00:00 (HH:mm:ss) time.')).to.equal(true);

        console.error.restore();
    });
});

In this example DataName component will throw an error when initialised with a timestamp value that does not represent a precise date (12:00:00 AM).

I am stubbing the console.error method (This is what Facebook warning module is using internally to generate the error). I ensure that the stub has been called once and with exactly one parameter representing the error.

dylan-myers
  • 176
  • 2
  • 15
Gajus
  • 55,791
  • 58
  • 236
  • 384
  • I think this answer should be marked as the accepted answer.It's a much cleaner solution and provides a test to the exact behaviour the browser would have. – Ricardo Brazão Oct 20 '15 at 17:47
  • Is there any reason `sinon` is being used here? (I've used `jasmine` and `spyOn` doesn't seem to be rest between specs). Am wondering perhaps certain test frameworks (`sinon`?) do this better. – AdamT Jan 03 '16 at 00:35
  • @AdamT I assume because the example uses Chai as an assertion library, which isn't as fully-featured as Jasmine. I don't believe there is a significant difference between Sinon and Jasmine. – Joshua Comeau Jan 13 '16 at 12:57
  • If the expectation fails, how does the console get restored? Seems like the stub should in a beforeEach and the restore in an afterEach. – boatcoder Jan 09 '17 at 14:45
  • 1
    See [this other SO question/answer](http://stackoverflow.com/a/41916993/5218951) for a discussion of some inherent limitations to using a spy/mock of `console.error` for testing `propTypes`. – Andrew Willems Jan 29 '17 at 02:21
  • 2
    This solution is not suitable for tests, for the reasons linked to by @AndrewWillems. I just published a package that provides an alternative checkPropTypes function that is suitable for use in tests: https://github.com/ratehub/check-prop-types – Phil Jul 06 '17 at 20:09
  • This doesnt work for me - Expected: true Received: false. Assume the stub is never called – alexr89 Dec 23 '20 at 11:20
9

Intro

The answer by @Gajus definitely helped me (so, thanks Gajus). However, I thought I would provide an answer that:

  • Uses a more up-to-date React (v15.4.1)
  • Uses Jest (which comes with React)
  • Allows testing multiple prop values for a single prop
  • Is more generic

Summary

Like the approach suggested here by Gajus and elsewhere by others, the basic approach I'm suggesting is also to determine whether or not console.error is used by React in response to an unacceptable test prop value. Specifically, this approach involves doing the following for each test prop value:

  • mocking and clearing console.error (to ensure prior calls to console.error aren't interfering),
  • creating the component using the test prop value under consideration, and
  • confirming whether or not console.error was fired as expected.

The testPropTypes Function

The following code can be placed either within the test or as a separate imported/required module/file:

const testPropTypes = (component, propName, arraysOfTestValues, otherProps) => {
    console.error = jest.fn();
    const _test = (testValues, expectError) => {
        for (let propValue of testValues) {
            console.error.mockClear();
            React.createElement(component, {...otherProps, [propName]: propValue});
            expect(console.error).toHaveBeenCalledTimes(expectError ? 1 : 0);
        }
    };
    _test(arraysOfTestValues[0], false);
    _test(arraysOfTestValues[1], true);
};

Calling the Function

Any test examining propTypes can call testPropTypes using three or four parameters:

  • component, the React component that is modified by the prop;
  • propName, the string name of the prop under test;
  • arraysOfTestValues, an array of arrays of all the desired test values of the prop to be tested:
    • the first sub-array contains all acceptable test prop values, while
    • the second sub-array contains all unacceptable test prop values; and
  • optionally, otherProps, an object containing prop name/value pairs for any other required props of this component.

    The otherProps object is needed to ensure React doesn't make irrelevant calls to console.error because other required props are inadvertently missing. Simply include a single acceptable value for any required props, e.g. {requiredPropName1: anyAcceptableValue, requiredPropName2: anyAcceptableValue}.

Function Logic

The function does the following:

  • It sets up a mock of console.error which is what React uses to report props of incorrect type.

  • For each sub-array of test prop values provided it loops through each test prop value in each sub-array to test for prop type:

    • The first of the two sub-arrays should be a list of acceptable test prop values.
    • The second should be of unacceptable test prop values.
  • Within the loop for each individual test prop value, the console.error mock is first cleared so that any error messages detected can be assumed to have come from this test.

  • An instance of the component is then created using the test prop value as well as any other necessary required props not currently being tested.

  • Finally, a check is made to see whether a warning has been triggered, which should happen if your test tried to create a component using an inappropriate or missing prop.

Testing for Optional versus Required Props

Note that assigning null (or undefined) to a prop value is, from React's perspective, essentially the same thing as not providing any value for that prop. By definition this is acceptable for an optional prop but unacceptable for a required one. Thus, by placing null in either the array of acceptable or unacceptable values you test whether that prop is optional or required respectively.

Example Code

MyComponent.js (just the propTypes):

MyComponent.propTypes = {
    myProp1: React.PropTypes.number,      // optional number
    myProp2: React.PropTypes.oneOfType([  // required number or array of numbers
        React.PropTypes.number,
        React.PropTypes.arrayOf(React.PropTypes.number)
    ]).isRequired

MyComponent.test.js:

describe('MyComponent', () => {

    it('should accept an optional number for myProp1', () => {
        const testValues = [
            [0, null],   // acceptable values; note: null is acceptable
            ['', []] // unacceptable values
        ];
        testPropTypes(MyComponent, 'myProp1', testValues, {myProp2: 123});
    });

    it('should require a number or an array of numbers for myProp2', () => {
        const testValues = [
            [0, [0]], // acceptable values
            ['', null] // unacceptable values; note: null is unacceptable
        ];
        testPropTypes(MyComponent, 'myProp2', testValues);
    });
});

Limitation of This Approach (IMPORTANT)

There are currently some significant limitations on how you can use this approach which, if over-stepped, could be the source of some hard-to-trace testing bugs. The reasons for, and implications of, these limitations are explained in this other SO question/answer. In summary, for simple prop types, like for myProp1, you can test as many unacceptable non-null test prop values as you want as long as they are all of different data types. For some complex prop types, like for myProp2, you can only test a single unacceptable non-null prop value of any type. See that other question/answer for a more in-depth discussion.

Community
  • 1
  • 1
Andrew Willems
  • 9,768
  • 8
  • 39
  • 58
  • i think you made a typo on the testPropTypes function, should it be: React.createElement(component, {...otherProps, [propName]: propValue}); ?? (notice component instead of Toolbar) – qbantek Feb 02 '17 at 19:47
  • You're right. It's fixed now. As you may have guessed, the typo was a left-over from converting my own specific code into something generic that was more appropriate for StackOverflow. Thanks. – Andrew Willems Feb 02 '17 at 20:02
  • I'm not sure how many people do this, but I have a custom validator function which, internally, is calling `PropTypes.checkPropTypes(...)`. Using `console.error = jest.fn()`, as explained here, was the solution to testing the validator function that worked for me. – Zac Seth Sep 06 '17 at 14:41
6

Mocking console.error is not suitable for use in unit tests! @AndrewWillems linked to another SO question in a comment above that describes the problems with this approach.

Check out this issue on facebook/prop-types for discussion about the ability for that library to throw instead of logging propType errors (at the time of writing, it's not supported).

I have published a helper library to provide that behaviour in the mean time, check-prop-types. You can use it like this:

import PropTypes from 'prop-types';
import checkPropTypes from 'check-prop-types';

const HelloComponent = ({ name }) => (
  <h1>Hi, {name}</h1>
);

HelloComponent.propTypes = {
  name: PropTypes.string.isRequired,
};

let result = checkPropTypes(HelloComponent.propTypes, { name: 'Julia' }, 'prop', HelloComponent.name);
assert(`result` === null);

result = checkPropTypes(HelloComponent.propTypes, { name: 123 }, 'prop', HelloComponent.name);
assert(`result` === 'Failed prop type: Invalid prop `name` of type `number` supplied to `HelloComponent`, expected `string`.');
Phil
  • 849
  • 7
  • 19
5

A new package jest-prop-type-error is simple to add and fails on PropType errors:

Install via:

yarn add -D jest-prop-type-error

Then add the following to your package.json's setupFiles in the jest section:

"setupFiles": [
  "jest-prop-type-error"
]
YPCrumble
  • 20,703
  • 15
  • 86
  • 149
1

Since ReactJS will only send warnings to the console but will not actually throw an error, I test prop values in this way:

var myTestElement = TestUtils.renderIntoDocument(
<MyTestElement height={100} /> );

it("check MyTestElement props", function() {

   expect( typeof myTestElement.props.height ).toEqual ( 'number' );

});
Guy Laor
  • 417
  • 1
  • 4
  • 7
  • 2
    This way you're reimplementing the proptypes in the test. – Koen. Oct 06 '16 at 12:00
  • How do you test shape then? See for instance https://stackoverflow.com/questions/45736470/how-to-test-a-react-component-proptypes-validation – Gabe Aug 17 '17 at 13:38
0

For unit tests based on Jest, using this on your setup.js will fail any test where console.error (prop-type errors) or console.warn (React compat issues, like still using componentWillUpdate) ends up being called:

beforeEach(() => {
  jest.spyOn(console, 'error')
  jest.spyOn(console, 'warn')
})

afterEach(() => {
  /* eslint-disable no-console,jest/no-standalone-expect */
  expect(console.error).not.toBeCalled()
  expect(console.warn).not.toBeCalled()
})

This breaks when any test calls jest.restoreAllMocks() - for us, calling jest.clearAllMocks()` instead helped.

It also requires your app to not call console.error or console.warn for "error handling" (scare quotes, since that's usually not a good idea).

Jörn Zaefferer
  • 5,555
  • 3
  • 27
  • 34