Reputation: 13509
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 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.
Upvotes: 2
Views: 4941
Reputation: 19762
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.
Upvotes: 4