2

I want to test if the messageListener appends the data to the messages array state. However I've gotten some blocks on this. 1. Can't test methods inside a stateless component 2. React hooks cannot be used outside a component function.

I can't figure out how to get around this? Should I use a different approach for testing this? Should I extract it into a custom hook? I'm new to both Enzyme and React hooks. I basically want to test if given that socket.io receives the MESSAGE event, the component will append another item to the message array to be displayed inside MessageFeed.

The full project can also be seen on https://github.com/Hyllesen/react-chat

App.js

import React from "react";
import UsernameInput from "components/UsernameInput";
import MessageFeed from "components/MessageFeed";
import io from "socket.io-client";
import * as eventTypes from "eventTypes";

function joinWithUsername(username) {
  const socket = io("http://localhost:3001");
  socket.emit(eventTypes.USER_JOIN, { username });
  socket.on(eventTypes.MESSAGE, messageListener);
}

function messageListener(data, setMessages) {
  setMessages([...messages, data]);
}

const App = () => {
  const [messages, setMessages] = React.useState([]);

  return (
    <div className="App">
      <UsernameInput onSubmit={joinWithUsername} />
      <MessageFeed messages={messages} />
    </div>
  );
};

export default App;
export { joinWithUsername, messageListener };

App.test.js

import React from "react";
import App, { joinWithUsername, messageListener } from "../App";
import { shallow } from "enzyme";
import io from "socket.io-client";
import * as eventTypes from "eventTypes";

let wrapper, setState, useStateSpy;
jest.mock("socket.io-client", () => {
  const emit = jest.fn();
  const on = jest.fn();
  const socket = { emit, on };
  return jest.fn(() => socket);
});

describe("App", () => {
  beforeEach(() => {
    wrapper = shallow(<App />);
    setState = jest.fn();
    useStateSpy = jest.spyOn(React, "useState");
    useStateSpy.mockImplementation(init => [init, setState]);
  });

  afterEach(() => jest.clearAllMocks());

  it("has a usernameinput component", () => {
    expect(wrapper.find("UsernameInput").length).toBe(1);
  });

  it("has a joinWithUsername function", () => {
    const usernameInput = wrapper.find("UsernameInput").props();
    console.log(usernameInput.onSubmit);
    expect(usernameInput.onSubmit).toBe(joinWithUsername);
  });

  it("connect to socket.io server", () => {
    joinWithUsername("Bobby");
    expect(io).toHaveBeenCalledWith("http://localhost:3001");
  });

  it("emits the USER_JOIN event", () => {
    joinWithUsername("John");
    const socket = io();
    expect(socket.emit).toHaveBeenCalledWith(eventTypes.USER_JOIN, {
      username: "John"
    });
  });

  it("listens for the MESSAGE event", () => {
    joinWithUsername("John");
    const socket = io();
    expect(socket.on).toHaveBeenCalledWith(eventTypes.MESSAGE, messageListener);
  });

  it("Appends a message to state when message is received", () => {
    const testData = {
      from: "John",
      message: "Hello everyone!"
    };
    messageListener(testData);
    expect(setState).toHaveBeenCalledWith(testData); //ReferenceError: setMessages is not defined
  });
});

1 Answer 1

1

By now your code does not work as intendend:

function messageListener(data, setMessages) {
  setMessages([...messages, data]);
}

const App = () => {
  const [messages, setMessages] = React.useState([]);
...
};

see, messageListener tries to call setMessages that's declared as a local variable in another function.

So it should be all inside App:

const App = () => {
  const [messages, setMessages] = React.useState([]);

  function joinWithUsername(username) {
    const socket = io("http://localhost:3001");
    socket.emit(eventTypes.USER_JOIN, { username });
    socket.on(eventTypes.MESSAGE, messageListener);
  }

  function messageListener(data, setMessages) {
    setMessages([...messages, data]);
  }


  return (
    <div className="App">
      <UsernameInput onSubmit={joinWithUsername} />
      <MessageFeed messages={messages} />
    </div>
  );
};

Now, let's talk about test. If you reconsider your approach it will be much easier to write tests. I mean, to communicate by calling callback props and mocked function and to validate against render result and mocks' called only. No access to state, locale variables or mocking useState

import React from "react";
import App from "../App";
import { shallow } from "enzyme";
import io from "socket.io-client";
import * as eventTypes from "eventTypes";

jest.mock("socket.io-client", () => {
  const emit = jest.fn();
  const on = jest.fn();
  const socket = { emit, on };
  return jest.fn(() => socket);
});

describe("<App />", () => {
  beforeEach(() => { 
    io.mockClear();
    io().on.mockClear();
    io().emit.mockClear();
  });

  it("sends data on login and subscribes for response", () => {
    const root = shallow(<App />);
    root.find(UsernameInput).props().onSubmit("_someuser_"); 
    expect(io).toHaveBeenCalledWith("http://localhost:3001");
    expect(io().emit).toHaveBeenCalledWith(eventTypes.USER_JOIN, { username: "_someuser_" });   
    expect(io().on).toHaveBeenCalledTimes(1);
    expect(io().on.mock.calls[0][0]).toEqual(eventTypes.MESSAGE);
    // we don't check what function has been passed; it does not matter here
  });

  it("adds message after server responded", () => {
    const root = shallow(<App />);
    root.find(UsernameInput).props().onSubmit("_someuser_"); 
    io().on.mock.calls[0][1]({fakeMessage: "test"}); // we don't care where this listener is coming from
    expect(root.find(Messages).props().messages).toEqual([{fakeMessage: "test"}]);
  });
});

See, we actually don't need to focus on internals. Accessing Props and mocks should be enough. Calling io.on.mock.calls[0][1] may look tricky but it's safe since we validated there is just single listener passed so we don't need to care here if it's listener for eventTypes.MESSAGE or not. Having more complex component, probably, I'd mock io.client to be more smart so I could ioMocked.simulateEmitForListenerByType(eventTypes.MESSAGE); or look for existing package.

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

2 Comments

Interesting approach, but I'm pretty much stuck at the same place. The test for "adds message after server responded" is not passing for me. The MessageFeed is not receiving the array. I'm not sure what io().on.mock.calls[0][1]({ fakeMessage: "test"}); does? Is that making a fake .on response? I always thought .calls it was just for reading/getting mock calls.
fixed few typos in code(like wrong case for onsubmit) - it passes to me. io().on.mock.calls([0][1])({ fakeMessage: "test" }) takes calls history for io.on, takes second argument from first call to this mock and execute it. Now, after some time, I see this approach looking weird and suggest to use socket.io-mock instead.

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.