Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

自动化测试 JavaScript #106

Open
lmk123 opened this issue Dec 2, 2021 · 0 comments
Open

自动化测试 JavaScript #106

lmk123 opened this issue Dec 2, 2021 · 0 comments
Labels

Comments

@lmk123
Copy link
Owner

lmk123 commented Dec 2, 2021

鉴于划词翻译的代码越来越庞大,我决定引入自动化测试。开发划词翻译 v6.x 版本时,我就是用 Karam 和 Jasmine 写测试的,但现如今我接触到的概念和工具还是看得我眼花缭乱:

我决定在这篇文章里厘清这些工具和概念之间的关系。

假设我们写了一个函数,用于将一个字符串转为数字:

const myIsNumber = (str) => Number(str)

即使不用任何工具,我们也可以通过代码来测试这个函数是否是符合预期的:

console.log(myIsNumber('0') === 0)
console.log(myIsNumber('1.1') === 1.1)
console.log(isNaN(myIsNumber('not a number')))

你可以把这段代码粘贴到浏览器的控制台运行,也可以把它保存成一个文件,用 Node.js 运行。运行之后,如果出现了 false 就说明有不符合我们预期的情况出现。

但是如果需要测试的函数变多了,那么靠人工手动运行测试代码显然是不现实的。于是这个时候,测试运行器出现了。

测试运行器

你可以把所有测试代码用同一个规则命名、或者放在同一个文件夹里,然后通过命令行执行测试运行器,测试运行器就会自动执行这些测试代码。

除此之外,测试运行期还有一些额外的功能,例如它运行完之后会输出一个报告,告诉你哪些测试失败了。一个使用了测试运行器的测试代码长下面这样:

// myIsNumber.test.js
import { myIsNumber } from './myIsNumber'

describe('测试 myIsNumber 传入正常的数字字符串是否符合预期', () => {
  test('传字符串 0 会转换成数字 0', () => {
    if (myIsNumber('0') !== 0) throw new Error('不正确')
  })

  test('传字符串 1.1 会转换成数字 1.1', () => {
    if (myIsNumber('1.1') !== 1.1) throw new Error('不正确')
  })
})

describe('测试 myIsNumber 不传入字符串是否符合预期', () => {
  test('传字符串 not a number 会转换成 NaN', () => {
    if (!isNaN(myIsNumber('not a number'))) throw new Error('不正确')
  })
})

如果有测试抛错了,那么测试运行器就会在终端里指出是哪些测试抛了错、错误信息是什么,帮助你定位问题。

Jasmine 就是一个测试运行器,但同时它自带了断言库;与它相比,Mocha 就是一个纯粹的测试运行器——它只负责运行测试,其它工具(例如断言库)都需要你自己引入。

断言库

在上面的例子中,我们是通过 throw new Error() 的方式来抛错的,但是这样不是很直观,这时,断言库就出现了。

一个用了断言(assert)的测试长这样:

test('传字符串 0 会转换成数字 0', () => {
  expect(myIsNumber('0')).toBe(0)
})

除了 .toBe(),断言库一般还带有其它判断方式,比如两个对象是否相等、一个函数是否抛了错、一个字符串是否满足正则表达式等,这样我们就可以专注于写测试,不需要我们自己来写判断逻辑了。

测试覆盖率

在目前的 myIsNumber() 函数中,如果传了空字符串,它会返回 0。假设在之后的版本中,我们希望传空字符串时能返回 NaN,那我们就需要加个 if

const myIsNumber = (str) => {
  if (str === '') return NaN
  return Number(str)
}

由于我们之前的测试代码没有测试过传空字符串的情况,所以之前的测试代码仍然会全部通过,但如果这时我们忘了给新加的代码补上测试,这就会造成一个隐患——新加的代码没有通过测试来保证它是能正常运行的。

为了确保所有代码都用测试覆盖到了,测试覆盖率工具就应运而生了。

测试覆盖率工具的原理其实就是给 JavaScript 代码注入计数函数。举个例子,myIsNumber 可能会被测试覆盖率工具改成这样:

const myIsNumber = (str) => {
  _line111()
  if (str === '') {
    _line222()
    return NaN
  }
  _line333()
  return Number(str)
}

这样一来,当我们重新运行测试之后,测试覆盖率工具就会检测到 _line222() 从来没有被执行过,测试运行器就会告诉你你的覆盖率从 100% 变成了 66%(3 条语句中执行了 2 条),这时你就要关注一下有哪些代码没有被测试了。

JavaScript 中的测试覆盖率工具基本都用的是 Istanbul。Istanbul 已经基本集成到了流行的测试运行器中,只需改一下配置就可以启用。

出个题:下面这段代码在开启了测试覆盖率工具 Istanbul 之后会报错 _gVxdceSfe is not a function,你知道原因了吗?

function code() {
  window.alert('hi')
}

const script = document.createElement('script')
script.textContent = '(' + code.toString() + ')()'
document.head.appendChild(script)

测试运行环境

测试代码写好了,接下来就要决定我们的代码应该在哪个环境运行了。测试的运行环境一般有两种:Node.js 和浏览器。

用于测试 JavaScript 的测试运行器本身就是用 Node.js 开发的,所以测试代码在 Node.js 里运行是最快的——但如果我们的代码用到了浏览器 API 呢?

这时候,社区出现了两种解决方案:

  1. 在 Node.js 里模拟浏览器 API,如 jsdom
  2. 通过 Node.js 操纵真实的浏览器来运行测试,如 SeleniumPuppeteer

第一点的好处是代码实际上是在 Node.js 里运行的,所以速度比第二点快,缺点是 jsdom 可能跟真实的浏览器有些许差异,也不能在多个不同版本的浏览器中测试,所以多用于单元测试。

第二点的好处就是代码是在真实的浏览器里运行的,可以做到在多个浏览器的多个版本中运行测试,覆盖面更广,但缺点也显而易见:跟直接在 Node.js 运行测试相比,它比较慢,运行一次测试会花费更多时间。这种方法多用于集成测试。

这里面我们引入了两个新的概念:单元测试集成测试。在介绍它们之前,我先简单解介绍一下 Selenium 和 Puppeteer。

这两个工具的功能都是为了通过代码来操纵浏览器,准确点说,它们并不是专门为了运行测试而设计出来的,比如你可以用它们来操纵浏览器并自动填写表单、或者写一个爬虫来爬网站等,但大多数人会用它们来做网页的集成测试。

Selenium 和 Puppeteer 最大的不同是:Selenium 支持几乎所有浏览器,并且支持多个编程语言,而 Puppeteer 只支持 Chrome / Chromium 浏览器,编程语言只支持 Node.js。

相关阅读 - Is Puppeteer replacing Selenium/WebDriver?

无头(Headless)浏览器指的是当你在操纵浏览器时,并不会真的弹出来一个浏览器页面,这对于运行测试或写爬虫来说很有用,因为在这个过程中你并不关心网页显示成什么样子,特别是当你的测试或爬虫是运行在服务器上的时候。

一开始,最流行的无头浏览器是 PhantomJS,但自从 Chrome 从 v59 开始支持无头模式之后,PhantomJS 就停止维护了。

同样的,测试运行器一般都集成了不同的测试环境。

单元测试与集成测试

在前面的例子中,我们对 myIsNumber() 做的测试就是单元测试——也就是说,我们会单独测试不同的代码(即"单元"),每个单元之间的测试是相互独立的。

有了单元测试的好处就是,当我对代码做了修改之后,我可以运行单元测试来确保这次改动不会破坏掉以前的功能。

但是,软件在实际运行的时候,所有代码都是集成在一起运行的,我们需要确保它们集合在一起也是正常运行的,这个时候就需要用到集成测试了。

集成测试其实很好理解,公司里的测试妹子 QA 在测试网站是否运行正常时,做的就是集成测试:他点了一个按钮,就应该显示一个结果,如果链路中的任意一个环节(前端代码、后端代码或者数据库)出了问题,显示出来的结果都是不正确的。而有了 Selenium 和 Puppeteer 这类工具之后,我们就可以编写代码来取代人工点击的测试方式了。

据我的理解,端到端测试似乎跟集成测试是同一个概念,如有有误欢迎指正。

主流测试运行器对比

虽然测试涉及到的工具和概念很多,但当我们需要写测试时,测试运行器才是我们首先需要选择的,其它工具基本都会被集成进测试运行器里。

就我个人所见,目前主流的三个测试运行器是 Mocha、Jest 与 Karam。这三个测试运行器基本上都对以上介绍的工具做了集成,但它们都有不同的侧重点。

Mocha

Mocha 主要用来做 Node.js 代码的单元测试,例如用 Express 写的后台;你也可以用它来做 API 的集成测试,例如通过代码调用注册用户的 API,然后检查数据库里有没有生成符合预期的数据。

Jest

Jest 主要用来做前端代码的单元测试(配合 jsdom),特别是用来测试 React 组件。Jest 的最大特点是 0 配置即可上手使用。注意,虽然 Jest 通过 jsdom 模拟了浏览器环境,但你要清楚你的代码是跑在 Node.js 里的。

另外,你也可以用 jest-poppeteer 做集成测试,例如,像划词翻译这样的扩展程序就可以用它来做集成测试。我目前正在尝试中,有结果的话会另写一篇文章来分享心得。

Karam

Karam 只能用来做单元测试。需要注意的是,它会把测试代码跑在真实的浏览器里,也就是说它不能测试 Node.js 代码。

注意:虽然 Jest 集成了 Poppeteer,但它的运行方式跟 Karam 是有本质上的不同的。

通过 Karam - How it works 可以得知,Karam 会把测试代码打包(类似于 Webpack 的形式)成一个在浏览器里可以运行的 js 文件并生成一个引用地址(例如 http://localhost:6742/my-test-bundle.js),然后操纵浏览器利用 <script> 引用这个 js,这段 js 会负责运行测试并将测试结果传回给 Karam。

Jest 虽然集成了 Poppeteer,但是我们的测试代码仍然是运行在 Node.js 里的,这也是为什么开启覆盖率选项之后,使用 Poppeteer 的 page.evaluate 会报错的原因,见 jestjs/jest#7962

举个例子,如果想要将前文里的 myIsNumber 的测试通过 Jest 运行在 Poppeteer 里,需要这么写:

import { myIsNumber } from './myIsNumber'

beforeAll(async () => {
  // 随便前往一个网址
  await page.goto('https://google.com');
});

test('将 myIsNumber 运行在 poppeteer 中', async () => {
  await expect(page.evaluate(`${myIsNumber.toString()}; return myIsNumber('1.1')`)).resolves.toBe(1.1);
});

如果我们开启了覆盖率选项会怎么样?我们在前面的题目里思考过这个问题 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant