Search code examples
reactjsreduxenzymeredux-form

Testing simple Redux-Form with Enzyme (where is value??)


I have the most simple redux-form connected to redux example :

import * as React from 'react';
import { Field, reduxForm } from 'redux-form';
import { connect } from 'react-redux';

class TestForm extends React.PureComponent {
    render() {
        return (
            <form>
                <Field component={Input} name={'testField'}/>
            </form>
        );
    }
}

class Input extends React.PureComponent {
    render(): React.Node {
        let { input } = this.props;
        // input = {name: 'testField', value: 'test value'};
        return (
                <input name={input.name} value={input.value} type='text' onChange={()=>1}/>
        );
    }
}

const mapStateToProps = ({ testForm }) => {
    return {
        initialValues: testForm,
    };
};

export const TestFormWithReduxForm = reduxForm({ form: 'test form'})(TestForm);

export default connect(mapStateToProps)(TestFormWithReduxForm);

Note the following :

  • I have my own custom Input (called Input)
  • I am connecting to reduxForm, and then connecting to redux.
  • The initial values that are passed in should have 'name' and 'value'.

I have the following test (Jest+Enzyme)

import React from 'react';
import { Provider } from 'react-redux';
import Enzyme, { mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import configureStore from 'redux-mock-store';

import TestForm from './TestForm';

Enzyme.configure({ adapter: new Adapter() });

describe('Redux Form Test', () => {
    let wrapper;

    let mockStoreData = {
        testForm: {
            testField: 'test value',
        },
    };

    const mockStore = configureStore();
    const store = mockStore(mockStoreData);

    beforeEach(() => {
        wrapper = mount(
            <Provider store={store}>
                    <TestForm />
            </Provider>
        );
    });

    it('Check Value', () => {
        let inputs = wrapper.find('input');
        expect(inputs.at(0).prop('name')).toBe('testField'); //OK!!!
        expect(inputs.at(0).prop('value')).toBe('test value'); //empty string!!
    });
});

The Jest test passes in a 'testForm' object with 'testField' (and its value) into the the store.

As expected, the name on the first input is 'testField', however the 'value' is empty (ie. an empty string).

This is not expected, because if I were to render the component in a normal page then 'test value' would appear.

So something seems to be broken here. I am not sure if it has something to do with redux-form or enzyme, but the redux-form Field object seems to be intercepting the properties that are passed into the Input object.

I am starting to question whether it is even possible to test redux form.


Solution

  • I'm not sure why you'd want nor need to test Redux Form's functionality, as it's already been tested by the creators/maintainers. However, redux-mock-store appears to be made for unit tests only (where you'll mock middlewares and call store.dispatch(actionType) and expect the action to be called). It doesn't handle reducer side effects nor track changes in state.

    In the case above, you'll only need to do a unit test on your custom Input component, because that's the only component that's different from what would be considered a standard redux form.

    That said... for an integration test, you'll need to use a real store that contains the redux-form's reducer and your field state.

    Working example: https://codesandbox.io/s/zl4p5w26xm (I've included a Form integration test and an Input unit test -- as you'll notice, the Input unit test covers most of your testing needs)

    containers/Form/Form.js

    import React, { Component } from "react";
    import { Form, Field, reduxForm } from "redux-form";
    import { connect } from "react-redux";
    import Input from "../../components/Input/input";
    
    const isRequired = value => (!value ? "Required" : undefined);
    
    class SimpleForm extends Component {
      handleFormSubmit = formProps => {
        alert(JSON.stringify(formProps, null, 4));
      };
    
      render = () => (
        <div className="form-container">
          <h1 className="title">Text Field</h1>
          <hr />
          <Form onSubmit={this.props.handleSubmit(this.handleFormSubmit)}>
            <Field
              className="uk-input"
              name="testField"
              component={Input}
              type="text"
              validate={[isRequired]}
            />
            <button
              type="submit"
              className="uk-button uk-button-primary uk-button-large submit"
              disabled={this.props.submitting}
            >
              Submit
            </button>
            <button
              type="button"
              className="uk-button uk-button-default uk-button-large reset"
              disabled={this.props.pristine || this.props.submitting}
              onClick={this.props.reset}
              style={{ float: "right" }}
            >
              Clear
            </button>
          </Form>
        </div>
      );
    }
    
    export default connect(({ field }) => ({
      initialValues: { [field.name]: field.value }
    }))(
      reduxForm({
        form: "SimpleForm"
      })(SimpleForm)
    );
    

    containers/Form/__test__/Form.js

    import React from "react";
    import { Provider } from "react-redux";
    import { mount } from "enzyme";
    import SimpleForm from "../Form";
    import store from "../../../store/store";
    
    const wrapper = mount(
      <Provider store={store}>
        <SimpleForm />
      </Provider>
    );
    
    describe("Redux Form Test", () => {
      it("renders without errors", () => {
        expect(wrapper.find(".form-container")).toHaveLength(1);
      });
    
      it("fills the input with a default value", () => {
        expect(wrapper.find("input").prop("name")).toBe("testField");
        expect(wrapper.find("input").prop("value")).toBe("Test Value");
      });
    
      it("updates input value when changed", () => {
        const event = { target: { value: "Test" } };
        wrapper.find("input").simulate("change", event);
        expect(wrapper.find("input").prop("value")).toBe("Test");
      });
    
      it("resets the input value to defaults when the Clear button has been clicked", () => {
        wrapper.find("button.reset").simulate("click");
        expect(wrapper.find("input").prop("value")).toBe("Test Value");
      });
    });
    

    stores/stores.js (for simplicity, I lumped reducers and store into one file)

    import { createStore, combineReducers } from "redux";
    import { reducer as formReducer } from "redux-form";
    
    const initialValues = {
      name: "testField",
      value: "Test Value"
    };
    
    const fieldReducer = (state = initialValues, { type, payload }) => {
      switch (type) {
        default:
          return state;
      }
    };
    const reducer = combineReducers({
      field: fieldReducer,
      form: formReducer
    });
    
    export default createStore(reducer);
    

    Note: Aside from using initialValues, buried in the documentation, there are three other ways to update field values: Utilizing redux-form's reducer.plugin and dispatching an action to update the form, or by using this.props.intialize({ testField: "Test Value" }); with enableReinitialize: true and keepDirtyOnReinitialize: true,, or by using this.props.change("SimpleForm", { testField: "Test Value" });. Important to note because sometimes mapStateToProps is asynchronous.