2

我想测试messageListener是否将数据附加到消息数组状态。但是,我对此有一些障碍。1. 无法在无状态组件内部测试方法 2. React 钩子不能在组件函数外部使用。

我不知道如何解决这个问题?我应该使用不同的方法来测试吗?我应该将其提取到自定义钩子中吗?我是 Enzyme 和 React 钩子的新手。我基本上想测试如果 socket.io 接收到 MESSAGE 事件,该组件会将另一个项目附加到要在 MessageFeed 中显示的消息数组中。

完整的项目也可以在https://github.com/Hyllesen/react-chat上看到

应用程序.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 };

应用程序.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
  });
});
4

1 回答 1

1

到目前为止,您的代码不能按预期工作:

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

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

看,messageListener尝试调用setMessages在另一个函数中声明为局部变量的那个。

所以它应该在里面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>
  );
};

现在,让我们谈谈测试。如果你重新考虑你的方法,编写测试会容易得多。我的意思是,通过调用回调props和模拟函数进行通信,并仅针对渲染结果和模拟进行验证。无法访问状态、语言环境变量或模拟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"}]);
  });
});

看,我们实际上不需要关注内部。访问Props和模拟就足够了。调用io.on.mock.calls[0][1]可能看起来很棘手,但它是安全的,因为我们验证了只传递了一个侦听器,所以我们不需要在这里关心它是否是侦听器eventTypes.MESSAGE。可能有更复杂的组件,我会模拟io.client更聪明,所以我可以ioMocked.simulateEmitForListenerByType(eventTypes.MESSAGE);或寻找现有的 package

于 2019-09-29T08:05:04.427 回答