Reputation: 6825
I need some help understanding how one can test an application using React
Context
.
Here's my sample setup.
context.js
import React from 'react'
export const AppContext = React.createContext()
App.js
import React from 'react'
import MyComponent from './MyComponent'
import {AppContext} from './context'
const App extends React.Component {
state = {
items: []
}
handleItemAdd = newItem => {
const {items} = this.state
items.push(newItem)
this.setState(items)
}
render() {
return (
<AppContext.Provider value={{
addItem: this.handleItemAdd
}}>
<MyComponent />
</AppContext.Provider>
)
}
}
export default App
MyComponent.js
import React from 'react'
import {AppContext} from './context'
const MyComponent extends React.Component {
render() {
return (
<AppContext.Consumer>
{addItem =>
<button onClick={() => addItem('new item')}>
Click me
</button>
}
</AppContext.Consumer>
)
}
}
export default MyComponent
This is a simplified example. Imagine that there are more layers between App
and MyComponent
, hence the use of React
Context
.
And here's my test file for MyComponent
.
MyComponent.test.js
import React from 'react'
import {render, fireEvent} from 'react-testing-library'
test('component handles button click', () => {
const {getByText} = render(
<MyComponent />
)
const button = getByText('Click me')
fireEvent.click(button)
expect...?
}
The thing is, AppContext.Consumer
is part of the implementation of MyComponent
, so in this test I don't have direct access to it. How do I mock AppContext.Consumer
so I am actually able to verify that clicking a button fires a function call?
I know that in theory I can test this by rendering MyComponent
in my App
, but I want to write a unit test for MyComponent
only.
Upvotes: 94
Views: 167147
Reputation: 409
In case there are any TypeScript users who can't get this solution to work, I want to add my test example for anyone in distress.
In my case I added a custom component wrapper that can be reused across the testing environment in your application. This can be handy if you are testing more than one component that is nested in the context that you wish to mock.
Also as a bonus I mocked the rxjs behavior subject in case anyone utilizes this library in their tests:
// Create a mock BehaviorSubject instance
class MockBehaviorSubject<T> extends BehaviorSubject<T> {
constructor(initialValue: T) {
super(initialValue);
}
// Override necessary methods
getValue(): T {
return this.getValue();
}
// Add any other required methods here
// For example, you might need to add `next` method:
// next(value: T): void {
// this.next(value);
// }
// Create a custom wrapper component to provide the mock context values
export const MockAppProvider: React.FC<WithChildren> = ({ children }) => {
// Define your mock context values here
const id= 'test';
// Create a mock BehaviorSubject instance
const mockCreateObservablesCallbackSubject = new MockBehaviorSubject<
TObservableEvent | undefined
>(undefined);
return (
<AppProviderContext.Provider
value={{
id: mockCycleId,
setId: jest.fn(),
observables: undefined,
setObservables: jest.fn(),
createObservablesCallbackSubject: mockCreateObservablesCallbackSubject,
}}
>
{children}
</AppProviderContext.Provider>
);
};
}
Upvotes: 2
Reputation: 1950
I want to add a complete test example by using the solution from @Giorgio. Here we are testing that MyComponent is rendered and that its button will be clicked once.
MyComponent.test.js
import React from 'react'
import { render, fireEvent } from 'react-testing-library'
test('component handles button click', () => {
const addItem = jest.fn()
render(
<AppContext.Provider value={{ addItem }}>
<MyComponent />
</AppContext.Provider>
)
fireEvent.click(screen.getByText(/click me/))
expect(addItem).toHaveBeenCalledTimes(1)
}
Upvotes: 8
Reputation: 27098
You just render the context with your component.
const addItem = jest.fn()
render(
<AppContext.Provider value={{ addItem }}>
<MyComponent />
</AppContext.Provider>
)
See Mocking context with react-testing-library
Upvotes: 174