I'm trying to test a component which inherits context from a root component, without loading/rendering everything from the root down. I've tried and searched for examples on how to mock the context but can't find anything (at least that doesn't use jest).
Here's a simplified example of what I'm trying to achieve.
Is there a simple way I can mock reactEl.context for the test?
/** * Root Element that sets up & shares context */ class Root extends Component { getChildContext() { return { language: { text: 'A String'} }; } render() { return ( <div> <ElWithContext /> </div> ); } } Root.childContextTypes = { language: React.PropTypes.object }; /** * Child Element which uses context */ class ElWithContext extends React.Component{ render() { const {language} = this.context; return <p>{language.text}</p> } } ElWithContext.contextTypes = { language: React.PropTypes.object } /** * Example test where context is unavailable. */ let el = React.createElement(ElWithContext) element = TestUtils.renderIntoDocument(el); // ERROR: undefined is not an object (evaluating 'language.text') describe("ElWithContext", () => { it('should contain textContent from context', () => { const node = ReactDOM.findDOMNode(element); expect(node.textContent).to.equal('A String'); }); })
To mock a React component, the most straightforward approach is to use the jest. mock function. You mock the file that exports the component and replace it with a custom implementation. Since a component is basically a function, the mock should also return a function.
The best way to test Context is to make our tests unaware of its existence and avoiding mocks. We want to test our components in the same way that developers would use them (behavioral testing) and mimic the way they would run in our applications (integration testing).
Mock functions allow you to test the links between code by erasing the actual implementation of a function, capturing calls to the function (and the parameters passed in those calls), capturing instances of constructor functions when instantiated with new , and allowing test-time configuration of return values.
I went into the same issue as you did and found out two ways of doing it.
The first one is a basic copycat of your own way: Create a wrapper around my component and inject it with a dynamic context. I put the source code below for those interested, because it's ES6 unlike your example. But it's just to show how it would be done in ES6 and I do NOT recommend anyone using it (I haven't actually tested it myself).
src/testUtils/mockWithContext.js
import React, { Component } from 'react'; import wrapDisplayName from 'recompose/wrapDisplayName'; import hoistStatics from 'recompose/hoistStatics'; export const defaultContext = { permissions: [ ], user: { id: '1', display_name: 'Default user', email: '<your.email>[email protected]', // Trick with "+" for infinite aliases using gmail. username: 'default_user', created: '2016-08-01T15:50:13.246Z', }, }; export const defaultContextType = { permissions: React.PropTypes.array, user: React.PropTypes.shape({ id: React.PropTypes.string.isRequired, display_name: React.PropTypes.string.isRequired, email: React.PropTypes.string.isRequired, username: React.PropTypes.string.isRequired, created: React.PropTypes.string.isRequired, }), }; /** * HOC for context */ const withContext = ({ context = defaultContext, contextType = defaultContextType }) => (WrappedComponent) => { class WithContext extends Component { getChildContext() { return context; } render() { return <WrappedComponent {...this.props} />; } } WithContext.displayName = wrapDisplayName(WrappedComponent, 'WithContext'); WithContext.WrappedComponent = WrappedComponent; WithContext.childContextTypes = contextType; return WithContext; }; export default hoistStatics(withContext);
As I said, I wrote it, but didn't test it because I found a much better way of doing context-injecting when trying to write tests for this mock.
Using Enzyme library, which is definitely built to support React components testing, there is the ability to shallow
/mount
/static
render your component, for testing purpose. And each of these methods allow a second argument: the context.
SimpleComponent.js
const SimpleComponent = React.createClass({ contextTypes: { name: React.PropTypes.string, }, render() { return <div>{this.context.name}</div>; }, });
SimpleComponent.test.js
const context = { name: 'foo' }; const wrapper = mount(<SimpleComponent />, { context }); expect(wrapper.text()).to.equal('foo'); wrapper.setContext({ name: 'bar' }); expect(wrapper.text()).to.equal('bar'); wrapper.setContext({ name: 'baz' }); expect(wrapper.text()).to.equal('baz');
Pretty straight-forward. I didn't use it yet but it looks like what I (and you) wanted to do. I guess I'll just have to throw my own implementation to the garbage.
http://airbnb.io/enzyme/docs/api/ReactWrapper/setContext.html
I went with a solution of creating a wrapping component with context. Not sure if this is a great approach but is working for me right now:
/** * Helper function to wrap component with a component that has context */ function wrapWithContext(context, contextTypes, children, React){ const wrapperWithContext = React.createClass({ childContextTypes: contextTypes, getChildContext: function() { return context }, render: function() { return React.createElement('div', null, children) } }); return React.createElement(wrapperWithContext); } /** * Usage */ // in setup function of test framework const el = React.createElement(ElWithContext); const context = { language: { text: 'A String' } }; const contextTypes = { language: React.PropTypes.object }; const wrapper = wrapWithContext(context, contextTypes, [el], React); const ElWithContext = TestUtils.renderIntoDocument(wrapper); // do tests describe('ElWithContext', () => { it('should contain textContent from context', () => { const node = ReactDOM.findDOMNode(element); expect(node.textContent).to.equal('A String'); }); })
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With