如何通过Jest测试React的PropTypes?

36

我正在为我的React代码编写Jest测试,并希望利用/测试PropType检查。我对Javascript世界还很陌生。我使用npm安装了react-0.11.2并有一个简单的:

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

在我的测试中,我的测试看起来与jest/react教程示例非常相似,代码如下:
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 ');

然而,看起来在 EventCell 组件中的 PropType 检查并没有被触发。我知道这些检查只在开发模式下运行,但是我还以为通过 npm 安装 react 可以获取开发版本。当我使用 watchify 构建组件时,在浏览器中会触发检查。

我错过了什么?


1
你能详细说明一下你正在使用的 PropType 吗?我通过监听 console.warn 来测试我的。 - srph
对于EventCell,我正在使用number.isRequired和object.isRequired。我已经很久没有看它了,也不确定如何使用console.warn。控制台是否在运行jest测试的node.js中可用? - MichaelJones
是的,它应该可用。 - srph
运行测试时使用 NODE_ENV=development 能解决问题吗? - Patrick Burtchaell
8个回答

41

根本问题是如何测试console.log

简而言之,在测试期间,您应该替换console.{method}。常用的方法是使用spies。在这种特殊情况下,您可能需要使用stubs来防止输出。

以下是使用Sinon.js(Sinon.js提供独立的spies、stubs和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();
    });
});
在这个例子中,DataName组件将在使用不表示精确日期(12:00:00 AM)的时间戳值进行初始化时抛出错误。
我正在设置console.error方法的桩函数(这是Facebook的warning模块内部使用的方法来生成错误)。我确保该桩函数已被调用一次,并且仅带有一个表示错误的参数。

我认为这个答案应该被标记为被接受的答案。它是一个更干净的解决方案,并提供了一个测试来确定浏览器的确切行为。 - Ricardo Brazão
这里使用 sinon 有什么原因吗?(我已经使用了 jasmine,但是 spyOn 在规范之间似乎没有重置)。我想知道是否有某些测试框架(sinon?)可以更好地处理这个问题。 - AdamT
1
请参考 这个 SO 问题/答案 获取有关使用 console.error 的间谍/模拟进行测试 propTypes 的一些固有限制的讨论。 - Andrew Willems
2
这个解决方案不适用于测试,原因由@AndrewWillems链接提供。我刚刚发布了一个软件包,提供了一种替代的checkPropTypes函数,适用于测试使用:https://github.com/ratehub/check-prop-types - Phil
1
这对我不起作用 - 预期:true 收到:false。假设存根从未被调用。 - alexr89
显示剩余2条评论

14

简介

@Gajus的答案对我有所帮助,所以感谢Gajus。但是,我想提供一个更好的答案:

  • 使用更新的React(v15.4.1)
  • 使用React自带的Jest
  • 测试单个prop的多个值
  • 更通用

总结

和Gajus以及其他人建议的方法类似,我的基本方法也是确定React是否会在响应不可接受的测试prop值时使用console.error。具体来说,这种方法涉及以下每个测试prop值的操作:

  • 模拟并清除console.error(以确保先前对console.error的调用不会干扰),
  • 使用正在考虑的测试prop值创建组件,并
  • 确认是否按预期触发了console.error

testPropTypes函数

以下代码可以放在测试中或作为单独的导入/需要的模块/文件中:

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

调用函数

任何检查propTypes的测试都可以使用三个或四个参数来调用testPropTypes

  • component,被该属性修改的 React组件
  • propName,被测试的属性名称的字符串;
  • arraysOfTestValues,要测试的属性的所有所需测试值的数组的数组:
    • 第一个子数组包含所有可接受的测试属性值,而
    • 第二个子数组包含所有不可接受的测试属性值;以及
  • 可选的,otherProps,包含该组件其他所需属性的名称/值对的对象。

    需要otherProps对象是为了确保 React 不会因无意间缺少其他所需属性而进行无关紧要的 console.error 调用。只需为任何所需属性包括一个可接受的值,例如 {requiredPropName1: anyAcceptableValue, requiredPropName2: anyAcceptableValue}

函数逻辑

该函数执行以下操作:

  • 设置了console.error的模拟,这是 React 用于报告类型不正确的属性的方法。

  • 对于提供的每个测试属性值子数组,它循环遍历每个测试属性值来测试属性类型:

    • 两个子数组中的第一个应该是可接受的测试属性值列表。
    • 第二个子数组应为不可接受的测试属性值。
  • 在每个单独的测试属性值的循环中,首先清除console.error模拟,以便可以假定检测到的任何错误消息来自此测试。

  • 然后,使用测试属性值和当前未测试的任何其他所需属性之一创建组件实例

  • 最后,检查是否已触发警告,如果您的测试尝试使用不合适或丢失的属性创建组件,则应该会发生这种情况。

测试可选和必需属性

请注意,将null(或undefined)分配给属性值从React的角度来看与不为该属性提供任何值本质上相同。定义上,这对于可选的 prop 是可接受的,但对于必需的 prop 是不可接受的。因此,通过将null放入可接受或不可接受值的任一数组中,您可以测试该属性是可选还是必需

示例代码

MyComponent.js(仅包含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);
    });
});

该方法的局限性(重要)

目前对于这种方法有一些显著的限制,如果超出限制,可能是一些难以追踪的测试错误的来源。这些限制的原因和影响在另一个SO问题/答案中有解释。简而言之,对于像myProp1这样的简单属性类型,可以测试多个不可接受的非null测试属性值,只要它们都是不同的数据类型。对于某些复杂的属性类型(例如myProp2),只能测试任意类型的单个不可接受的非null属性值。请参阅其他问题/答案进行更深入的讨论。


我认为你在testPropTypes函数中打错了字,应该是:React.createElement(component, {...otherProps, [propName]: propValue}); ??(注意component而不是Toolbar) - qbantek
你说得对。现在已经修复了。正如你所猜测的那样,这个错别字是将我自己特定的代码转换为更适合于StackOverflow的通用代码时留下的。谢谢。 - Andrew Willems
我不确定有多少人这样做,但是我有一个自定义验证函数,在内部调用PropTypes.checkPropTypes(...)。根据这里的说明使用 console.error = jest.fn() 是测试验证函数的解决方案,对我很有效。 - Zac Seth

7

在单元测试中,模拟 console.error 不太适合使用!@AndrewWillems 在上面的评论中链接了另一个 SO 问题,其中描述了这种方法的问题。

查看 facebook/prop-types 上的此问题,了解有关该库能否抛出而不是记录 propType 错误的讨论(撰写本文时,不支持此功能)。

我发布了一个辅助库来提供该行为,check-prop-types。您可以像这样使用它:

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`.');

5

一款名为jest-prop-type-error的新包很容易添加,并且可以在PropType错误时失败:

安装方式如下:

yarn add -D jest-prop-type-error

接下来,在 jest 部分的 package.json 文件中添加以下内容到 setupFiles 中:

"setupFiles": [
  "jest-prop-type-error"
]

1

由于ReactJS只会将警告发送到控制台,而不会真正抛出错误,因此我会以这种方式测试属性值:

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

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

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

});

2
这样做会在测试中重新实现proptypes。 - Koen.
你如何测试形状呢?可以参考 https://stackoverflow.com/questions/45736470/how-to-test-a-react-component-proptypes-validation - Gabe

1

对于基于Jest的单元测试,如果在setup.js中使用此代码,任何调用了console.error(prop-type错误)或console.warn(React兼容性问题,如仍在使用componentWillUpdate)的测试都将失败:

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

这段代码会在任何测试调用 jest.restoreAllMocks() 时出错 - 对我们来说,使用 jest.clearAllMocks() 代替可以解决问题。
此外,你的应用还需要避免通过 console.errorconsole.warn 进行 "错误处理"(引号中的“错误处理”,因为通常不是一个好主意)。

你能解释一下为什么有 eslint-disable 的注释吗? - richardsonwtr

0
这是我如何使用 Sinon 在 React 中测试 PropType 错误的方法。此外,为了检查每个缺失原型的错误,请尝试 console.log(sinon.assert.notCalled(console.error))
import { expect } from 'chai';
import DateName from './../../src/app/components/DateName';
import sinon from 'sinon';
    
describe('DateName', () => {

  function renderComponent(date) {
    return render(
        <DateName date={date} />
    );
  }

  it('throws an error if date input does not represent 12:00:00 AM UTC', () => {
          let stub;
          stub = sinon.stub(console, 'error');
          renderComponent(undefined);
          expect(stub.calledOnce).toEqual(true);
          sinon.assert.calledWithMatch(console.error,'Warning: Failed %s type: %s%s', 'prop', 'The prop `date` is marked as required in `DateName`, but its value is `undefined`.');
          console.error.restore();
 });
});

0
使用React测试库https://reactjs.org/docs/testing-recipes.html

Dog.js

import PropTypes from 'prop-types';

export default class Dog {
    constructor(breed, dob, weight) {
        this.breed = breed;
        this.dob = dob;
        this.weight = weight;
    }
}

Dog.propTypes = {
    breed: PropTypes.string,
    dob: PropTypes.instanceOf(Date),
    weight: PropTypes.number,
};

Dog.spec.js

import PropTypes from 'prop-types';
import Dog from './Dog';

describe('Dog.js tests', () => {
    const mockData = {
        breed: 'Shiba Inu',
        dob: new Date('March 14, 2017 08:30:00'),
        weight: 21,
    }

    const instance = {
        name: 'MockDogComponent',
        model: new Dog(mockData),
    };

    beforeEach(() => {
        const { breed, dob, weight } = mockData;
        instance.model = new Dog(breed, dob, weight);
    });

    it('should have valid propTypes', () => {        
        Object.keys(instance.model).forEach((prop) => 
            expect(PropTypes
                .checkPropTypes(Dog.propTypes, instance.model, prop, instance.name))
                .toBeUndefined()
            );
    });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接