How do I test component methods on a React component that are defined as arrow functions (class properties)?
Asked Answered
J

1

5

I am able to test class methods just fine by using spies and Component.prototype. However, many of my class methods are class properties because I need to use this (for this.setState, etc.), and since binding in the constructor is very tedious and looks ugly, using arrow functions is much better in my opinion. The components I have built using class properties work in the browser, so I know my babel config is correct. Below is the component I am trying to test:

    //Chat.js
    import React from 'react';
    import { connect } from 'react-redux';

    import { fetchThreadById, passMessageToRedux } from '../actions/social';
    import withLogin from './hoc/withLogin';
    import withTargetUser from './hoc/withTargetUser';
    import withSocket from './hoc/withSocket';
    import ChatMessagesList from './ChatMessagesList';
    import ChatForm from './ChatForm';

    export class Chat extends React.Component {
        state = {
            messages : [],
        };
        componentDidMount() {
            const { auth, targetUser, fetchThreadById, passMessageToRedux } = this.props;
            const threadId = this.sortIds(auth._id, targetUser._id);
            //Using the exact same naming scheme for the socket.io rooms as the client-side threads here
            const roomId = threadId;
            fetchThreadById(threadId);
            const socket = this.props.socket;
            socket.on('connect', () => {
                console.log(socket.id);
                socket.emit('join room', roomId);
            });
            socket.on('chat message', message => passMessageToRedux(message));
            //socket.on('chat message', message => {
            //    console.log(message);
            //    this.setState(prevState => ({ messages: [ ...prevState.messages, message ] }));
            //});
        }

        sortIds = (a, b) => (a < b ? `${a}_${b}` : `${b}_${a}`);

        render() {
            const { messages, targetUser } = this.props;
            return (
                <div className='chat'>
                    <h1>Du snakker med {targetUser.social.chatName || targetUser.info.displayName}</h1>
                    <ChatMessagesList messages={messages} />
                    <ChatForm socket={this.props.socket} />
                </div>
            );
        }
    }
    const mapStateToProps = ({ chat: { messages } }) => ({ messages });

    const mapDispatchToProps = dispatch => ({
        fetchThreadById    : id => dispatch(fetchThreadById(id)),
        passMessageToRedux : message => dispatch(passMessageToRedux(message)),
    });

    export default withLogin(
        withTargetUser(withSocket(connect(mapStateToProps, mapDispatchToProps)(Chat))),
    );

    Chat.defaultProps = {
        messages : [],
    };

And here is the test file:

//Chat.test.js
import React from 'react';
import { shallow } from 'enzyme';
import { Server, SocketIO } from 'mock-socket';

import { Chat } from '../Chat';
import users from '../../fixtures/users';
import chatMessages from '../../fixtures/messages';

let props,
    auth,
    targetUser,
    fetchThreadById,
    passMessageToRedux,
    socket,
    messages,
    wrapper,
    mockServer,
    spy;

beforeEach(() => {
    window.io = SocketIO;
    mockServer = new Server('http://localhost:5000');
    mockServer.on('connection', server => {
        mockServer.emit('chat message', chatMessages[0]);
    });
    auth = users[0];
    messages = [ chatMessages[0], chatMessages[1] ];
    targetUser = users[1];
    fetchThreadById = jest.fn();
    passMessageToRedux = jest.fn();
    socket = new io('http://localhost:5000');
    props = {
        mockServer,
        auth,
        messages,
        targetUser,
        fetchThreadById,
        passMessageToRedux,
        socket,
    };
});

afterEach(() => {
    mockServer.close();
    jest.clearAllMocks();
});

test('Chat renders correctly', () => {
    const wrapper = shallow(<Chat {...props} />);
    expect(wrapper).toMatchSnapshot();
});

test('Chat calls fetchThreadById in componentDidMount', () => {
    const wrapper = shallow(<Chat {...props} />);
    const getThreadId = (a, b) => (a > b ? `${b}_${a}` : `${a}_${b}`);
    const threadId = getThreadId(auth._id, targetUser._id);
    expect(fetchThreadById).toHaveBeenLastCalledWith(threadId);
});

test('Chat calls componentDidMount', () => {
    spy = jest.spyOn(Chat.prototype, 'componentDidMount');
    const wrapper = shallow(<Chat {...props} />);
    expect(spy).toHaveBeenCalled();
});

test('sortIds correctly sorts ids and returns threadId', () => {
    spy = jest.spyOn(Chat.prototype, 'sortIds');
    const wrapper = shallow(<Chat {...props} />);
    expect(spy).toHaveBeenCalled();
});

The second to last test which checks if componentDidMount(not a class method) was called runs with no errors, as do all the other tests except the last one. For the last test, Jest gives me the following error:

FAIL  src\components\tests\Chat.test.js
  ● sortIds correctly sorts ids and returns threadId

    Cannot spy the sortIds property because it is not a function; undefined given instead

      65 |
      66 | test('sortIds correctly sorts ids and returns threadId', () => {
    > 67 |     spy = jest.spyOn(Chat.prototype, 'sortIds');
      68 |     const wrapper = shallow(<Chat {...props} />);
      69 |     expect(spy).toHaveBeenCalled();
      70 | });

      at ModuleMockerClass.spyOn (node_modules/jest-mock/build/index.js:699:15)
      at Object.<anonymous> (src/components/tests/Chat.test.js:67:16)

I have been told that I can use mount from enzyme instead of shallow and then use Chat.instance instead of Chat.prototype, but to my understanding, if I do so, enzyme will also render Chat's children, and I certainly do not want that. I actually tried using mount, but then Jest started complaining about connect(ChatForm) not having store in either its context or props (ChatForm is connected to redux, but I like to test my redux-connected components by importing the non-connected component and mocking a redux store). Does anyone know how to test class properties on React components with Jest and Enzyme? Thanks a bunch in advance!

Jordonjorey answered 31/3, 2018 at 0:3 Comment(2)
I'm not sure why you are using the property initializer syntax for sortIds when it doesn't use this. Also, what aspect are you testing, that componentDidMount called the method, or that it computed a valid result for its inputs? It seems the latter in the case of your test description, in which case it can be tested in isolation of the class being instantiated.Protectorate
You are completely right it doesn't use this! It did use this, but then my tests didn't work, and during debugging I removed this from it, but I'll put it back in now :). Thanks for the comment, but I've cleared everything up now, and everything is working. Have a nice day!Jordonjorey
E
11

Even if the rendering is shallow, you can call the wrapper.instance() method.

it("should call sort ids", () => {
    const wrapper = shallow(<Chat />);
    wrapper.instance().sortIds = jest.fn();
    wrapper.update();    // Force re-rendering 
    wrapper.instance().componentDidMount();
    expect(wrapper.instance().sortIds).toBeCalled();
 });
Exclusive answered 31/3, 2018 at 2:52 Comment(1)
Thanks! This is just what I was looking for! I will accept your answer.Jordonjorey

© 2022 - 2024 — McMap. All rights reserved.