9

I am new to testing and I'm trying to test my app with react testing library. The first issue I ran into is testing functions that are inside of my components, like event handlers. I have found few examples where functions (event handlers) are passed in components as props but I don't want to do that if possible.

My Component:

import React, { useState } from 'react';
import { Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';

import { login } from 'actions/auth';
import Wrapper from './styled/Wrapper';
import Button from './styled/Button';
import Title from './styled/Title';
import { FormWrapper, StyledForm, FormField } from './styled/Form'
import Input from './styled/Input';
import Link from './styled/Link';


const Login = (props) => {
    const [formData, setFormData] = useState({
        email: '',
        password: ''
    });

    const { email, password } = formData;

    const onChange = e => setFormData({ ...formData, [e.target.name]: e.target.value });

    const onSubmit = e => {
        e.preventDefault();
        props.login(email, password);
    }

    if (props.isAuthenticated) {
        return <Redirect to='/' />
    }

    return (
        <Wrapper color='#F2F2F2'>
            <FormWrapper>
                <Title>Log In</Title>
                <StyledForm data-testid='auth-form' onSubmit={e => onSubmit(e)}>
                    <FormField>
                        <Input
                            placeholder='Email'
                            type='email'
                            name='email'
                            value={email}
                            onChange={e => onChange(e)}
                            required
                        />
                    </FormField>
                    <FormField>
                        <Input
                            placeholder='Password'
                            type='password'
                            name='password'
                            value={password}
                            onChange={e => onChange(e)}
                            required
                        />
                    </FormField>
                    <Button type='submit' marginTop='6%'>LOG IN</Button>
                </StyledForm>
                <p>Don't have an account? <Link to='/register'>Sign Up!</Link> </p>
            </FormWrapper>
        </Wrapper>
    )
}

Login.propTypes = {
    login: PropTypes.func.isRequired,
    isAuthenticated: PropTypes.bool
}

const mapStateToProps = state => ({
    isAuthenticated: state.auth.isAuthenticated
});

export default connect(mapStateToProps, { login })(Login);

My Test file:

import React from 'react';
import { render, cleanup, fireEvent } from '@testing-library/react';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import store from '../../store';

import Login from '../Login';


afterEach(cleanup);
const onChange = jest.fn();


test('<Login>', () => {

    const { queryByTestId, getByPlaceholderText, getByText, debug } =
        render(
            <Provider store={store}>
                <Router>
                    <Login />
                </Router>
            </Provider>);

    expect(queryByTestId('auth-form')).toBeTruthy();

    fireEvent.change(getByPlaceholderText('Email'), {
        target: { value: '[email protected]' },
    });
    fireEvent.change(getByPlaceholderText('Password'), {
        target: { value: 'testpassword' },
    });

    fireEvent.click(getByText('LOG IN'));
    expect(onChange).toHaveBeenCalledTimes(1);

});

test output:

 FAIL  src/components/__tests__/Login.test.js
  ✕ <Login> (125 ms)

  ● <Login>

    expect(jest.fn()).toHaveBeenCalledTimes(expected)

    Expected number of calls: 1
    Received number of calls: 0

      32 | 
      33 |     fireEvent.click(getByText('LOG IN'));
    > 34 |     expect(onChange).toHaveBeenCalledTimes(1);
         |                      ^
      35 | 
      36 | });

      at Object.<anonymous>.test (src/components/__tests__/Login.test.js:34:22)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        6.013 s
Ran all test suites related to changed files.

Watch Usage: Press w to show more.

Is there a way to make onSubmit function accessible to the test without passing it trough as a prop?

1 Answer 1

4

The main point of the React Testing Library is that it's very easy to write tests that resembles on how users would use the application. This is what we want to achieve in order to give us full confidence and this is what's advocated in the industry as well. We write tests to raise confidence in our applications rather than to up the coverage.

Considering the above, you should not use toHaveBeenCalledTimes instead, you should test the visual result of triggering the onChange function. Based on the code you shared I don't know what is rendered upon successful login, but for our use case, let's say the text Welcome, User! renders.

You could fire an event as you do now, and instead of testing with haveBeenCalled, use the following:

expect(getByText('Welcome, User!').toBeInTheDocument();

This will give you full confidence as opposed to testing if a function was called which won't give you enough confidence because what if the implementation of the function is not correct?

Sign up to request clarification or add additional context in comments.

4 Comments

Thank you for your answer and sorry that I wasn't clear enough on what I'm trying to do in my test. haveBeenCalled was there just for me to get a better idea where the test fails. My goal is to test what user experiences. My onClick handler calls login action (redux) that sends request to the server and gets token back. I need to somehow mock these functions and this response in order to get a successful login and check for elements that show up after. right? Please correct me if I am going about this the wrong way.
I see you wrapped your test component in <Provider /> and injected in the store. That's fine, better than mocking, the test will cover the redux action as well, this is actually done often instead of separately writing tests for actions, reducers, etc. Based on your code and what you said, redirection is based on authentication which is based on the response from the backend. You need to mock out your response, and upon triggering the login, the logged-in page will show, that then can be asserted the way I advised. Mocking the HTTP responses can be done with something like nock.
If you decide to do this, know that your test'll be an integration test and not a unit test. So for anyone reading this, write this as an integration test in the parent component which has both your login page and "success" page, and not in the login page itself. Otherwise, there is no way to make the above assertion pass.
"integration"... it has more meaning than developer in the world. In what concept? Testing pyramid? Testing trophy? What you said is irrelevant. It's fine if you call something 'integration' but please be more explicit with your answer and explain what you mean by that so others will understand. The assertion will pass regardless, call it whatever you want. Just because you call something 'integration' that doesn't necessarily imply that the lowest parent component should be rendered for testing. And if it does in your vocabulary, fair enough, it's still irrelevant.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.