测试React事件处理程序

4
有人可以告诉我如何测试一个类似这样的React组件事件处理程序吗?...
handleChange(e) {
    this.setObjectState(e.target.getAttribute("for").toCamelCase(), e.target.value);
}

setObjectState(propertyName, value) {
    let obj = this.state.registrationData;
    obj[propertyName] = value;
    this.setState({registrationData: obj});
}

我已经使用Enzyme编写了一个测试来测试渲染,并可以模拟事件以测试处理程序是否被调用,但这只是简单的部分。 我想测试事件代码运行时会发生什么。我可以手动触发它们,但我不知道在测试中应该传递什么到'e'参数。如果我使用Enzyme,则除非将事件处理程序存根为“ e”,否则测试将失败,因为“ e”未定义。

以下是Enzyme测试代码...

    describe("uses internal state to", () => {
        let stateStub = null;
        let formWrapper = null;

        beforeEach(() => {
            wrapper = shallow(<RegistrationForm />);
            instance = wrapper.instance();
            stateStub = sinon.stub(instance, 'setState');
            formWrapper = wrapper.find(Form.Wrapper);
        });

        afterEach(() => {
            stateStub.restore();
        });

        it("handle changing an email address", () => {
            formWrapper.find("[name='Email']").simulate('change')
            sinon.called(stateStub);
        })
    });

我简要研究了使用“挂载”而不是“浅渲染”,但我根本无法运行它。它有很多问题,比如不能在执行之前存根查找下拉菜单的数据加载。

这是我理想情况下要尝试的内容...

    describe("ALT uses internal state to", () => {
        let stateStub = null;
        let formWrapper = null;

        beforeEach(() => {
            wrapper = shallow(<RegistrationForm />);
            instance = wrapper.instance();
            stateStub = sinon.stub(instance, 'setState');
        });

        afterEach(() => {
            stateStub.restore();
        });

        it("handle changing an email address", () => {
            let e = 'some fake data - what IS this object?';
            instance.handleChange(e);
            sinon.called(stateStub);
            sinon.calledWith({registrationData: errr.. what?});
        })
    });

以下是完整的组件代码:

import ErrorProcessor from "./error-processor";
import Form from "../../../../SharedJs/components/form/index.jsx"
import HiddenState from "../../data/hidden-state"
import LookupRestServiceGateway from "../../../../SharedJs/data/lookup-rest-service-gateway"
import React from "react";
import RegistrationRestServiceGateway from "../../data/registration-rest-service-gateway"

export default class RegistrationForm extends React.Component {

    constructor(props) {
        super(props);
        this.state = props.defaultState || { 
            registrationData: {
                email: "",
                password: "",
                confirmPassword: "",
                firstName: "",
                lastName: "",
                employerID: null
            },
            registered: false,
            employersLookupData: []
        };

        this.formId = "registration-form";
        this.errorProcessor = new ErrorProcessor();
        this.employersDataSource = new LookupRestServiceGateway(`/api/lookups/employers/${HiddenState.getServiceOperatorCode()}`);
        this.registrationGateway = new RegistrationRestServiceGateway();

        this.handleChange = this.handleChange.bind(this);
        this.handleEmployerChange = this.handleEmployerChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    }

    handleChange(e) {
        this.setObjectState(e.target.getAttribute("for").toCamelCase(), e.target.value);
    }

    handleEmployerChange(e) {
        this.setObjectState("EmployerID", e.target.value);
    }

    handleSubmit() {
        this.submitRegistration();
    }

    componentDidMount() {
        this.loadLookupData();
    }

    loadLookupData() {
        this.employersDataSource.getListItems({ successCallback: (data) => {
            this.setState({ employersLookupData: data ? data.items : [] });
        }});
    }

    setObjectState(propertyName, value) {
        let obj = this.state.registrationData;
        obj[propertyName] = value;
        this.setState({registrationData: obj});
    }

    submitRegistration() {
        this.registrationGateway.register({
            data: this.state.registrationData,
            successCallback: (data, status, xhr) => {
                this.setState({registered: true});
                if (data.errors && data.errors.length) {
                    this.errorProcessor.processErrorObject(this.formId, xhr);
                }
            },
            errorCallback: (xhr) => {
                this.errorProcessor.processErrorObject(this.formId, xhr);
            }
        });
    }

    render() {
        return (this.state.registered ? this.renderConfirmation() : this.renderForm());
    }

    renderConfirmation() {
        return (
            <div className = "registration-form">
                <p>Your registration has been submitted. An email will be sent to you to confirm your registration details before you can log in.</p>
                <Form.ErrorDisplay />
            </div>
        );
    }

    renderForm() {
        return (
            <Form.Wrapper formId = {this.formId}
                          className = "registration-form form-horizontal"
                          onSubmit = {this.handleSubmit}>
                <h4>Create a new account.</h4>
                <hr/>
                <Form.ErrorDisplay />
                <Form.Line name = "Email" 
                           label = "Email" 
                           type = "email"
                           inputClassName = "col-md-10 col-sm-9" labelClassName = "col-md-2 col-sm-3"
                           value = {this.state.registrationData.email}
                           onChange = {this.handleChange} />
                <Form.Line name = "Password" 
                           label = "Password" 
                           type = "password"
                           inputClassName = "col-md-10 col-sm-9" labelClassName = "col-md-2 col-sm-3"
                           value = {this.state.registrationData.password}
                           onChange = {this.handleChange} />
                <Form.Line name = "ConfirmPassword" 
                           label = "Confirm Password" 
                           type = "password"
                           inputClassName = "col-md-10 col-sm-9" labelClassName = "col-md-2 col-sm-3"
                           value = {this.state.registrationData.confirmPassword}
                           onChange = {this.handleChange} />
                <Form.Line name = "FirstName" 
                           label = "First Name" 
                           inputClassName = "col-md-10 col-sm-9" labelClassName = "col-md-2 col-sm-3"
                           value = {this.state.registrationData.firstName}
                           onChange = {this.handleChange} />
                <Form.Line name = "LastName" 
                           label = "Last Name" 
                           inputClassName = "col-md-10 col-sm-9" labelClassName = "col-md-2 col-sm-3"
                           value = {this.state.registrationData.lastName}
                           onChange = {this.handleChange} />
                <Form.DropDownLine name = "EmployerID"
                                   label = "Employer"
                                   inputClassName = "col-md-10 col-sm-9" labelClassName = "col-md-2 col-sm-3"
                                   emptySelection = "Please select an employer&hellip;"
                                   onChange = {this.handleEmployerChange}
                                   selectedValue = {this.state.registrationData.employerID}
                                   items = {this.state.employersLookupData}/>                                    
                <Form.Buttons.Wrapper className="col-sm-offset-3 col-md-offset-2 col-md-10 col-sm-9">
                    <Form.Buttons.Submit text = "Register"
                                         icon = "fa-user-plus" />
                </Form.Buttons.Wrapper>     
            </Form.Wrapper>                                                                                                                  
        );
    }
}

RegistrationForm.PropTypes = {
    defaultState: React.PropTypes.object
}

我现在已经设法让它工作了,但这感觉非常非常糟糕——这让我想到Enzyme的mount是正确的选择,但这也带来了很多问题,而且我的规范文件已经比我的组件大10倍,这一切似乎都毫无意义...

    describe("uses internal state to", () => {
        let stateStub = null;
        let formWrapper = null;

        beforeEach(() => {
            wrapper = shallow(<RegistrationForm />);
            instance = wrapper.instance();
            formWrapper = wrapper.find(Form.Wrapper);
            stateStub = sinon.stub(instance, 'setState');
        });

        afterEach(() => {
            stateStub.restore();
        });

        it("handle changing an email address", () => {
            $("body").append(`<input type="email" for="Email" id="field" class="form-control form-control " maxlength="10000" value="">`);
            let node = $("#field")[0];
            node.value = "new@ddress.co.uk";
            instance.handleChange({target: node});
            sinon.assert.called(stateStub);
        })
    });

React提供了测试工具来模拟点击处理程序:https://facebook.github.io/react/docs/test-utils.html#simulate - chrisjlee
1
Enzyme可以做到这一点 - 我没有使用Jest。我也没有测试点击事件。不过,Jest的文档可能给了我一些启示。感谢提供链接。 - Keith Jackson
1个回答

3
您可以模拟调用方法并在模拟后检查组件状态,例如。
class Foo extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  render() {
    const { count } = this.state;
    return (
      <div>
        <div className={`clicks-${count}`}>
          {count} clicks
        </div>
        <a onClick={() => this.setState({ count: count + 1 })}>
          Increment
        </a>
      </div>
    );
  }
}

const wrapper = shallow(<Foo />);

expect(wrapper.find('.clicks-0').length).to.equal(1);
wrapper.find('a').simulate('click');
expect(wrapper.find('.clicks-1').length).to.equal(1);

如果您正在使用Jest,请尝试按照官方文档中的示例进行操作。希望对您有所帮助。

我已经尝试过这个了 - 我遇到的问题是不知道在那种情况下传递什么给handleChange。 - Keith Jackson
尝试模拟一个动作,并检查模拟后组件的状态。 - Rafael Berro
请查看此示例 - Rafael Berro
我已经花了几个小时来分析这个示例(完全深入Enzyme文档),但它们似乎非常依赖于点击事件(click centric),其中不需要传递任何参数,而我无法掌握的是传递给handleChange的参数值。 - Keith Jackson
这个方法是通过props传递的吗?如果是,为什么不模拟它呢?const handleClickStub = sinon.spy(); const component = shallow(<Component handleClick={handleClickStub} />); - Rafael Berro
不,它是一种终端组件,完全自包含,除了导入的数据类。 - Keith Jackson

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