I have a react context like this
UserContext.js
export const UserContext = createContext({
createUser: async () => {
...execute some xhr requests and manipulateData
},
getUser: () => {},
});
const UserProvider ({ children }) => {
const context = {
createUser,
getUser,
};
return (
<UserContext.Provider value={context}>{children}</UserContext.Provider>
);
}
And the following unit test
UserContext.spec.js
import { render } from '#testing-library/react';
import UserProvider, { UserContext } from '#my-contexts/user';
...
let createUser = () => {};
render(
<UserProvider>
<UserContext.Consumer>
{value => {
createUser = value.createUser;
return null;
}}
</UserContext.Consumer>
</UserProvider>
);
await createUser(data);
expect(data).toEqual({ status: 200 });
I am not sure if this way
let createUser = () => {};
render(
<UserProvider>
<UserContext.Consumer>
{value => {
createUser = value.createUser;
return null;
}}
</UserContext.Consumer>
</UserProvider>
);
it is the better technique for "extract" the inner methods exposed in the UserContext (in this case createUser method)
In my App code I use this context like following:
import { useContext } from 'react';
import { UserContext } from '#my-contexts/user';
const someComponent = (props) => {
...
const { createUser } = useContext(UserContext);
const handleCreate = (e) => {
createUser(form);
};
return (
<form>
<label>Username</label>
<input value={form.userName} />
<button type="submit" onClick={handleCreate}> create user </button>
</form>
)
};
But the problem is that I can get the exposed methods from context only if I have a component and get the method using useContext hook.
If I want to test the createUser method in isolation:
Exists a better way for get the methods exposed in context without rendering a provider and consumer and "manually extract" him?
I think I found another way to test a component using the useContext hook. I have seen a few tutorials that test if a value can be successfully passed down to a child component from a parent Context Provider but did not find tutorials on the child updating the context value.
My solution is to render the root parent component along with the provider, because the state is ultimately changed in the root parent component and then passed to the provider which then passes it to all child components. Right?
The tests seem to pass when they should and not pass when they shouldn't.
Can someone explain why this is or isn't a good way to test the useContext hook?
The root parent component:
...
const App = () => {
const [state, setState] = useState("Some Text")
const changeText = () => {
setState("Some Other Text")
}
...
<h1> Basic Hook useContext</h1>
<Context.Provider value={{changeTextProp: changeText,
stateProp: state
}} >
<TestHookContext />
</Context.Provider>
)}
The context object:
import React from 'react';
const Context = React.createContext()
export default Context
The child component:
import React, { useContext } from 'react';
import Context from '../store/context';
const TestHookContext = () => {
const context = useContext(Context)
return (
<div>
<button onClick={context.changeTextProp}>
Change Text
</button>
<p>{context.stateProp}</p>
</div>
)
}
And the tests:
import React from 'react';
import ReactDOM from 'react-dom';
import TestHookContext from '../test_hook_context.js';
import {render, fireEvent, cleanup} from '#testing-library/react';
import App from '../../../App'
import Context from '../../store/context';
afterEach(cleanup)
it('Context is updated by child component', () => {
const { container, getByText } = render(<App>
<Context.Provider>
<TestHookContext />
</Context.Provider>
</App>);
console.log(container)
expect(getByText(/Some/i).textContent).toBe("Some Text")
fireEvent.click(getByText("Change Text"))
expect(getByText(/Some/i).textContent).toBe("Some Other Text")
})
The problem with the approach that you mention is coupling.
Your Context to be tested, depends on <TestHookContext/> and <App/>
As Kent C. Dodds, the author of react-testing-library, has a full article on "Test Isolation with React" if you want to give it a read.
TLDR: Demo repo here
How to test Context
Export a <ContextProvider> component that holds the state and returns <MyContext.Provider value={{yourWhole: "State"}}>{children}<MyContext.Provider/> This is the component that we are going to test for the provider.
On the components that consume that Context, create a MockContextProvider to replace the original one. You want to test the component in isolation.
You can test the whole app workflow to by testing the root component.
Testing Auth Provider
Let's say we have a component that provides Auth using context the following way:
import React, { createContext, useState } from "react";
export const AuthContext = createContext();
const AuthProvider = ({ children }) => {
const [isLoggedin, setIsLoggedin] = useState(false);
const [user, setUser] = useState(null);
const login = (user) => {
setIsLoggedin(true);
setUser(user);
};
const logout = () => {
setIsLoggedin(false);
setUser(null);
};
return (
<AuthContext.Provider value={{ logout, login, isLoggedin, user }}>
{children}
</AuthContext.Provider>
);
};
export default AuthProvider;
The test file would look like:
import { fireEvent, render, screen } from "#testing-library/react";
import AuthProvider, { AuthContext } from "./AuthProvider";
import { useContext } from "react";
const CustomTest = () => {
const { logout, login, isLoggedin, user } = useContext(AuthContext);
return (
<div>
<div data-testid="isLoggedin">{JSON.stringify(isLoggedin)}</div>
<div data-testid="user">{JSON.stringify(user)}</div>
<button onClick={() => login("demo")} aria-label="login">
Login
</button>
<button onClick={logout} aria-label="logout">
LogOut
</button>
</div>
);
};
test("Should render initial values", () => {
render(
<AuthProvider>
<CustomTest />
</AuthProvider>
);
expect(screen.getByTestId("isLoggedin")).toHaveTextContent("false");
expect(screen.getByTestId("user")).toHaveTextContent("null");
});
test("Should Login", () => {
render(
<AuthProvider>
<CustomTest />
</AuthProvider>
);
const loginButton = screen.getByRole("button", { name: "login" });
fireEvent.click(loginButton);
expect(screen.getByTestId("isLoggedin")).toHaveTextContent("true");
expect(screen.getByTestId("user")).toHaveTextContent("demo");
});
test("Should Logout", () => {
render(
<AuthProvider>
<CustomTest />
</AuthProvider>
);
const loginButton = screen.getByRole("button", { name: "logout" });
fireEvent.click(loginButton);
expect(screen.getByTestId("isLoggedin")).toHaveTextContent("false");
expect(screen.getByTestId("user")).toHaveTextContent("null");
});
Testing Component that consumes Context
import React, { useContext } from "react";
import { AuthContext } from "../context/AuthProvider";
const Welcome = () => {
const { logout, login, isLoggedin, user } = useContext(AuthContext);
return (
<div>
{user && <div>Hello {user}</div>}
{!user && <div>Hello Anonymous Goose</div>}
{!isLoggedin && (
<button aria-label="login" onClick={() => login("Jony")}>
Log In
</button>
)}
{isLoggedin && (
<button aria-label="logout" onClick={() => logout()}>
Log out
</button>
)}
</div>
);
};
export default Welcome;
We will mock the AuthContext value by providing one of our own:
import React, { useContext } from "react";
import { render, screen } from "#testing-library/react";
import "#testing-library/jest-dom";
import Welcome from "./welcome";
import userEvent from "#testing-library/user-event";
import { AuthContext } from "../context/AuthProvider";
// A custom provider, not the AuthProvider, to test it in isolation.
// This customRender will be a fake AuthProvider, one that I can controll to abstract of AuthProvider issues.
const customRender = (ui, { providerProps, ...renderOptions }) => {
return render(
<AuthContext.Provider value={providerProps}>{ui}</AuthContext.Provider>,
renderOptions
);
};
describe("Testing Context Consumer", () => {
let providerProps;
beforeEach(
() =>
(providerProps = {
user: "C3PO",
login: jest.fn(function (user) {
providerProps.user = user;
providerProps.isLoggedin = true;
}),
logout: jest.fn(function () {
providerProps.user = null;
providerProps.isLoggedin = false;
}),
isLoggedin: true,
})
);
test("Should render the user Name when user is signed in", () => {
customRender(<Welcome />, { providerProps });
expect(screen.getByText(/Hello/i)).toHaveTextContent("Hello C3PO");
});
test("Should render Hello Anonymous Goose when is NOT signed in", () => {
providerProps.isLoggedin = false;
providerProps.user = null;
customRender(<Welcome />, { providerProps });
expect(screen.getByText(/Hello/i)).toHaveTextContent(
"Hello Anonymous Goose"
);
});
test("Should render Logout button when user is signed in", () => {
customRender(<Welcome />, { providerProps });
expect(screen.getByRole("button", { name: "logout" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "login" })).toBeNull();
});
test("Should render Login button when user is NOT signed in", () => {
providerProps.isLoggedin = false;
providerProps.user = null;
customRender(<Welcome />, { providerProps });
expect(screen.getByRole("button", { name: "login" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "logout" })).toBeNull();
});
test("Should Logout when user is signed in", () => {
const { rerender } = customRender(<Welcome />, { providerProps });
const logout = screen.getByRole("button", { name: "logout" });
expect(logout).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "login" })).toBeNull();
userEvent.click(logout);
expect(providerProps.logout).toHaveBeenCalledTimes(1);
//Technically, re renders are responsability of the parent component, but since we are here...
rerender(
<AuthContext.Provider value={providerProps}>
<Welcome />
</AuthContext.Provider>
);
expect(screen.getByText(/Hello/i)).toHaveTextContent(
"Hello Anonymous Goose"
);
expect(screen.getByRole("button", { name: "login" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "logout" })).toBeNull();
});
test("Should Login when user is NOT signed in", () => {
providerProps.isLoggedin = false;
providerProps.user = null;
const { rerender } = customRender(<Welcome />, { providerProps });
const login = screen.getByRole("button", { name: "login" });
expect(login).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "logout" })).toBeNull();
userEvent.click(login);
expect(providerProps.login).toHaveBeenCalledTimes(1);
//Technically, re renders are responsability of the parent component, but since we are here...
rerender(
<AuthContext.Provider value={providerProps}>
<Welcome />
</AuthContext.Provider>
);
expect(screen.getByText(/Hello/i)).toHaveTextContent("Hello Jony");
expect(screen.getByRole("button", { name: "logout" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "login" })).toBeNull();
});
});
Your example/code is dead on. (Not sure you need to mount the wrapping <App /> - you should just wrap in the Context Provider directly).
To your question:
The tests seem to pass when they should and not pass when they shouldnt. Can someone explain why this is or isnt a good way to test the useContext() hook.
It is a good way to test when using useContext() because it looks like you have abstracted out your context so that your child (consuming) component and its test both use the same context. I don't see any reason why you would mock or emulate what the context provider is doing when (as you do in your example) you use the same context provider.
The React Testing Library docs point out that:
The more your tests resemble the way your software is used, the more confidence they can give you.
Therefore, setting up your tests the same way you set up your components achieves that goal. If you have multiple tests in one app that do need to be wrapped in the same context, this blog post has a neat solution for reusing that logic.
I have a connected component with Form and button. While unit testing the component using mount , i get the error "Actions must be plain objects."
This is happening due to the redux calls being made in the handleSubmit method, is there a way to mock the calls from happening.
import React from "react";
import {Provider} from "react-redux";
import configureStore from "redux-mock-store";
import {Props, InitialState} from "../mock_data/mock.data.jsx";
const mockStore = configureStore();
let store = mockStore(InitialState);
const params = {
url: "http://localhost:9879"
};
let props = {};
beforeEach(() => {
// reset props before each test
props = JSON.parse(JSON.stringify(Props));
props.actions = {};
props.email = Props.email;
props.params = params;
});
handleSubmit({ values }) {
const {url} = this.props;
const val = values.valcode;
this.props.actions.getCode(url);
this.props.makeExternalCall(val);
}
return (
<Form
initialValues={{ ...personalUser }}
onSubmit={this.handleSubmit}>
{
({ form, errors }) => {
const isDisabled = errors !== null || this.state.disableReset;
return (
<div>
<button name="btnConfirm" type="submit"
className={styles.btnConfirm} disabled={isDisabled}>/>
</button>
</div>
);
}
}
</Form>
);
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actions, dispatch),
makeExternalCall: bindActionCreators(makeExternalCallAction, dispatch)
});
export default connect(null, mapDispatchToProps)(injectIntl(TestComponent));
Test
it(" - should display the proper label", () => {
props.error = "This is an error message";
props.actions.getCode = sinon.stub().returns({
then: () => {}
});
props.makeExternalCall = sinon.stub().returns({
then: () => {}
});
store = mockStore(props);
const wrapper = mountWithIntl(<Provider store={store}>
<TestComponent {...props}/>
</Provider>);
expect(wrapper).to.not.be.null;
expect(wrapper.find("button").prop("disabled")).equals(true);
wrapper.find("TestComponent").get(0).handleSubmit({values:{valcode: "AkPdQ2" }});
});
Is it possible to mock the mapDispatchToProps ,which is giving the error
PhantomJS 2.1.1 (Mac OS X 0.0.0) <TestComponent Component /> - should display the proper label FAILED
Actions must be plain objects. Use custom middleware for async actions.
I'm trying to simulate a form submission in redux-form for a unit-test and I can't even get the onSubmit handler to be called.
Test Snippet
import React from 'react';
import { AddBudgetForm } from '../../../src/Budget/components/AddBudgetForm';
import { SubmissionError } from 'redux-form';
import { shallow, configure } from 'enzyme';
import sinon from 'sinon';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
describe("AddBudgetForm Component", () => {
let budgetForm = null;
let submitting, touched, error, reset,addBudget, addBudgetResponse, handleSubmit
beforeEach(() => {
submitting = false
touched = false
error = null
reset = sinon.spy()
addBudgetResponse = Promise.resolve()
handleSubmit = fn => fn
})
const buildForm = () => {
addBudget = sinon.stub().returns(addBudgetResponse)
const props = {
addBudget,
submitting: submitting,
fields: {
budgetName: {
value: '',
touched: touched,
error: error
}
},
handleSubmit,
reset
}
return shallow(<AddBudgetForm {...props}/>)
}
it ('Calls reset after onSave', () => {
budgetForm = buildForm();
budgetForm.find('[type="submit"]').simulate('submit')
})
})
Above, I'm mocking some of the actions and my test will inevitably just check the callCount from the sinon spy.
Form
submit(dataValues) {
console.log("called")
dataValues.preventDefault();
this.props.addBudget({})
}
render() {
const { handleSubmit } = this.props;
return (
<div className='form-group has-danger'>
<form onSubmit={(e) => this.submit(e)}>
<Field name='budgetName' component={this.categoryInput} placeholder="Enter Category of Budget" label="Budget Category"/>
<button type='submit' className='btn btn-primary'>Submit</button>
</form>
</div>
)
}
}
I've shortened my code a bit, but the idea is that the onSubmit should call the function, but I can't even get the console log to print. Not really sure why this is happening.
If you want to simulate a submit event you should select a form tag. With the code above should be a click. So:
budgetForm.find('[type="submit"]').simulate('click')
Im trying to create a React Component using Redux and with functions
userSignUpRequest I follow a tutorial but still getting the error:
I think is possible becouse I use module.exports to export my component instead of export by default but im not sure how to fix it.
My Store:
const middleware = routerMiddleware(hashHistory);
const store = createStore(
reducers,
applyMiddleware(middleware)
);
render(
<Provider store={store}>
<Router
onUpdate={scrollToTop}
history={history}
routes={rootRoute}
/>
</Provider>,
document.getElementById('app-container')
);
This is my Component:
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'; //to pass functions
import { userSignUpRequest } from '../../../actions/loginAxios';
class Login extends React.Component {
constructor() {
super();
this.state = {
{** initial state **}
};
}
onSubmit(e) {
e.preventDefault();
if(this.isValid()){
//reset errros object and disable submit button
this.setState({ errors: {}, isLoading: true });
//we store a function in the props
this.props.userSignUpRequest(this.state).then(
(response) => {
//succes redirect
this.context.router.history.push('/');
},
(error) => {
console.log("error");
this.setState({ errors: error.response.data, isLoading: false });
});
} else {
console.log(this.state.errors);
}
}
onChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
render() {
const { errors } = this.state; //inicializate an get errors
const { userSignUpRequest } = this.props;
return (
<div className="body-inner">
{*** My React Login React Form ***}
</div>
);
}
}
const Page = () => (
<div className="page-login">
<div className="main-body">
<QueueAnim type="bottom" className="ui-animate">
<div key="1">
<Login />
</div>
</QueueAnim>
</div>
</div>
);
//To get the main Actions
Login.propTypes = {
userSignUpRequest: PropTypes.func.isRequired
};
function mapStateToProps(state) {
//pass the providers
return {
}
}
module.exports = connect(mapStateToProps, { userSignUpRequest })(Page);
This is my function "loginAxios.js"
import axios from "axios";
export function userSignUpRequest(userData) {
return dispatch => {
return axios.post("/api/users", userData);
}
}
I am new to React so I would greatly appreciate your support!! Thanks
You explicitly use propTypes and even use isRequired, which is exactly what it sounds like.
Login.propTypes = {
userSignUpRequest: PropTypes.func.isRequired
};
But in your Page component, you don't provide a prop name userSignUpRequest. You just have it as this:
<Login/>
If userSignUpRequest is fundamentally needed for that component to work, you need to pass it in.
<Login userSignUpRequest={() => myFunction()}/>
If it's not, then just delete your propTypes declaration.
You are importing the module userSignUpRequest. It doesn't appear you are passing it through as a prop. Try just calling your module without the "this.props"
In my case, I have resolved the Warning by using below syntax
parentRoute={`${parentRoute}`}
Instead of
parentRoute={parentRoute}
Hope it helps.