Typescript、React 和 Socket.io-客户端测试

Typescript, React & Socket.io-client Tests

我想弄清楚如何编写一个 Typescript/React 应用程序,它使用 socket.io 与服务器通信,从而与其他客户端通信。但是我想在这样做的时候写一些测试。

在我的示例应用程序中,我有:

    import io, { Socket } from 'socket.io-client';
    
    const App = () => {
      let socket: Socket;
      const ENDPOINT = 'localhost:5000';
    
      const join = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
        event.preventDefault();
        socket = io(ENDPOINT);
        socket.emit('join', { name: 'Paola', room: '1' }, () => {});
      };
    
      return (
        <div className="join-container">
          <button className="join-button" onClick={join} data-testid={'join-button'}>
            Sign in
          </button>
        </div>
      );
    };
    
    export default App;

我的测试如下:

    import App from './App';
    import { render, screen, fireEvent } from '@testing-library/react';
    import 'setimmediate';
    
    
    describe('Join', () => {
      let mockEmitter = jest.fn();
      beforeEach(() => {
        jest.mock('socket.io-client', () => {
          const mockedSocket = {
            emit: mockEmitter,
            on: jest.fn((event: string, callback: Function) => {}),
          };
          return jest.fn(() => {
            return mockedSocket;
          }
            );
        });
      });
    
      afterEach(() => {
        jest.clearAllMocks();
      });
    
      it('joins a chat',  () => {
        // Arrange
        render(<App />);
    
        const btn =  screen.getByTestId('join-button');
    
        // Act
        fireEvent.click(btn);
    
        // Assert
        expect(btn).toBeInTheDocument();
        expect(mockEmitter).toHaveBeenCalled();
      });
    });

我只是想确保我可以模拟 socket.io-client 以便我可以验证消息是否正在发送到客户端并且它(稍后)对发送的消息做出反应。

但是测试失败了,它似乎没有使用我的模拟。

Error: expect(jest.fn()).toHaveBeenCalled()

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

manual-mocks#examples文档中,有一条注释:

Note: In order to mock properly, Jest needs jest.mock('moduleName') to be in the same scope as the require/import statement.

所以,有两种解决方案:

app.tsx:

import React from 'react';
import io, { Socket } from 'socket.io-client';

const App = () => {
  let socket: Socket;
  const ENDPOINT = 'localhost:5000';

  const join = (event: React.MouseEvent<HTMLButtonElement>) => {
    event.preventDefault();
    socket = io(ENDPOINT);
    socket.emit('join', { name: 'Paola', room: '1' }, () => {});
  };

  return (
    <div className="join-container">
      <button className="join-button" onClick={join} data-testid={'join-button'}>
        Sign in
      </button>
    </div>
  );
};

export default App;

选项 1:调用 jest.mock 并在测试文件的模块范围内导入 ./app 模块。

app.test.tsx:

import App from './App';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import React from 'react';

let mockEmitter = jest.fn();
jest.mock('socket.io-client', () => {
  return jest.fn(() => ({
    emit: mockEmitter,
    on: jest.fn(),
  }));
});

describe('Join', () => {
  afterEach(() => {
    jest.clearAllMocks();
  });

  it('joins a chat', () => {
    // Arrange
    render(<App />);
    const btn = screen.getByTestId('join-button');

    // Act
    fireEvent.click(btn);

    // Assert
    expect(btn).toBeInTheDocument();
    expect(mockEmitter).toHaveBeenCalled();
  });
});

选项2:因为你在beforeEach钩子中调用了jest.mockrequirebeforeEach钩子函数中的'./app'模块范围也是如此。

app.test.tsx:

import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import React from 'react';

describe('Join', () => {
  let mockEmitter = jest.fn();
  let App;
  beforeEach(() => {
    App = require('./app').default;
    jest.mock('socket.io-client', () => {
      const mockedSocket = {
        emit: mockEmitter,
        on: jest.fn(),
      };
      return jest.fn(() => mockedSocket);
    });
  });

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

  it('joins a chat', () => {
    // Arrange
    render(<App />);
    const btn = screen.getByTestId('join-button');

    // Act
    fireEvent.click(btn);

    // Assert
    expect(btn).toBeInTheDocument();
    expect(fakeEmitter).toHaveBeenCalled();
  });
});

包版本:

"jest": "^26.6.3",
"ts-jest": "^26.4.4"

jest.config.js:

module.exports = {
  preset: 'ts-jest/presets/js-with-ts',
  testEnvironment: 'jsdom'
}