测试React事件处理程序

时间:2017-03-23 18:25:11

标签: reactjs sinon enzyme

有人可以告诉我如何测试看起来像这样的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'参数的内容。如果我使用酶,那么测试就会失败,除非我将事件处理程序存根为'e'未定义。

这是酶测试位......

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

我简单地看一下使用'mount'而不是'shallow',但我根本无法运行。这有很多问题,例如在尝试执行之前无法查找诸如查找下拉列表的数据加载之类的事情。

这是我正在尝试(理想情况下)...

    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的装载是要走的路但是它引入了很多自己的问题而且我的spec文件已经是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);
        })
    });

1 个答案:

答案 0 :(得分:2)

例如,您可以模拟调用方法的操作并检查模拟后的组件状态。

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,请尝试从official documentation

开始使用此示例

希望它有所帮助。