The redux docs for writing tests suggest to export the undecorated version of a connected component alongside the decorated one for testing purposes. Redux form v6 currently breaks this functionality here:
class Field extends Component {
constructor(props, context) {
if (!context._reduxForm) {
throw new Error('Field must be inside a component decorated with reduxForm()')
}
// ...
So, given a component:
// MyForm.js
export function MyForm (props) {
return (
<Field name="field1" component={React.DOM.input}>
);
}
export default reduxForm({form: 'myform'})(MyForm);
It is impossible to run tests on either the undecorated or the connected MyForm
. Trying to test the connected form like this:
// testMyForm.js
import MyForm from '../src'; // This is the connected (default) export, not recommended by redux docs
describe('<MyForm>', function () {
before('Render and locate', function () {
this.tree = TestUtils.renderIntoDocument(<MyForm>);
// ...
});
});
Obviously throws errors about not having a store
at all, since we haven't provided the redux
context.
Changing the import to:
import { MyForm } from '../src'; // recommended way from redux docs
So that we are now disconnected from redux, however, causes redux-form
to throw the error that I showed above: Field must be inside a component decorated with reduxForm()
We solved this at my company by building a test helper that will generate a fake store that uses redux-form: https://github.com/smaato/react-test-kit#mockformstore
Here's the source: https://github.com/smaato/react-test-kit/blob/master/src/mockFormStore.js
You should checkout how Redux Form is tested.
https://github.com/erikras/redux-form/blob/v6/src/__tests__/reduxForm.spec.js
Would it be possible to add documentation / an example to the redux-form website?
I feel like there is still a good bit of confusion. (I personally am having a hard time deducting the essential parts from the referenced ~1000LOC test file).
More so, I have seen wildely differing approaches and have a hard time figuring, which are the easiest and most beneficial to use. (See for example #111 but also the different approaches in this thread).
TL;DR An official and documented approach to testing would be very appreciated.
This is still unclear to me. I am unable to test any undecorated components that include Field components due to Field must be inside a component decorated with reduxForm()
Hello, same thing here. Is it possible for someone who does good unit testing to do a PR on the documentation for this part please ?
I think I succeed to make a snapshot test to a component based on what I saw in the source: https://github.com/erikras/redux-form/blob/master/src/__tests__/reduxForm.spec.js#L66
Basically instead on try to render directly my component Products
I did a decorated one:
const Decorated = reduxForm({ form: 'testForm' })(Products);
And then I passed my props directly to the decorated component:
<Decorated
repository={fixture}
handleSubmit={spy}
submitting={false}
submit={spy}
/>
Here the full test, let me know if it's working for you guys.
import React from 'react';
import renderer from 'react-test-renderer';
import { reduxForm } from 'redux-form';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { Products } from '../Products';
import { default as fixture } from './fixture';
const spy = jest.fn();
const store = createStore(() => ({}));
const Decorated = reduxForm({ form: 'testForm' })(Products);
describe('Products Component snapshot', () => {
it('should render the snapshot', () => {
const tree = renderer.create(
<Provider store={store}>
<Decorated
repository={fixture}
handleSubmit={spy}
submitting={false}
submit={spy}
/>
</Provider>
).toJSON();
expect(tree).toMatchSnapshot();
});
});
So I separate form from state to gain testability, but in the tests I need to wrap it again in a fake state to gain testability? Yeah, that's non sense for me.
Not really if you do only shallow rendering.
import { expect } from "chai";
import { shallow } from "enzyme";
const fooField = shallow( <MyForm /> ).find( Field ).filter({ name: "foo" });
expect( fooField ).to.be.present();
However, if you want to test Jest snapshots, I think it's very likely you'll need the full HTML generated by every component.
Yes, you're right. I am able to test it individually with shallow. I am using Mocha, so it will be fine to use shallow. Thank you. :)
There are other scenarios such as using React Storybook where it would be useful to be able to mount non-decorated components for simulating different states without mocking a full redux store.
We can mock out the Field implementation to render its props component instead of Field itself in order to achieve store-less testing.
The snapshot result should be the same as rendering Field (I think).
Here's my mock to be used in Jest:
import React from 'react'
const ReduxForm = require.requireActual('redux-form')
ReduxForm.Field = (props) => {
const { component: Component, ...others } = props
const input = {onChange: () => {}}
const meta = {}
return <Component input={input} meta={meta} {...others} />
}
export const {
actionTypes,
arrayInsert,
arrayMove,
arrayPop,
arrayPush,
arrayRemove,
arrayRemoveAll,
arrayShift,
arraySplice,
arraySwap,
arrayUnshift,
autofill,
blur,
change,
destroy,
Field,
Fields,
FieldArray,
Form,
FormSection,
focus,
formValueSelector,
getFormNames,
getFormValues,
getFormInitialValues,
getFormSyncErrors,
getFormAsyncErrors,
getFormSyncWarnings,
getFormSubmitErrors,
initialize,
isDirty,
isInvalid,
isPristine,
isValid,
isSubmitting,
hasSubmitSucceeded,
hasSubmitFailed,
propTypes,
reducer,
reduxForm,
registerField,
reset,
setSubmitFailed,
setSubmitSucceeded,
startAsyncValidation,
startSubmit,
stopAsyncValidation,
stopSubmit,
submit,
SubmissionError,
touch,
unregisterField,
untouch,
values
} = ReduxForm
+1 for this error still being a drag for unit testing.
We statefully manage certain props (related to presentation purposes only) within our components. It makes for better efficiency and readability. We are unable to test the state for components that contain a Field element (even if they are subcomponents of the form).
When mocking the store using the methods described above, I get the following problem:
Actions must be plain objects. Use custom middleware for async actions.
What's that about? Even if I force all actions to be plain objects in the mocked store, it comes up (and the snapshot test fails).
That's a Redux error. Not related to redux-form
.
I just wanted to see readonly versions of various states of my component. So I made this ugly fake context feeder which still need react-redux provider, cause it is still requiring connect.
But the store does not affect anything and Field can spam actions as it wish.
You can swap data in provider as your heart pleases you.
There is a lot of cleaning possible, but I am not in a mood today after so much time wasted.
import React from "react";
import PropTypes from "prop-types";
import { storiesOf } from "@storybook/react";
import UniversitySuggestionComponent from "../components/UniversitySuggestion/UniversitySuggestionComponent.jsx";
import { withContext } from 'recompose';
import { createStore } from "redux";
import { Provider } from 'react-redux';
const fields = {
universityName: {
value: 'Michigan',
touched: false,
error: null,
},
name: {
value: 'Somebody',
touched: false,
error: null,
},
email: {
value: '[email protected]',
touched: false,
error: null,
},
};
const values = {
universityName: 'Michigan',
name: 'Somebody',
email: '[email protected]',
};
const defaultReduxFormProps = {
onSave: () => {},
handleSubmit: () => {},
reset: () => {},
submitting: false,
initial: values,
values: values,
fields: fields,
};
const fakeReducer = (state = {}) => state;
const store = createStore(
fakeReducer,
);
const fakeFunction = () => ({ type: 'fake'});
const reduxFormShape = PropTypes.shape({
register: PropTypes.func.isRequired,
unregister: PropTypes.func.isRequired,
getValues: PropTypes.func.isRequired,
getFormState: PropTypes.func.isRequired,
change: PropTypes.func.isRequired,
focus: PropTypes.func.isRequired,
blur: PropTypes.func.isRequired,
asyncValidate: PropTypes.func.isRequired,
});
const reduxFormFakeContext = formState => ({
register: fakeFunction,
unregister: fakeFunction,
getValues: fakeFunction,
getFormState: () => formState,
change: fakeFunction,
focus: fakeFunction,
blur: fakeFunction,
asyncValidate: fakeFunction,
});
const contextEnhancer = withContext(
{ _reduxForm: reduxFormShape }, //childContextTypes
(props) => ({ _reduxForm: reduxFormFakeContext(props.fakeForm) }) //getChildContext
);
const FormContainer = props => (<div>{props.children}</div>);
const FormProvider = contextEnhancer(FormContainer);
storiesOf('UniversitySuggestion', module)
.add('Default form', () => (
<Provider store={store}>
<FormProvider fakeForm={defaultReduxFormProps}>
<UniversitySuggestionComponent />
</FormProvider>
</Provider>
));
I've got the same issue and try to test component that receives props from 'formValues', How to simulate and pass the props to 'shallow' and check some statement in render with
export const MiConfiguration = ({miConfiguration, change}) =>
<FormSection name='miConfiguration'>
<Field
name='miConfigurationType'
component={renderSelectField}
label={<FormattedMessage id='passage.label.scenario' />}
style={{width: 400, ...styles.selectField}}
hintText={<FormattedMessage id='passage.selectConfiguration.hint'/>}
autoWidth
onChange={(e, newValue) => Object.keys(defaultValues[newValue]).forEach(key => change(key, defaultValues[newValue][key]))}
>
{Object.keys(MiConfigurationTypes).map(type =>
<MenuItem value={type} primaryText={<FormattedMessage id={`passage.scenario.${type}`} />}/>)}
</Field>
{miConfiguration && !!miConfiguration.miConfigurationType &&
<InfoMessage id={`miConfiguration.description.${miConfiguration.miConfigurationType}`} />}
{miConfiguration && miConfiguration.miConfigurationType !== MiConfigurationTypes.AccessPointOnly &&
<Paper style={{padding: 15, marginTop: 10}}>
<Typography type="subheading" gutterBottom>
<FormattedMessage id='passage.configurationOptions'/>
</Typography>
<InfoMessage id='passage.label.commissioning'/>
{getMiConfiguration(miConfiguration.miConfigurationType)}
</Paper>
}
</FormSection>
export default formValues('miConfiguration')(MiConfiguration)
describe.only('<MiConfiguration/>', () => {
let component, onChange, initialValues
let device = {miConfiguration:{isMiEnabled: 'dimon', miConfigurationType: 'Type'}}
beforeEach(() => {
initialValues ={ initialValues:{
...device,
}}
component = shallow(<MiConfiguration miConfiguration={{miConfiguration: {}}} miConfiguration={{}} change={onChange = sinon.spy()}/>, { context:{ _reduxForm: {}}})
})
it.('should render sInfoMessage correct id', () => {
component.find(InfoMessage).props().id.should.be.equal(`miConfiguration.description.${miConfiguration.miConfigurationType}``)
})
})
@hellosmithy I'm using the following decorators to render my form components in Storybook:
ProviderDecorator.js
import React from 'react'
import { Provider as ReduxProvider } from 'react-redux'
import store from '../store/store'
const ProviderDecorator = ( story ) => {
return (
<ReduxProvider store={ store }>
{ story() }
</ReduxProvider>
)
}
export default ProviderDecorator
ReduxFormDecorator.js
import React from 'react'
import { reduxForm } from 'redux-form'
const ReduxFormDecorator = reduxFormConfig => story => {
const ReduxFormed = reduxForm( reduxFormConfig )( story )
return (
<ReduxFormed />
)
}
export default ReduxFormDecorator
And then I might use them like this:
const happyPath = () => (
<EditProfileForm
handleSubmit={ handleSubmit }
userProfile={ completeUser }
/>
)
storiesOf( 'components/settings/EditProfileForm', module )
.addDecorator( ReduxFormDecorator( { form: 'editProfile' } ) )
.addDecorator( ProviderDecorator )
.add( 'happy path', happyPath )
Hope that helps...
@javidjamae
How to use with Field?
In my case "React.Children.only expected to receive a single React element child" error happened.
storiesOf("Task", module)
.addDecorator(ReduxFormDecorator({ form: "taskAssetManager" }))
.addDecorator(Provider)
.add("TaskAssetManager", () => {
return (
<Field
component={TaskAssetManager}
name="assets"
onChange={action("onChanged")}
/>
);
});
@ChrisRyuGS I don't know, maybe this: https://github.com/storybooks/storybook/issues/113
Mocking redux-form
with jest allows me to snapshot test without having to mess with the store at all:
// At the top of the spec file
jest.mock('redux-form', () => {
Field: 'Field',
reduxForm: () => component => component,
}));
This doesn't render the contents of whatever component you pass to Field
, but that's the way I prefer to do my snapshots anyway. If I have <Field component={FormComponent} />
, I can snapshot test FormComponent
separately.
```javascript
import ConnectedComponent from './Component';
import React from 'react';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import toJson from 'enzyme-to-json';
import configureStore from 'redux-mock-store';
import {
reducer1,
reducer2
} from 'redux-base/reducers';
import {
action1,
action2
} from 'redux-base/actions';
const initialState = {
reducer1: {
...reducer(),
data: {
a: 1,
b: 2,
c: 3,
}
},
reducer2: reducer2(),
}
describe('ConnectedComponent', () => {
it('saves smth on submit ', () => {
const store = mockStore(initialState);
const component = mount(
<Provider store={ store }>
<ConnectedCustomer
{ ...props } // props you can define above and pass to the component
/>
</Provider>
);
component.find('button#submitButton').simulate('submit');
const actions = store.getActions();
const actionOnSubmit = actions.find(action => action.type === action2().type);
expect(actionOnSubmit.type).toBe(action2().type)
component.unmount();
}
}
}
example of testing ConnectedComponent in case of submit action
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Most helpful comment
I think I succeed to make a snapshot test to a component based on what I saw in the source: https://github.com/erikras/redux-form/blob/master/src/__tests__/reduxForm.spec.js#L66
Basically instead on try to render directly my component
Products
I did a decorated one:const Decorated = reduxForm({ form: 'testForm' })(Products);
And then I passed my props directly to the decorated component:
Here the full test, let me know if it's working for you guys.