共享测试 - 如何编写可重复使用的故事书交互测试
#javascript #前端 #测试 #storybook

tl; dr

编写可重复使用的测试意味着它们可以在组件之间和组件之间共享。

目标:减少测试足迹,同时增加覆盖范围

什么是共享测试?

共享测试是为UI组件创建可重复使用的Testing Library测试的概念。编写可重复使用的测试,该测试符合测试库查询节点的范式的范式,可以使共享测试通常为 javaScript框架agnostic agnostic

该概念期望测试套件在函数范围内,这些功能是作为参数的渲染UI和用于渲染UI的道具的参数。在该方法内部,应根据用于配置UI的道具有条件地进行断言。

每个组件都应有一组共享测试,该测试应涵盖组件的所有可能配置。

实时代码示例!

请参阅this codesandbox Storybook appliction,其中包含为样板故事书示例组件编写的一组共享测试。

共享测试的目的是什么?

共享测试使您的总体代码库保持干燥,如Don't Repeat Yourself

如果测试涵盖了UI组件的所有配置可能性,则可以在不编写新测试的情况下添加任意数量的测试示例。一组共享的测试功能可以在故事书交互测试和规格/单位测试中使用,甚至可以使用E2E,它们被配置为使用测试图语法进行测试。

使用子组件测试的父组件意味着父组件不需要知道它是否正确实现了子 - 孩子的测试将确认这一点。这种关系可以创建一组非常深,全覆盖的末端2端测试。

是什么组成了一组共享测试?

最基本的是,我们需要回答以下问题:

  1. “使用我的道具渲染组件时呈现的是什么?”
  2. “该组件是否正确渲染了?”
  3. “当我与之互动时,它是否正常运行?” (如果互动)

问题1.“什么是渲染的?”

我们需要查询渲染UI的所有可能测试元素。查找这些元素的函数应返回代表每个元素的对象,即使它们返回null

一个简单的示例:

export const getElements = async (canvasElement: HTMLElement) => {
  const screen = within(canvasElement);

  return { 
    screen,

    // Searches the canvasElement for a button
    button: await screen.queryByRole('button'),
  };
}

这将带有渲染按钮和初始HTMLElement的对象返回对象。两者都通过testing-library's "within" method增强。 within(node) 拿一个Element,并返回具有与之绑定的所有测试图形查询的对象。

请注意使用queryBy代替getByfindBy。为了使此函数可共享,理想情况下,如果没有任何元素不存在,则不应丢下错误。

问题2.“它正确渲染了吗?”

我们知道 渲染了什么,现在我们需要按预期进行测试。

我们需要一种具有条件应用的断言的方法,该方法是根据UI组件作为props的。

此示例使用截断的版本的样板按钮组件在初始化新的故事书实例时已安装。

// A Button component
export const Button = ({ primary = false, label = 'Button', onClick = () => ({}) }) => {
  const modeClass = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
  return (
    <button
      type="button"
      className={modeClass}
      @click=${onClick}
    >
      {label}
    </button>
  );
};

此示例方法可确保按预期呈现的组件呈现。它包含有条件的断言,这些断言理解预期的输出并相应调整测试。

// `args` are the props used to configure the Button component
export const ensureElements = async (canvasElement: HTMLElement, args: ButtonProps) => {
  // uses method from above
  const { button } = getElements(canvasElement);

  // does the button exist?
  await expect(button).toBeTruthy();

  // does the button have the right content?
  await expect(button).toHaveTextContent(args.label);

  // conditional tests
  if (args.primary) {
    await expect(button).toHaveClass('storybook-button--primary');
  } else {
    await expect(button).toHaveClass('storybook-button--secondary');
  }
};

问题3:“它可以按预期运行吗?”

如果您的组件是交互式的,则需要测试所有类型的交互式。这至少意味着检查组件功能的功能 仅键盘用户和鼠标用户。

这次我们将创建两种方法。这些方法将使用testing-library's "user-event" library

// create a mockFunction as a backup if no `onClick` is in `args`
const mockOnClick = jest.fn();

/**
 * test mouse interactions as though a user
 */
export const mouseInteraction = async (canvasElement, args) => {
  // uses method from above
  const { button } = getElements(canvasElement);

  // ensures a function exists to test a mouse click
  const onClick = args.onClick ? args.onClick : mockOnClick;

  // clicks the button
  await userEvent.click(button);

  // tests the `onClick` function was called
  await expect(onClick).toHaveBeenCalled();

  // ensures the event was only called once
  await expect(onClick).toHaveBeenCalledTimes(1);

  // clear out the mock which resets calls to zero
  await onClick.mockClear();
}

/**
 * test keyboard interactions as though a user
 */
export const keyboardInteraction = async (canvasElement, args) => {
  // uses method from above
  const { button } = getElements(canvasElement);

  // ensures a function exists to test a key stroke
  const onClick = args.onClick ? args.onClick : mockOnClick;

  // gives the user's focus to the button
  await button.focus();

  // with the button focused, hitting `enter` triggers `@click`
  await userEvent.keyboard('{enter}');

  // tests the `onClick` function was called
  await expect(onClick).toHaveBeenCalled();

  // good thing we cleared the mock from above, or this would be `2`
  await expect(onClick).toHaveBeenCalledTimes(1);

  // clear out the mock which resets calls to zero
  await onClick.mockClear();
}

他们如何在故事书互动测试中使用

参见Button.stories.ts on codesandbox

// Button.stories.ts
export const Primary: Story = {
  args: {
    primary: true,
    label: 'Button',
  },
  play: async ({ args, canvasElement, step }) => {

    // "What was rendered?"
    const elements = await getElements(canvasElement);

    // "Did it render correctly?"
    await ensureElements(elements, args, step);

    // "Does it function as expected for mouse users?"
    await mouseInteraction(elements, args, step);

    // "Does it function as expected for keyboard-only users?"
    await keyboardInteraction(elements, args, step);
  },
};

export const Secondary: Story = {
  args: {
    label: 'Button',
  },

  // Reuse the exact same tests
  play: Primary.play,
};

export const Large: Story = {
  args: {
    size: 'large',
    label: 'Button',
  },

  // again and again
  play: Primary.play,
};

接下来,第2部分“共享儿童和父母之间的测试”