How to test a react connected component and what to test of component?

Viewed 2270

Following is a simple login component in my learning application.

I'm trying to write test of the following component via jest and testing-library/react

import React, { useEffect } from 'react';
import { Form, Input, Button, Checkbox } from 'antd';
import { connect } from "react-redux";
import { userLoginThunk } from "../Thunk/LoginThunk";
import { getUserState } from '../selector';

const Login = ({ onFormSubmitFailed, onLoginPressed, history }) => {
    const layout = {
        labelCol: { span: 8 },
        wrapperCol: { span: 8 },
    };
    const tailLayout = {
        wrapperCol: { offset: 8, span: 16 },
    };
    const onFinish = (values) => {
        onLoginPressed(values, history);
    };

    const onFinishFailed = (errorInfo) => {
        onFormSubmitFailed();
    };
    
    return (
        <Form data-testid="loginForm"
            {...layout}
            style={{
                padding: '30vh',
                alignItems: 'center'
            }}
            name="basic"
            initialValues={{ remember: true }}
            onFinish={onFinish}
            onFinishFailed={onFinishFailed}
        >
            <Form.Item
                label="Username"
                name="username"
                rules={[{ required: true, message: 'Please input your username!' }]}
            >
                <Input data-testid="UsernameInput" placeholder="UserName" />
            </Form.Item>

            <Form.Item
                label="Password"
                name="password"
                rules={[{ required: true, message: 'Please input your password!' }]}
            >
                <Input.Password data-testid="PasswordInput" placeholder="Password" />
            </Form.Item>

            <Form.Item {...tailLayout}>
                <Button data-testid="submitButton" type="primary" htmlType="submit">
                    Submit
                 </Button>
            </Form.Item>
        </Form>
    );
};


// const mapStateToProps = state => ({
//     logingIn: getUserState(state),
// });

// const mapDispatchToProp = dispatch => ({
//     onLoginPressed: (userObj, history) => dispatch(userLoginThunk(userObj, history)),
// });
export default connect((state) => ({
    logingIn: getUserState(state)
}), {
    onLoginPressed: (userObj, history) => userLoginThunk(userObj, history)
})(Login);
//export default connect(mapStateToProps, mapDispatchToProp)(Login);

I read in a discussion that we don't need mapStateToProps, mapDispatchToProp so I removed them too but still my test is getting failed.

What i'm trying to achieve is that passing a mock function as prop and recording that It was executed once as my
onFinish function is getting called in my login component, but as I have passed my mocked jest function as a prop it's not executing and passing my test.

test.only("Login On Submit", () => {
        const onLoginPressed = jest.fn();   
        //(() => {throw new Error('QWERTY')});
        // // (values, history) => {
        // //     console.log('Test');
        // //     console.log('inside Test', values)
        // // }
        const { getByTestId } = render(
            <Provider store={storeMock}>
                <Login onLoginPressed={onLoginPressed} />
            </Provider>
        );
        fireEvent.change(screen.getByTestId(/UsernameInput/i), {
            target: { value: 'admin' }
        });
        fireEvent.change(screen.getByTestId(/PasswordInput/i), {
            target: { value: 'admin' }
        });
        fireEvent.click(screen.getByTestId("submitButton"));
        expect(onLoginPressed).toHaveBeenCalled();

    });

Test result expect(jest.fn()).toHaveBeenCalled()

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

  90 |         fireEvent.click(screen.getByTestId("submitButton"));
  91 |         console.log("debug after testUtil render", debug());
> 92 |         expect(onLoginPressed).toHaveBeenCalled();
     |                                ^
  93 | 
  94 |     });
  95 | });
2 Answers

Issue

The issue here is that your component is still having props from redux injected into it by the connect Higher Order Component. (You removed mapStateToProps and mapDispatchToProps but you've still "defined" them inline in connect) The injected onLoginPressed is overwriting your manually passed jest mock function.

Solution

IMO the easy solution, since you are still using the connect decorator, is to export the undecorated/unconnected Login component so you can pass the props that would normally be provided by redux.

export Login = ({ onFormSubmitFailed, onLoginPressed, history }) => {...

Test

import { Login } from './path/to/Login'; // <-- named import

...

test.only("Login On Submit", () => {
    const onLoginPressed = jest.fn();
    const { getByTestId } = render(Login onLoginPressed={onLoginPressed} />);

    fireEvent.change(screen.getByTestId(/UsernameInput/i), {
        target: { value: 'admin' }
    });
    fireEvent.change(screen.getByTestId(/PasswordInput/i), {
        target: { value: 'admin' }
    });
    fireEvent.click(screen.getByTestId("submitButton"));

    expect(onLoginPressed).toHaveBeenCalled();

});

Solution #2

Mock your store using redux-mock-store and assert the correct action(s) was/were dispatched. Here the mock store simply stores an array of dispatched actions to test against.

test.only("Login On Submit", () => {
    const store = mockStore({});
    const { getByTestId } = render(
        <Provider store={store}>
            <Login onLoginPressed={onLoginPressed} />
        </Provider>
    );

    fireEvent.change(screen.getByTestId(/UsernameInput/i), {
        target: { value: 'admin' }
    });
    fireEvent.change(screen.getByTestId(/PasswordInput/i), {
        target: { value: 'admin' }
    });
    fireEvent.click(screen.getByTestId("submitButton"));

    const actions = store.getActions();
    expect(actions).toEqual([{ type: 'LOGIN' }]); // <-- your action type here!
});

I think there's a few key issues with the overall approach to testing that we should address:

  1. How do we test Redux connected components?
  2. How do we test behavior?

Test Connected Components by hijacking render()

Our team has a testUtils wrapper that hijacks the react-testing-library render() method and wraps components with a Redux Provider. You can find it on the Redux docs.

// test-utils.js
import React from 'react'
import { render as rtlRender } from '@testing-library/react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
// Import your own reducer
import reducer from '../reducer'

function render(
  ui,
  {
    initialState,
    store = createStore(reducer, initialState),
    ...renderOptions
  } = {}
) {
  function Wrapper({ children }) {
    return <Provider store={store}>{children}</Provider>
  }
  return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
}

// re-export everything
export * from '@testing-library/react'
// override render method
export { render }

What's cool about this solution is you get your Redux store and you can initialize state the way you need.

Test behavior, not implementation details

For the next issue, I wouldn't test the mock/implementation. Does your form submit to a backend? Does it show a success message? You could use that to determine the test assertions. Below I used your test with the wrapped render() method and look for a flash message that signifies successful login.

test.only("<Login/>", async () => {
   // You can stub the server response with nock or msw
    nock(`${yoursite}`)
      .post('/someApiEndpoint')
      .reply(200);
    
    const { getByTestId } = render(
      <Login />
    );

    fireEvent.change(screen.getByTestId(/UsernameInput/i), {
        target: { value: 'admin' }
    });
    fireEvent.change(screen.getByTestId(/PasswordInput/i), {
        target: { value: 'admin' }
    });
    fireEvent.click(screen.getByTestId("submitButton"));
    
    await waitFor(() => expect(getById('login-success')).toBeTruthy()); // <-- This is missing
});

What, as a user, would signify a successful login?

Related