测试 setState 匿名函数调用

Testing setState anonymous function call

考虑 JSX 中的以下按钮:

interface Props {
  index: number;
  setIndex: Dispatch<SetStateAction<number>>;
}

export function Button({ index, setIndex }: Props) {
  return (
    <button
      type="button"
      data-testid="prev_btn"
      onClick={() => setIndex(prevIndex => prevIndex - 1)}
    >
      Previous
    </button>
  );
}

我想在 Jest / React 测试库中编写一个测试来呈现按钮并将 setIndex 作为道具传递,然后检查是否使用正确的值调用了 setIndex。例如:

const mockSetIndex = jest.fn();
const mockProps = {
  setIndex: mockSetIndex,
  index: 1,
};

describe('Button test', () => {
  it('should update the index when the "previous" button is clicked', () => {
    const { getByTestId } = render(<Button {...mockProps} />);
    const prevBtn = getByTestId('prev_btn');

    userEvent.click(prevBtn);

    expect(mockSetIndex).toBeCalledWith(0);
  });
});

我得到的结果是失败的:

    expect(jest.fn()).toBeCalledWith(...expected)

    Expected: 0
    Received: [Function anonymous]

还有一个类似按钮的实例增加索引,所以我想测试一下这两个按钮的行为是否正确。

您是在调用 setIndex 时使用的是函数而不是数字。虽然这是 setIndex 可接受的论点,但它不是您要测试的内容。将您的功能更改为

interface Props {
  index: number;
  setIndex: Dispatch<SetStateAction<number>>;
}

export function Button({ index, setIndex }: Props) {
  return (
    <button
      type="button"
      data-testid="prev_btn"
      onClick={() => setIndex(index - 1)}
    >
      Previous
    </button>
  );
}

或测试它是否传递了具有预期行为的函数

describe('Button test', () => {
  it('should update the index when the "previous" button is clicked', () => {
    const { getByTestId } = render(<Button {...mockProps} />);
    const prevBtn = getByTestId('prev_btn');

    userEvent.click(prevBtn);

    const passedFunction = mockProps.setIndex.mock.calls[0][0]

    expect(passedFunction(1)).toEqual(0);
  });
});

根据 Button 的属性,我猜它的用法是这样的:

const Parent = () => {
  const [index, setIndex] = useState(1);
  return (
    <>
      <Button index={index} setIndex={setIndex} />
      <p data-testid="current_index">Current index: {index}</p>
    </>
  );
};

这让 Parent 无法控制自己的状态,Button 可以调用例如setIndex(100) 是否有任何实际意义。由于这个原因,将 setter 直接传递给另一个组件通常是一种代码味道。


耦合度较低的结构,其中 Parent 负责其自身的状态,如下所示:

const Parent = () => {
  const [index, setIndex] = useState(1);

  const decrement = () => setIndex((previousIndex) => previousIndex - 1);

  return (
    <>
      <p>Current index: {index}</p>
      <Button onClick={decrement} />
    </>
  );
};

更容易测试:

describe("Button", () => {
  it("should signal when the Previous button is clicked", () => {
    const onClick = jest.fn();
    render(<Button onClick={onClick} />);

    userEvent.click(screen.getByRole("button", { name: /previous/i }));

    expect(onClick).toHaveBeenCalled();
  });
});

(注意我遵循 priority guide 使用 user-facing,尽可能使用可访问的选择器。)


或者,如果 Button Parent 中使用(正如状态上的高耦合级别所暗示的那样),请在该上下文中对其进行测试:

describe("Parent", () => {
  it("should allow the previous thing to be selected", () => {
    render(<Parent />);

    userEvent.click(screen.getByRole("button", { name: /previous/i }));

    // assert on actual outcome of index changing, e.g.
    expect(screen.getByTestId("current_index")).toHaveTextContent("Current index: 0");
  });
});

在这种情况下,在 Button 及其 Parent 之间的边界进行测试并不合适。要指示这种耦合,您可以将 Button 移动到定义 Parent 的模块中,并且 not 将其导出以供单独访问 - 处理按钮具有的事实作为 实施细节 .

提取到单独的组件