发布于 2026-01-06 2 阅读
0

Web 组件的测试工作流程

Web 组件的测试工作流程

当你发布供他人使用的代码时,你就肩负着提供安全稳定代码的责任。解决这个问题的方法之一就是测试你的代码。

无论项目多么小,无论项目多么简单,理想情况下都应该进行测试。

我知道现实很残酷,很多情况下检测结果都不会如预期那样——但你始终应该努力去做检测。

免责声明

在本教程中,我们将创建一个简单的输入元素。完成本教程后,您将掌握运用 open-wc 测试工具的技能和知识,并构建一个稳定、易用且经过充分测试的输入组件。

警告

这是一篇深入的教程,展示了使用 Web 组件时的一些陷阱和棘手案例。本教程面向高级用户。您应该具备LitElementJSDoc 类型的基本知识。了解MochaChai BDDKarma也可能有所帮助。

我们正在考虑发布一个更容易理解的版本,如果您想看到这样的版本,请在评论中告诉我们。

如果你想一起玩——所有代码都在GitHub上。

让我们开始吧!

在控制台中运行

$ npm init @open-wc

# Results in this flow
✔ What would you like to do today? › Scaffold a new project
✔ What would you like to scaffold? › Web Component
# Select with space! "Testing" => just enter will move one with no selection
✔ What would you like to add? › Testing
✔ Would you like to scaffold examples files for? › Testing
✔ What is the tag name of your application/web component? … a11y-input
✔ Do you want to write this file structure to disk? › Yes
Writing..... done
✔ Do you want to install dependencies? › No
Enter fullscreen mode Exit fullscreen mode

更多详情请参见https://open-wc.org/testing/

删除src/A11yInput.js

修改src/a11y-input.js为:

import { LitElement, html, css } from 'lit-element';

export class A11yInput extends LitElement {}

customElements.define('a11y-input', A11yInput);
Enter fullscreen mode Exit fullscreen mode

以及test/a11y-input.test.js

/* eslint-disable no-unused-expressions */
import { html, fixture, expect } from '@open-wc/testing';

import '../src/a11y-input.js';

/**
 * @typedef {import('../src/a11y-input.js').A11yInput} A11yInput
 */

describe('a11y input', () => {
  it('has by default an empty string as label', async () => {
    const el = /** @type {A11yInput} */ (await fixture('<a11y-input></a11y-input>'));
    expect(el.label).to.equal('');
  });
});
Enter fullscreen mode Exit fullscreen mode

我们目前的测试只包含一个特性(label属性)和一个断言expect。我们使用 Karma 和 Chai 的BDD语法,因此我们将测试集it按它们相关的特性或 API进行分组describe

让我们运行以下命令来查看一切是否正常:npm run test

SUMMARY:
✔ 0 tests completed
✖ 1 test failed

FAILED TESTS:
  a11y input
    ✖ has by default an empty string as label
      HeadlessChrome 73.0.3683 (Windows 10.0.0)
    AssertionError: expected undefined to equal ''

      + expected - actual

      -[undefined]
      +""
Enter fullscreen mode Exit fullscreen mode

太棒了——果然不出所料(🥁),我们又一次测试失败了:)

让我们切换到监视模式,这样每当您对代码进行更改时,测试都会持续运行。

npm run test:watch

01-手表模式介绍

以下代码已添加到上面的视频中src/a11y-input.js

static get properties() {
  return {
    label: { type: String },
  };
}

constructor() {
  super();
  this.label = '';
}
Enter fullscreen mode Exit fullscreen mode

目前为止一切顺利?你还在吗?太好了!让我们再加把劲……

添加 Shadow DOM 测试

让我们添加一个断言来测试元素的影子根的内容。

为了确保元素的行为/外观保持一致,我们应该确保其 DOM 结构也保持不变。
所以,让我们将实际的 Shadow DOM 与我们期望的 Shadow DOM 进行比较。

it('has a static shadowDom', async () => {
  const el = /** @type {A11yInput} */ (await fixture(html`
    <a11y-input></a11y-input>
  `));
  expect(el.shadowRoot.innerHTML).to.equal(`
    <slot name="label"></slot>
    <slot name="input"></slot>
  `);
});
Enter fullscreen mode Exit fullscreen mode

正如预期的那样,我们得到:

✖ has a static shadowDom
AssertionError: expected '' to equal '\n      <slot name="label"></slot>\n      <slot name="input"></slot>\n    '

  + expected - actual

  +
  +      <slot name="label"></slot>
  +      <slot name="input"></slot>
  +
Enter fullscreen mode Exit fullscreen mode

所以让我们在我们的元素中实现它。

render() {
  return html`
    <slot name="label"></slot>
    <slot name="input"></slot>
  `;
}
Enter fullscreen mode Exit fullscreen mode

有意思,测试结果应该是绿色的……但不是🤔 让我们看看。

✖ has a static shadowDom
AssertionError: expected '<!---->\n      <slot name="label"></slot>\n      <slot name="input"></slot>\n    <!---->' to equal '\n        <slot name="label"></slot>\n        <slot name="input"></slot>\n    '

  + expected - actual

  -<!---->
  -      <slot name="label"></slot>
  -      <slot name="input"></slot>
  -    <!---->
  +
  +        <slot name="label"></slot>
  +        <slot name="input"></slot>
  +
Enter fullscreen mode Exit fullscreen mode

你可能已经注意到那些奇怪的空注释<!---->标签。它们是lit-html用来标记动态部分所在位置的,以便高效地进行更新。然而,在测试过程中,处理这些标签可能会有点麻烦。

如果我们使用innerHTML比较 DOM,就只能依赖简单的字符串相等性。在这种情况下,我们需要精确匹配生成的 DOM 中的空格、注释等等;换句话说,必须完全匹配。实际上,我们只需要测试想要渲染的元素是否被渲染即可。我们想要测试的是影子根的语义内容。

幸运的是,我们已经考虑到了这一点。如果您正在使用该插件@open-wc/testing,它会自动加载@open-wc/semantic-dom-diff供我们使用的 Chai 插件。

那我们来试试吧💪

// old:
expect(el.shadowRoot.innerHTML).to.equal(`...`);

// new:
expect(el).shadowDom.to.equal(`
  <slot name="label"></slot>
  <slot name="input"></slot>
`);
Enter fullscreen mode Exit fullscreen mode

砰🎉

a11y input
  ✔ has by default an empty string as a label
  ✔ has a static shadowDom
Enter fullscreen mode Exit fullscreen mode

shadowDom.to.equal() 的工作原理是什么?

  1. 它获取了innerHTML影子根
  2. 解析它(实际上,浏览器会解析它——不需要任何库)
  3. 将其规范化(可能将每个标签/属性放在单独的行上)
  4. 解析并规范化预期的 HTML 字符串
  5. 将两个规范化的 DOM 字符串传递给 chai 的默认比较函数。
  6. 如果失败,则分组,并以清晰的方式显示任何差异。

如果您想了解更多信息,请查看semantic-dom-diff的文档

测试“轻量级”DOM

我们可以用轻量级 DOM 做同样的事情。(轻量级 DOM 将由用户提供或我们的默认值提供,即元素的 DOM children)。

it('has 1 input and 1 label in light-dom', async () => {
  const el = /** @type {A11yInput} */ (await fixture(html`
    <a11y-input .label=${'foo'}></a11y-input>
  `));
  expect(el).lightDom.to.equal(`
    <label slot="label">foo</label>
    <input slot="input">
  `);
});
Enter fullscreen mode Exit fullscreen mode

让我们把它付诸实施。

connectedCallback() {
  super.connectedCallback();
  this.labelEl = document.createElement('label');
  this.labelEl.innerText = this.label;
  this.labelEl.setAttribute('slot', 'label');
  this.appendChild(this.labelEl);

  this.inputEl = document.createElement('input');
  this.inputEl.setAttribute('slot', 'input');
  this.appendChild(this.inputEl);
}
Enter fullscreen mode Exit fullscreen mode

我们已经测试了光影领域💪,测试结果一切正常🎉

注意:在 lit 元素的生命周期中使用 DOM API 是一种反模式,但为了实现无障碍访问(a11y),这可能是一个实际的用例——无论如何,它非常适合用于演示目的。

在应用程序中使用我们的元素

现在我们有了基本的 a11y 输入,让我们在我们的应用程序中使用它并进行测试。

我们再次从一个骨架开始。src/my-app.js

/* eslint-disable class-methods-use-this */
import { LitElement, html, css } from 'lit-element';

export class MyApp extends LitElement {}

customElements.define('my-app', MyApp);
Enter fullscreen mode Exit fullscreen mode

我们的测试在test/my-app.test.js

/* eslint-disable no-unused-expressions */
import { html, fixture, expect } from '@open-wc/testing';

import '../src/my-app.js';

/**
 * @typedef {import('../src/my-app.js').MyApp} MyApp
 */

describe('My Filter App', () => {
  it('has a heading and a search field', async () => {
    const el = /** @type {MyApp} */ (await fixture(html`
      <my-app .label=${'foo'}></my-app>
    `));
    expect(el).shadowDom.to.equal(`
      <h1>My Filter App</h1>
      <a11y-input></a11y-input>
    `);
  });
});
Enter fullscreen mode Exit fullscreen mode

运行测试 => 失败,然后我们添加实现。src/a11y-input.js

render() {
  return html`
    <h1>My Filter App</h1>
    <a11y-input></a11y-input>
  `;
}
Enter fullscreen mode Exit fullscreen mode

哦不!现在应该是绿色的了……

SUMMARY:
✔ 3 tests completed
✖ 1 test failed

FAILED TESTS:
  My Filter App
    ✖ has a heading and a search field
    AssertionError: expected '<h1>\n  My Filter App\n</h1>\n<a11y-input>\n  <label slot="label">\n  </label>\n  <input slot="input">\n</a11y-input>\n' to equal '<h1>\n  My Filter App\n</h1>\n<a11y-input>\n</a11y-input>\n'

      + expected - actual

       <h1>
         My Filter App
       </h1>
       <a11y-input>
      -  <label slot="label">
      -  </label>
      -  <input slot="input">
       </a11y-input>
Enter fullscreen mode Exit fullscreen mode

发生了什么事?
你还记得我们之前做过一个专门的测试来确保 a11y-input 的 light-dom 功能正常吗?
所以即使用户只是输入<a11y-input></a11y-input>代码——实际输出结果是……

<a11y-input>
  <label slot="label"></label>
  <input slot="input">
</a11y-input>
Enter fullscreen mode Exit fullscreen mode

例如,它a11y-input实际上是在你的 Shadow DOM 内部创建节点my-app。这太荒谬了!在我们的示例中,我们假设这就是我们想要的。
那么我们该如何测试它呢?

幸运的.shadowDom是,它还有另一张王牌;它允许我们忽略 dom 的某些部分。

expect(el).shadowDom.to.equal(`
  <h1>My Filter App</h1>
  <a11y-input></a11y-input>
`, { ignoreChildren: ['a11y-input'] });
Enter fullscreen mode Exit fullscreen mode

我们甚至可以指定以下属性:

  • ignoreChildren
  • ignoreTags
  • ignoreAttributes(全局或针对特定标签)

更多详情请参见semantic-dom-diff

快照测试

如果你有很多庞大的 DOM 树,手动编写和维护所有这些 expect 语句将会非常困难。
为了帮助你解决这个问题,可以使用半自动/自动快照功能。

所以如果我们修改代码

// from
expect(el).shadowDom.to.equal(`
  <slot name="label"></slot>
  <slot name="input"></slot>
`);

// to
expect(el).shadowDom.to.equalSnapshot();
Enter fullscreen mode Exit fullscreen mode

如果我们现在执行npm run test它,它将创建一个文件__snapshots__/a11y input.md并填充类似这样的内容。

# `a11y input`

#### `has a static shadowDom`

``html
<slot name="label">
</slot>
<slot name="input">
</slot>

``
Enter fullscreen mode Exit fullscreen mode

以前需要手动编写的内容现在可以在初始化时自动生成,或者通过强制执行npm run test:update-snapshots

如果文件__snapshots__/a11y input.md已存在,它会将文件与输出进行比较,如果您的 html 输出发生更改,您将会收到错误提示。

FAILED TESTS:
  a11y input
    ✖ has a static shadowDom
      HeadlessChrome 73.0.3683 (Windows 10.0.0)
    AssertionError: Received value does not match stored snapshot 0

      + expected - actual

      -<slot name="label-wrong">
      +<slot name="label">
       </slot>
       <slot name="input">
      -</slot>
      +</slot>
Enter fullscreen mode Exit fullscreen mode

更多详情请参见semantic-dom-diff

我觉得关于比较 DOM 树就到此为止吧……
是时候换个话题了🤗

代码覆盖率

在使用 open-wc 进行测试时,另一个有用的指标是代码覆盖率。
那么它是什么意思?我们又该如何获取它呢?代码覆盖率衡量的是测试覆盖了多少if代码。如果代码中存在测试未覆盖的行、语句、函数或分支(例如`/`else语句),则覆盖率得分会受到影响。我们只需要
一个简单的测试npm run test,即可获得以下结果:

=============================== Coverage summary ===============================
Statements   : 100% ( 15/15 )
Branches     : 100% ( 0/0 )
Functions    : 100% ( 5/5 )
Lines        : 100% ( 15/15 )
================================================================================
Enter fullscreen mode Exit fullscreen mode

这意味着我们代码中的所有语句、分支、函数和代码行都得到了测试覆盖。真棒!

那么我们反过来,src/a11y-input.js在添加测试之前先添加代码。假设我们想通过自定义元素直接访问输入框的值,并且当它的值为“cat”时,我们想记录一些内容。

get value() {
  return this.inputEl.value;
}

set value(newValue) {
  if (newValue === 'cat') {
    console.log('We like cats too :)');
  }
  this.inputEl.value = newValue;
}
Enter fullscreen mode Exit fullscreen mode

结果截然不同。

SUMMARY:
✔ 4 tests completed
TOTAL: 4 SUCCESS

=============================== Coverage summary ===============================
Statements   : 81.82% ( 18/22 )
Branches     : 0% ( 0/2 )
Functions    : 75% ( 6/8 )
Lines        : 81.82% ( 18/22 )
================================================================================
06 04 2019 10:40:45.380:ERROR [reporter.coverage-istanbul]: Coverage for statements (81.82%) does not meet global threshold (90%)
06 04 2019 10:40:45.381:ERROR [reporter.coverage-istanbul]: Coverage for lines (81.82%) does not meet global threshold (90%)
06 04 2019 10:40:45.381:ERROR [reporter.coverage-istanbul]: Coverage for branches (0%) does not meet global threshold (90%)
06 04 2019 10:40:45.381:ERROR [reporter.coverage-istanbul]: Coverage for functions (75%) does not meet global threshold (90%)
Enter fullscreen mode Exit fullscreen mode

我们的代码覆盖率比以前低得多。即使所有测试都成功运行,我们的测试命令甚至也失败了。
这是因为 open-wc 的默认配置将代码覆盖率阈值设置为 90%。

如果我们想提高代码覆盖率,就需要增加测试——那就开始吧!

it('can set/get the input value directly via the custom element', async () => {
  const el = /** @type {A11yInput} */ (await fixture(html`
    <a11y-input .value=${'foo'}></a11y-input>
  `));
  expect(el.value).to.equal('foo');
});
Enter fullscreen mode Exit fullscreen mode

糟糕😱 我们本来想提高覆盖率,但现在我们得先修复一个实际存在的bug😞

FAILED TESTS:
  a11y input
    ✖ can set/get the input value directly via the custom element
    TypeError: Cannot set property 'value' of null        at HTMLElement.set value [as value]
    // ... => long error stack
Enter fullscreen mode Exit fullscreen mode

这真是出乎意料……乍一看,我不太明白这意味着什么……最好检查一些实际节点,并在浏览器中进行检查。

在浏览器中进行调试

当我们使用 watch 运行测试时,karma 会设置一个持久的浏览器环境来运行测试。

你应该会看到类似这样的东西。
02-在浏览器中调试

您可以点击带圆圈的播放按钮,仅运行一次单独的测试。

现在让我们打开 Chrome 开发者工具 (F12),并在测试代码中插入调试器。

it('can set/get the input value directly via the custom element', async () => {
  const el = /** @type {A11yInput} */ (await fixture(html`
    <a11y-input .value=${'foo'}></a11y-input>
  `));
  debugger;
  expect(el.value).to.equal('foo');
});
Enter fullscreen mode Exit fullscreen mode

糟糕……错误甚至在此之前就发生了……
像这样的“致命”错误比较棘手,因为它们不是测试失败,而是整个组件的彻底崩溃。

好的,我们setter直接把一些代码放进去。

set value(newValue) {
  debugger;
Enter fullscreen mode Exit fullscreen mode

好了,成功了,我们在 Chrome 控制台中输入命令,console.log(this)看看这里有什么。

<a11y-input>
  #shadow-root (open)
</a11y-input>
Enter fullscreen mode Exit fullscreen mode

啊,找到了——调用 setter 方法时,Shadow DOM 还没有渲染完成。
所以为了保险起见,我们先添加一个检查。

set value(newValue) {
  if (newValue === 'cat') {
    console.log('We like cats too :)');
  }
  if (this.inputEl) {
    this.inputEl.value = newValue;
  }
}
Enter fullscreen mode Exit fullscreen mode

致命错误已解决🎉
但我们现在有一个失败的测试😭

✖ can set/get the input value directly via the custom element
AssertionError: expected '' to equal 'foo'
Enter fullscreen mode Exit fullscreen mode

我们可能需要改变策略🤔
我们可以将其添加为单独的value属性,并在需要时进行同步。

static get properties() {
  return {
    label: { type: String },
    value: { type: String },
  };
}

constructor() {
  super();
  this.label = '';
  this.value = '';
  // ...
}

update(changedProperties) {
  super.update(changedProperties);
  if (changedProperties.has('value')) {
    if (this.value === 'cat') {
      console.log('We like cats too :)');
    }
    this.inputEl.value = this.value;
  }
}
Enter fullscreen mode Exit fullscreen mode

我们终于恢复营业啦!🎉

好的,bug已修复——我们可以恢复正常覆盖了吗?谢谢🙏

返回报道

通过这项新增测试,我们取得了一些进展。

=============================== Coverage summary ===============================
Statements   : 95.83% ( 23/24 )
Branches     : 50% ( 2/4 )
Functions    : 100% ( 7/7 )
Lines        : 95.83% ( 23/24 )
================================================================================
06 04 2019 13:18:54.902:ERROR [reporter.coverage-istanbul]: Coverage for branches (50%) does not meet global threshold (90%)
Enter fullscreen mode Exit fullscreen mode

然而,我们仍未完全实现目标——问题是为什么?

要查看,请coverage/index.html在浏览器中打开该文件。无需网络服务器,只需在浏览器中打开该文件即可——在 Mac 上,您可以使用命令行执行此操作。open coverage/index.html

你会看到类似这样的内容。

03-覆盖范围概述

点击后,a11y-input.js您可以逐行查看代码执行次数的信息。
这样我们就能立即看到哪些代码行尚未被测试执行。

04-逐行覆盖

所以,我们来添加一个测试。

it('logs "We like cats too :)" if the value is "cat"', async () => {
  const el = /** @type {A11yInput} */ (await fixture(html`
    <a11y-input .value=${'cat'}></a11y-input>
  `));
  // somehow check that console.log was called
});
Enter fullscreen mode Exit fullscreen mode
=============================== Coverage summary ===============================
Statements   : 100% ( 24/24 )
Branches     : 75% ( 3/4 )
Functions    : 100% ( 7/7 )
Lines        : 100% ( 24/24 )
================================================================================
Enter fullscreen mode Exit fullscreen mode

这样一来,语句部分就恢复到100%了,但分支部分仍然缺少一些东西。
我们来看看为什么?

05-逐行覆盖-否则

E意味着else path not taken,无论何时调用
该函数,changedProperties 中总会有一个属性。updatevalue

我们label也有,所以测试一下很有必要。👍

it('can update its label', async () => {
  const el = /** @type {A11yInput} */ (await fixture('<a11y-input label="foo"></a11y-input>'));
  expect(el.label).to.equal('foo');
  el.label = 'bar';
  expect(el.label).to.equal('bar');
});
Enter fullscreen mode Exit fullscreen mode

太棒了!100% 💪 我们赢了🥇

=============================== Coverage summary ===============================
Statements   : 100% ( 24/24 )
Branches     : 100% ( 4/4 )
Functions    : 100% ( 7/7 )
Lines        : 100% ( 24/24 )
================================================================================
Enter fullscreen mode Exit fullscreen mode

等等,我们上面的测试还没完成——代码仍然

  // somehow check that console.log was called
Enter fullscreen mode Exit fullscreen mode

为什么我们的测试覆盖率能达到100%?

我们先来了解一下代码覆盖率的工作原理🤔
代码覆盖率的测量方法是应用一种特定的函数instrumentation。简而言之,在代码执行之前,它会被修改(instrumented),其行为大致如下:

注:此版本仅为极简化版本,仅用于说明目的。

if (this.value === 'cat') {
  console.log('We like cats too :)');
}

// becomes something like this (psoido code)
__instrumented['functionUpdate'] += 1;
if (this.value === 'cat') {
  __instrumented['functionUpdateBranch1yes'] += 1;
  console.log('We like cats too :)');
} else {
  __instrumented['functionUpdateBranch1no'] += 1;
}
Enter fullscreen mode Exit fullscreen mode

基本上,你的代码中会充斥着很多很多标志。根据哪些标志被触发,就会生成一个统计数据。

所以,100% 的测试覆盖率仅仅意味着在所有测试完成后,代码中的每一行都至少执行了一次。它并不意味着你测试了所有内容,也不意味着你的测试断言是正确的。

所以即使我们已经实现了 100% 的代码覆盖率,我们仍然会改进我们的日志测试。

因此,你应该把代码覆盖率看作是一个工具,它只能为你提供指导和帮助,以发现一些缺失的测试,而不是对代码质量的硬性保证。

监视代码

如果你想查看某个函数被调用的频率或调用参数,这叫做监视(spying)。open
-wc 推荐使用久负盛名的sinon包,它提供了许多用于监视和其他相关任务的工具。

npm i -D sinon
Enter fullscreen mode Exit fullscreen mode

所以你可以对特定对象创建一个间谍,然后检查它被调用的频率。

import sinon from 'sinon';

it('outputs "We like cats too :)" if the value is set to "cat"', async () => {
  const logSpy = sinon.spy(console, 'log');
  const el = /** @type {A11yInput} */ (await fixture(html`
    <a11y-input></a11y-input>
  `));

  el.value = 'cat';
  expect(logSpy.callCount).to.equal(1);
});
Enter fullscreen mode Exit fullscreen mode

糟糕……测试失败了:

AssertionError: expected 0 to equal 1
Enter fullscreen mode Exit fullscreen mode

像这样直接操作全局对象console可能会产生副作用,所以我们最好使用专门的日志函数进行重构。

update(changedProperties) {
  super.update(changedProperties);
  if (changedProperties.has('value')) {
    if (this.value === 'cat') {
      this.log('We like cats too :)');
    }
    this.inputEl.value = this.value;
  }
}

log(msg) {
  console.log(msg);
}
Enter fullscreen mode Exit fullscreen mode

这样一来,我们的测试代码中就没有全局对象了——太好了🤗

it('logs "We like cats too :)" if the value is set to "cat"', async () => {
  const el = /** @type {A11yInput} */ (await fixture(html`
    <a11y-input></a11y-input>
  `));
  const logSpy = sinon.spy(el, 'log');

  el.value = 'cat';
  expect(logSpy.callCount).to.equal(1);
});
Enter fullscreen mode Exit fullscreen mode

然而,我们仍然遇到同样的错误。让我们调试一下……boohoo 显然update没有同步——我之前做了个错误的假设🙈 我经常说假设很危险——但我还是会时不时地犯错😢。

那么我们该怎么办呢?遗憾的是,似乎没有公开的 API 可以执行由属性更新触发的同步操作。
让我们为此创建一个 issue:https://github.com/Polymer/lit-element/issues/643

目前看来,唯一的办法似乎是依赖私有API。🙈
另外,我们需要将值同步操作移到updated每次 DOM 渲染之后执行的位置。

_requestUpdate(name, oldValue) {
  super._requestUpdate(name, oldValue);
  if (name === 'value') {
    if (this.value === 'cat') {
      this.log('We like cats too :)');
    }
  }
}

updated(changedProperties) {
  super.updated(changedProperties);
  if (changedProperties.has('value')) {
    this.inputEl.value = this.value;
  }
}
Enter fullscreen mode Exit fullscreen mode

以下是更新后的日志记录测试。

it('logs "We like cats too :)" if the value is set to "cat"', async () => {
  const el = /** @type {A11yInput} */ (await fixture(html`
    <a11y-input></a11y-input>
  `));
  const logSpy = sinon.spy(el, 'log');

  el.value = 'cat';
  expect(logSpy.callCount).to.equal(1);
  expect(logSpy.calledWith('We like cats too :)')).to.be.true;

  // different values do NOT log
  el.value = 'foo';
  expect(logSpy.callCount).to.equal(1);

  el.value = 'cat';
  expect(logSpy.callCount).to.equal(2);
});
Enter fullscreen mode Exit fullscreen mode

哇,比预想的要难一些,但我们做到了💪

SUMMARY:
✔ 7 tests completed
TOTAL: 7 SUCCESS
Enter fullscreen mode Exit fullscreen mode

不使用 Karma 框架运行测试

Karma 框架功能强大且丰富,但有时我们可能希望简化测试流程。我们目前提出的方案的优点在于,除了裸模块说明符之外,我们只使用了浏览器标准的 ES 模块,无需转译。
因此,只需创建一个test/index.html……

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <link href="../node_modules/mocha/mocha.css" rel="stylesheet" />
  <script src="../node_modules/mocha/mocha.js"></script>
  <script src="../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
</head>
<body>
  <div id="mocha"></div>
  <script>
    mocha.setup('bdd');
  </script>

  <script type="module">
    import './a11y-input.test.js';
    import './my-app.test.js';

    mocha.checkLeaks();
    mocha.run();
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

在 Chrome浏览器中打开owc-dev-server,一切正常。
我们已经成功完成了所有设置并运行了webpack——karma太棒了🤗

实现跨浏览器兼容性

我们现在对我们的 Web 组件相当满意了。它已经过测试并涵盖了所有方面;只剩最后一步——我们要确保它在所有浏览器中都能运行并经过测试。

Open WC 推荐使用Browserstack进行跨浏览器测试。如果您还没有设置,现在就可以设置——链接再次奉上——https: //open-wc.org/testing/

那就运行一下吧。

npm run test:bs

SUMMARY:
✔ 42 tests completed
TOTAL: 42 SUCCESS
Enter fullscreen mode Exit fullscreen mode

是的,效果很好!🤗

如果测试失败,它会在摘要中输出失败的测试结果以及失败的具体浏览器。

SUMMARY:
✔ 40 tests completed
✖ 2 tests failed

FAILED TESTS:
  a11y input
    ✖ has a static shadowDom
      Firefox 64.0.0 (Windows 10.0.0)
      Safari 12.0.0 (Mac OS X 10.14.0)
    expected '<slot name="label">\n</slot>\n<slot name="input">\n</slot>\n<style>\n</style>\n' to equal '<slot name="label">\n</slot>\n<slot name="input">\n</slot>\n'

      + expected - actual

       <slot name="label">
       </slot>
       <slot name="input">
       </slot>
      -<style>
      -</style>
Enter fullscreen mode Exit fullscreen mode

如果您需要调试某个特定的浏览器:

  • npm run test:legacy:watch
  • 使用该浏览器(本地浏览器或通过 BrowserStack 访问)访问http://localhost:9876/debug.html
  • 选择特定测试(或it.only()在代码中使用)
  • 开始调试

另外,如果您想调整要测试的浏览器,您可以进行调整karma.bs.config.js

例如,如果您想将以下内容添加Firefox ESR到您的列表中。

module.exports = config => {
  config.set(
    merge(bsSettings(config), createBaseConfig(config), {
      browserStack: {
        project: 'testing-workflow-for-web-components',
      },
      browsers: [
        'bs_win10_firefox_ESR',
      ],
      // define browsers
      // https://www.browserstack.com/automate/capabilities
      customLaunchers: {
        bs_win10_firefox_ESR: {
          base: 'BrowserStack',
          browser: 'Firefox',
          browser_version: '60',
          os: 'Windows',
          os_version: '10',
        },
      },
    }),
  );

  return config;
};
Enter fullscreen mode Exit fullscreen mode

或者您可能只想测试两个特定的浏览器?

merge.strategy({
  browsers: 'replace',
})(bsSettings(config), createBaseConfig(config), {
  browserStack: {
    project: 'testing-workflow-for-web-components',
  },
  browsers: [
    'bs_win10_ie_11',
    'bs_win10_firefox_ESR',
  ],
}),
Enter fullscreen mode Exit fullscreen mode

注意:这使用了webpack 合并策略替换。

快速回顾

  • 测试对每个项目都至关重要。务必尽可能多地编写测试用例。
  • 尽量保持代码覆盖率高,但请记住,代码覆盖率并非万无一失,所以不必总是达到 100%。
  • 在浏览器中通过以下方式进行调试npm run test:watch。对于旧版浏览器,请使用npm run test:legacy.watch

接下来是什么?

  • 在 CI 系统中运行测试(与 BrowserStack 完美兼容)。请参阅我们的自动化建议。

请在Twitter上关注我们,或者关注我的个人Twitter账号
也请务必访问open-wc.org查看我们的其他工具和推荐。

感谢PascalBenny 的反馈,帮助我将涂鸦变成了一个可以理解的故事。

文章来源:https://dev.to/open-wc/testing-workflow-for-web-components-g73