组件之间共享交互测试
#javascript #前端 #测试 #storybook

tl; dr

描述和代码示例,用于共享组件之间的测试。包括有关故事书样板组件的编写测试的信息。

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

先决条件知识

为什么在组件之间共享测试?

时间和金钱!

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

在代码库的不同部分之间共享测试可以有几个好处。如果您有两个或多个取决于相同功能的模块,则编写一次涵盖该功能的测试,然后在模块之间共享这些测试可能很有用。对于UI组件,这意味着共享单元测试,用户交流测试,您可以将它们堆叠在一起以制作深入的E2E测试套件。

测试故事书样板故事

使用Storybook的安装程序将Storybook添加到您的项目中时,安装程​​序包含三个带有stories文件的组件,以使您启动。三个样板的故事是:

  • 按钮
  • 标题,导入按钮
  • 页面,导入标题

这些通常是所有框架中相同的组成部分和故事。由于它们相互建立,因此它们是共享测试的绝佳用例。

测试按钮

该按钮是最复杂的,具有三个用于样式的道具,一个用于内容的道具,一个用于用户操作。

Reusable Storybook Interaction Tests中详细介绍了按钮测试。包含按钮测试的Button.shared-spec.js导出以下内容:

方法 描述
getElements 返回要测试的元素
ensureElements 有条件测试元素
mouseInteraction 有条件测试鼠标相互作用
keyboardInteraction 有条件测试键盘交互

测试标头

标题内部配置按钮组件并具有自己的道具。下表显示了标头接受的道具:

时调用函数 时调用函数 时调用函数
prop 描述
user 一个带有name属性的简单对象
onLogout 用户触发Log Out按钮
onLogin 函数在用户触发Log In按钮
onCreateAccount 用户触发Sign Up按钮

要从标题收集的元素

将根据道具有条件更改的事情:

元素 描述
buttons 标题中的所有按钮
title headingh1)元素
header header元素

headertitle始终是相​​同的,因为它们没有被props更改,但我们还是在测试它们以确保它们存在。

buttons将根据user prop。

更改

测试标头测试将运行

  • header元素是否存在?
  • title元素是否存在?
  • title元素是否包含文本Acme
  • 如果用户已登录:
    • 有一个按钮吗?
  • 如果用户未登录:
    • 有两个按钮吗?
/**
 * Ensure elements are present and have the correct attributes/content
 */
export const ensureElements = async (elements, args) => {
  await expect(elements.header).toBeTruthy();
  await expect(elements.title).toBeTruthy();
  await expect(elements.title).toHaveTextContent('Acme');
  if (args.user) {
    await expect(buttons).toHaveLength(1);
  } else {
    await expect(buttons).toHaveLength(2);
  }
  ...
}

标头让按钮测试本身

由于按钮组件具有自己的测试,因此标头组件不需要知道按钮的工作方式。它只需要知道该按钮有效。

使用按钮的ensureElements方法,标头测试每个按钮。

// Import the Button's shared tests
import { ensureElements as buttonEnsureElements } from "./Button.shared-spec";

...

export const ensureElements = async (elements, args) => {
  ... // Header's tests, see above

  if (args.user) {
    // Recreates button configuration from Header
    await buttonEnsureElements({ button: buttons[0] }, {
      label: 'Log out',
      size: 'small',
    });
  } else {
    await buttonEnsureElements({ button: buttons[0] }, {
      label: 'Log in',
      size: 'small',
    });
    await buttonEnsureElements({ button: buttons[1] }, {
      label: 'Sign up',
      size: 'small',
      primary: true,
    });
  }

用户交互是由按钮拥有的

标题中唯一的交互式元素是按钮,因此,标头唯一需要测试的是键盘用户如何到达按钮。其他所有内容,包括onLogoutonLoginonCreateAccount函数是否均由按钮所有。

标题测试可以通过触发tab userevent本身来获得按钮。

/**
 * Test keyboard interaction
 */
export const keyboardInteraction = async (elements, args) => {
  const { buttons, header } = elements;
  if (args.user) {
    // `focusTrap` keeps the user's keyboard within the `header` HTMLElement
    await userEvent.tab({ focusTrap: header });

    // we expect the `Log Out` button to have focus
    await expect(buttons[0]).toHaveFocus();

    // Uses Button's keyboard interaction tests
    await buttonKeyboardInteraction({ button: buttons[0] }, {
      label: 'Log out',
      size: 'small',
      onClick: args.onLogout,
    });
  } else {
    // again, starts within Header
    await userEvent.tab({ focusTrap: header });

    // we expect the `Log In` button to have focus
    await expect(buttons[0]).toHaveFocus();

    // User hits `tab` once more
    await userEvent.tab({ focusTrap: header });

    // we expect the `Sign Up` button to have focus
    await expect(buttons[1]).toHaveFocus();

    // Button tests for first button
    await buttonKeyboardInteraction({ button: buttons[0] }, {
      ...
    });

    // Button tests for second button
    await buttonKeyboardInteraction({ button: buttons[1] }, {
      ...
    });
  }
}

测试页面

页面组件没有道具,所有页面内容都在代码(TSK TSK)内部,因此我们将使用标题测试来测试页面。

fyi-在某些框架中,页面样板组件依赖于JS框架来控制其状态。当我们在框架之间共享这些测试时,我们将测试JS框架。

从页面收集的元素

在这种情况下,我们将使用标头的getElements方法获取我们需要的元素。

// Import the Button's shared tests
import { getElements as headerGetElements } from './Header.shared-spec';

/**
 * Extract elements from an HTMLElement
 */
export const getElements = async (canvasElement) => {
  const screen = within(canvasElement);

  // Header knows how to get it's own elements
  const headerElements = await headerGetElements(canvasElement);


  return {
    // spreads the headerElements into this object
    ...headerElements,
    screen,
  };
}

测试页面测试将运行

它将仅使用标头测试:

import { 
    ensureElements as headerEnsureElements,
    mouseInteraction as headerMouseInteraction,
    keyboardInteraction as headerKeyboardInteraction
} from './Header.shared-spec';

/**
 * Ensure elements are present and have the correct attributes/content
 */
export const ensureElements = async (elements, args, step) => {
  await headerEnsureElements(elements, args, step, true);
}

/**
 * Test mouse interaction
 */
export const mouseInteraction = async (elements, args, step) => {
  await headerMouseInteraction(elements, args, step, true);
}

/**
 * Test keyboard interaction
 */
export const keyboardInteraction = async (elements, args, step) => {
  await headerKeyboardInteraction(elements, args, step, true);
}

包起来

组件的自我知识是带有共享测试的lynchpin。尽可能深入地,一个组件应该知道可能发生的每种可能发生的变化,以及如何测试每种可能性。

在设计系统中使用它,这应该大大减少测试足迹。只要您编写testing-library测试,此概念应允许在单位测试中使用shared-spec测试,此外还可以在故事书故事中使用。

所有事物的效率 - 生产力将遵循!