没有API就是最好的API-断言库的优雅力量
原文链接:
Facebook
的
React
库背后的核心思想之一是,不需要为已经知道如何在原生
JavaScript
中实现的东西学习新的
API
。当你可以使用很老的
Array.map()
时,为什么还要记住
Angular
的
ng-repeat
语法呢? 这是一个好主意,而这正是使该项目首先吸引开发人员的重要原因。 所以为什么
Jest
(
Jest
是这家公司的
JavaScript
测试框架,也是一个
React
开发者的流行选择)鼓励你学习一个新的断言
API
,并当你已经知道如何在原生
JavaScript
中断言同一件事时,编写像如下的代码呢?
expect(result).toEqual(expect.not.stringContaining(unexpectedSubstring));
assert(!result.includes(unexpectedSubstring));
Jest API
所鼓励的那种断言模式似乎与
React
背后的哲学恰恰相反。
这肯定不是针对于
Jest
的抱怨(而且
Jest
也不仅仅是一个断言库)。它适用于
JavaScript
中大多数流行的测试断言库,例如
chai
,
must.js
和
should.js
。
JavaScript
世界中的测试往往主要面向具有详细和复杂断言
API
的
BDD
风格断言库。如果你喜欢这种风格,这没问题,但是当你已经知道以更简单和更简洁的方式执行相同断言的其他方法时,你需要记忆很多额外的语法。
不出所料,为什么这些断言库倾向于按照它们的方式工作有一些很好的理由。如果您只是使用
Node
的
内置断言模块
来进行断言,那么您的错误消息会为您提供有关正在测试内容的相当有限的上下文。无论字符串包含什么,我们之前的
“unexpected substring”
测试的原生
JavaScript
版本都将失败并显示相同的错误消息。
AssertionError [ERR_ASSERTION]: false == true
这告诉您测试失败了,但是查看
result
和
unexpectedSubstring
变量的值是什么是有用的,这样您就可以确定测试失败的原因。
作为对比,以下是
Jest
测试失败时的输出。
expect(received).toEqual(expected)
Expected value to equal:
StringNotContaining "World"
Received:
"Hello World"
这立马让我们知道字符串
“Hello World”
意外地包含了子字符串
“World”
。我们当然可以使用原生方法编写您自己的更多信息性断言错误消息,但这需要为每个测试进行额外的工作。 这个子串的例子并不算太糟糕,以下内容就足够了。
assert(
!result.includes(unexpectedSubstring),
`"${result}" unexpectedly included "${unexpectedSubstring}".`,
);
但是,这种方法无法很好地扩展到更复杂的测试,尤其是在处理数组和嵌套对象时。使用
Jest
,您可以获得开箱即用的有用且信息丰富的错误消息。
很长一段时间,我认为你必须在使用详细的断言
API
或编写自己的断言消息之间做出选择。嗯,事实证明并非如此!有一个名为
Power Assert
的库,它可以使用标准的
assert
模块,同时仍然可以获得信息量大的错误消息。这是两全其美的。
Power Assert
背后的基本前提是,不需要使用复杂的
API
来提供关于正在测试内容的断言库上下文;可以从代码本身推断出该信息。这当然意味着
Power Assert
需要能够访问代码本身,但是无论好坏,无论如何,
JavaScript
往往会在代码运行之前通过一两次转换。使用像
Babel
,
WebPack
和
Gulp
这样的工具是常态而不是例外,
Power Assert
为每个工具提供了插件,以提供更多信息性的断言消息。
回到之前的
“unexpected substring”
,
Power Assert
将转换断言,以便产生如下错误消息:
assert(!result.includes(unexpectedSubstring))
|| | |
|| true "World"
|"Hello World"
false
+ expected - actual
-false
+true
此消息允许我们在评估期间立即查看
result
和
unexpectedSubstring
的值以及每个中间值。这里运行的代码与我们之前运行的代码完全相同。唯一改变的是我在第二次运行测试之前启用了
Babel Power Assert preset
。如果您已经在使用
Node
断言模块,那么这就是开始获取更多有用的错误消息所需的全部内容。您不需要学习任何新的断言方法,只需使用现有的
assert API
即可。
事实上,
Power Assert
的口号是
“没有API就是最好的API。”
。这是一个良好的情绪,真正引起我作为开发人员的共鸣。与早期的
Web
框架相比,这是关于
React
我最喜欢的一部分,它也是我们的浏览器自动化框架
Remote Browser
背后的主要思想之一。我们开发了一个尽可能少的
API
的库,同时使开发人员可以非常轻松地使用原生
JavaScript
,
HTML浏览器上下文
和
Web Extensions API
来完成复杂的测试和
Web
抓取任务(您可以查看
Remote Browser互动之旅
,看看我所说的)。我们没有提出一个时髦的口号,但我觉得它几乎同样适用于
Remote Browser
和
Power Assert
。
这种共鸣激发了我与
Power Assert
库的主要作者
Takuto Wada
的联系。长话短说,我们最后对
Power Assert
这个项目进行了简短的采访。我们将在下一节中看看他将会说些什么,然后我们将有一个如何将
Power Assert
集成到
JavaScript
项目中的快速教程。一旦将它添加到示例项目中,我们将构建一些测试作为示例,以查看可以使用
Power Assert
生成断言的错误消息类型。
与
Takuto Wada
的迷你访谈
这些问题和回答来自
Takuto Wada
和我交换过的一系列电子邮件。
Takuto Wada
- 除了作为
Power Assert
的主要作者 - 在编程方面拥有超过20年的经验,在日本被称为
“TDD传播者”
,并将
Kent Beck
的
测试驱动开发
翻译成日语。让我们看看他对其他测试框架,
Power Assert
的起源以及项目背后的理念有何看法。
-
其他语言中也有一些类似的框架也使用
“Power Assert”
名称,但根据提交历史记录看起来,您的版本最先出现。是真的吗?如果是这样,你是如何决定用“Power Assert”
这个词来表达,你最初是如何得到Power Assert
的这个想法的?
不是我最先使用的。“Power Assert”
的起源是用groovy编写的 Spock框架 。当我第一次看到groovy的Power Assert
时,我非常惊讶深深被吸引。
2013年1月8日,我决定使用Mozilla SpiderMonkey Parser API
(作为ESTree Spec
的前身)将power-assert
实现为POC
。
2. 自从你第一次开始研究
Power Assert
以来,主要断言库的流行度已经发生了一些变化。一个值得注意的转变是,
Jest
已经从最初的不起眼变得像
Chai
一样受欢迎。你认为
Jest
是否提供了比
Chai
更好的断言接口,还是它们基本是相同的?
基本是相同的。Jest
是Jasmine
的一个分支,是一种测试框架的“BDD风格”
方言。Jest
比chai
更谦虚,但它们基本相同,大量的匹配器,大量的API
需要学习。
Kent Beck
曾经说过,“约束之间存在冲突。易于编写与易于学习编写。“Chai
,Jest
及其祖先RSpec
旨在”易于编写“。Power-assert
的目标是“易于学习编写”=简单。
3. 您认为是否有任何实用程序库与
Power Assert
结合得特别好?信息性错误消息是等式的一半,但是一些断言可能需要从头开始构造一些样板。像
Lodash
这样的库提供了广泛的方法,可以促进与对象和数组的复杂交互,并且可以用来以更简洁的方式编写一些测试。你会认为
Lodash
是
Power Assert
的一个很好的补充,还是只是将一个
API
的复杂性换成另一个?
简单地返回布尔值的函数或方法(例如lodash
)能够很好的与power-assert
一起使用。由于测试失败时信息量较少,因此往往可以避免使用布尔函数,但是通过power-assert
,您可以获得信息性的失败消息。简单胜利。
4.
Power Assert
背后的理念似乎非常简约,与
Keep It Simple Stupid(KISS)
一致。 我很好奇这是否是一种延续到你的其他开发工具的偏好。 您的开发硬件/软件配置是什么样的,您是否认为它面向极简主义工具?
我20年来一直是Unix
公民,所以我的开发堆栈包含小巧漂亮的FLOSS
工具。
- Macbook Pro
- 主要是Emacs,有时是WebStorm,Atom,VSC。
- zsh / nodebrew / ghq / peco
正如您所猜测的,我的产品背后的哲学是KISS
原则。 简单至关重要。 我的编程风格受到Unix
哲学以及创作Clojure
语言的Rich Hickey
的哲学的强烈影响。
5. 您是业余时间贡献
Power Assert
,还是工作时间?
业余时间。
6. 接下来我要解决我的困惑,我猜测你认为
BDD
断言风格中的可链接语义
API
没必要那么复杂。
是的。
如果是,那么你对一般的可链接语义
API
你有同感吗?还是有些情况下它是一个不错的选择?
我看到BDD
断言样式中的许多可链接语义API
逐渐变得复杂。它们“易于编写”但不“易于学习”。可链接的语义API
(a.k.a Fluent interface )有时效果很好,尤其是在编译器和IDE
帮助下的静态类型语言(例如用Java
编写的SQL Query Builder
)。
是否有一个您知道的库使用了一个可链接的
API
,您认为它可以很好地完成?
Rails
中的
“Scopes”
设计得很好并且运行良好。
7. 我写这篇文章是因为我是
Power Assert
的忠实粉丝,我认为可能有很多人会喜欢使用它,但还没有听说过它。 有哪些开源项目与您没有关联,但您认为更多人应该了解这些项目?
我想介绍
ghq
和
peco
。 他们让我的
OSS
生活非常愉快。
8. 如果我们引用
“没有API是最好的API”
并且在谈论
Remote Browser
时使用它是否可以:-)?
当然,可以:)
使用
Mocha
和
Babel
设置
Power Assert
首先我要说的是,有很多插件,
preset
,任务以及诸如此类将
Power Assert
与各种
JavaScript
开发工具集集成的东西。我将专注于将
Babel
与
Mocha
结合使用,因为这是我喜欢的用于自己测试的设置。如果您喜欢不同的测试设置,可以在
Power Assert主仓库
中找到与许多其他方案相关的说明。然而,我们将看到的实际错误消息和测试策略应该独立于工具,因此无论如何都可以看到
Power Assert
的运行情况。
我还要提到最终项目配置和我们将要编写的测试可以在
intoli-article-materials
仓库中找到。如果您尝试将此代码用作模板以将
Power Assert
添加到您自己的项目中,那么可以在那里查看最终的产品。记得给仓库
star
哦!我们在那里为我们的大多数文章提供了补充材料,并且
star
仓库是了解新文章和即将发表的文章的好方法。
现在,一切准备就绪,让我们开始项目。与
JavaScript
项目一样,您需要在一个干净的工作目录中创建一个
package.json
文件。将以下内容添加到
package.json
文件将指定在启用
Power Assert
的情况下运行测试所需的所有开发依赖项。
{
"scripts": {
"test": "NODE_ENV=testing mocha --exit --require babel-register"
"devDependencies": {
"babel": "^6.23.0",
"babel-core": "^6.26.3",
"babel-preset-env": "^1.7.0",
"babel-preset-power-assert": "^2.0.0",
"babel-register": "^6.26.0",
"mocha": "^5.2.0",
"power-assert": "^1.5.0"
}
然后运行
npm install
或
yarn install
将依赖项安装到本地
node_modules /
子目录中。以上指定的依赖项中只有两个关于
Power Assert
......就是名字含有
“power-assert”
的依赖项。一个是
power-assert
包,另一个是
babel-preset-power-assert
,这是
Babel preset
,它可以让
Power Assert
与
Babel
一起工作。其他依赖项涵盖了
Mocha
测试框架和一组最小的
Babel
包。
从
package.json
文件中要注意的另一件事是我们定义了一个测试脚本命令。这不是针对
Power Assert
的,但我们在这里做了两件很重要的事情:将
NODE_ENV
环境变量设置为
test
,并在运行
Mocha
时使用
--require babel-register
命令行参数。
--require
标志只是告诉
Mocha
在运行测试之前它应该需要一个特定的包。
在这种情况下,
--require
标志告诉
Mocha
在测试之前评估
require('babel-require')
。这似乎相当无害,但它对我们的测试代码的运行方式产生了深远的影响。
babel-register
包是
Babel
的一个特殊部分,在需要时,它将自己绑定到
Node
的
require()
方法,并导致
Babel
动态地编译任何进一步需要的包。
可以通过创建
.babelrc
文件来配置编译的行为。包含
Power Assert
支持的基本
.babelrc
配置可能如下所示
{
"env": {
"testing": {
"presets": [
"power-assert"
"presets": [["env", {
"targets": {
"node": "6.10"
}
实际上针对
Power Assert
的唯一部分是我们告诉
Babel
在
NODE_ENV
是
testing
时添加
Power Assert preset
(即当我们运行我们的
test
脚本时)。 此测试配置将与默认配置
合并
。 在这种情况下,默认配置仅指定应使用
Babel env preset
来动态确定哪些插件是
Node 6.10
版本所必需的。
添加我们的
.babelrc
文件后,所有内容都应该可以使用
Babel
和
Power Assert
工作。 现在只需创建一个名为
test
的子目录,即
Mocha
的默认搜索目录,并创建一个名为
test / test-assertion-errors.js
的
JavaScript
文件,其中包含以下内容
import assert from 'assert';
// Note that all of these tests are designed to fail, so that we can see the error messages!
describe('Power Assert Testing Examples', () => {
it('check that an unexpected substring is not found', () => {
const result = 'Hello World';
const unexpectedSubstring = 'World';
// Jest Equivalent: expect(result).toEqual(expect.not.stringContaining(unexpectedSubstring));
assert(!result.includes(unexpectedSubstring));
我们可以在这里使用
ECMAScript 2015 import 语法
,因为
Babel
正在编译我们的测试。 您添加到
.babelrc
文件的任何其他配置也将适用于您的测试,因此如果你喜欢,您可以使用有趣的东西,如
transform-object-rest-spread
和
transform-optional-changing
。 然而,唯一对
Power Assert
真正重要的部分是我们将
Power Assert preset
指定为配置的一部分。
您现在可以使用
yarn test
或
npm run test
运行测试,您将看到我们在简介中查看的相同信息性错误消息。
assert(!result.includes(unexpectedSubstring))
|| | |
|| true "World"
|"Hello World"
false
+ expected - actual
-false
+true
使用
assert
模块编写的任何其他测试也将包含类似有用的错误消息。 我们将在下一节中看一些实际的例子!
运行中的
Power Assert
现在我们已经完成了所有配置,现在让我们快速浏览几个常见的实际测试模式以及
Power Assert
在其中生成的断言消息。 我们将使用原生
JavaScript
和
Node assert
模块编写每个断言,但我们还将包括已注释掉的
Jest
断言以进行比较。 请注意,为简洁起见,仅显示各个测试定义。 每个测试都要放在
test / test-assertion-errors.js
中的
describe()
块中,因此如果你想自己运行测试(使用
yarn test
),你可以将它们复制并粘贴到那里。
例1:检查另一个数组中没有包含当前数组成员
在第一个测试中,我们将有一个名为
result
的数组,我们需要检查它是否包含第二个名为
unexpectedMembers
的数组中的任何元素。有几种不同的逻辑等效方法可以为此测试编写断言。你可以循环遍历
result
并断言每个成员都不是
unexpectedMembers
的一部分,你可以循环遍历
unexpectedMembers
并断言每个成员不是
result
的一部分,你可以计算被视为集合的两个数组之间的交集并断言它是空集,等等。我个人认为这些方法中的每一种都意味着略有不同的意图,并且在任何给定情况下最合适的选择取决于测试的基本背景和目的。
当我写这个测试时,我认为
result
是一个有意义的单词序列,而
unexpectedMembers
则是一组无关的单词集合。鉴于上下文,将整个断言分解为一系列单独的断言是有意义的,其中按顺序检查
result
中每个
unexpected member
。我们可以使用
Array.forEach()
循环遍历
unexpectedMembers
,然后使用
Array.include()
来检查
result
是否包含其中一个
unexpected member
。
it('check that no members of an array are included in another array', () => {
const result = ['Hello', 'World'];
const unexpectedMembers = ['Evan', 'World'];
// Jest Equivalent: expect(result).toEqual(expect.not.arrayContaining(unexpectedMembers));
unexpectedMembers.forEach(member =>
assert(!result.includes(member))
运行此测试将失败并生成以下错误消息
assert(!result.includes(member))
|| | |
|| true "World"
|["Hello","World"]
false
+ expected - actual
-false
+true
此消息向我们显示
result
的完整值,即导致测试失败的
unexpectedMembers
的某个成员,
result.includes(member)
求值为
true
的事实,以及我们明确地将该值否定为
false
。
检查正则表达式是否与字符串匹配
这个很简单:我们有一个名为
result
的字符串,我们想检查一个名为
regex
的正则表达式是否匹配它。
JavaScript
对正则表达式有很大的支持,我们可以使用
RegExp.test()
来确定正则表达式是否与
result
字符串匹配。
it('check that a regular expression matches a string', () => {
const regex = /^Hello World!/;
const result = 'Hello World';
// Jest Equivalent: expect(result).toEqual(expect.stringMatching(regex));
assert(regex.test(result));
由于正则表达式中存在感叹号,此测试将失败,并且生成的错误消息将如下所示
assert(regex.test(result))
| | |
| | "Hello World"
| false
/^Hello World!/
+ expected - actual
-false
+true
我们可以很容易地从中看到正则表达式的值是什么,
result
字符串的值,以及正则表达式检查失败。
检查数组是否包含至少一个数字
在此示例中,我们将要检查名为
result
的数组是否包含至少一个数字。
Array.some()
方法允许我们检查函数是否对数组中的任何元素求值为
true
。 我们可以将它与箭头函数结合使用,该函数检查每个成员的类型以构建此测试。
it('check that an array contains at least one number', () => {
const result = ['Hello', 'World'];
// Jest Equivalent: expect(result).toContainEqual(expect.any(Number));
assert(result.some(member => typeof member === 'number'));
当然,这个测试也会失败,它的输出看起来像这样
assert(result.some(member => typeof member === 'number'))
| |
| false
["Hello","World"]
+ expected - actual
-false
+true
这让我们清楚地看到我们如何定义检查每个成员的箭头函数,
result
数组的值,以及箭头函数对所有成员求值为
false
的事实。
检查两个对象之间的深度相等
到目前为止,我们一直在进行空
assert()
调用,但我应该指出,
assert
模块确实有一个超出此范围的
API
。这包括用于检查深度相等性的
assert.deepStrictEqual()
/
assert.notDeepStrictEqual()
方法,用于检查是否抛出错误的
assert.throws()
,以及用于检查
promise
被拒绝的
assert.rejects()
。由于适用于第三方断言库,当使用
Power Assert
时,大部分
API
都变得多余。例如,使用
Power Assert
对
assert(a === b)
进行
assert.strictEqual(a, b)
调用几乎没有什么好处,类似的语句适用于其余的大部分
API
。
我认为深度相等比较是一个值得注意的例外。虽然很有可能使用递归手动检查,但这是我认为这样编码没有意义的一种情况。到目前为止我们看过的其他示例实际上并没有必要牺牲代码的简洁性或意图的清晰度来使用空
assert()
调用,但是我们需要牺牲这两个来手动检查深度相等。所以我们将使用
assert.deepStrictEqual()
代替,并且断言的错误消息仍然会被
Power Assert
转换。
it('check for deep equality between two objects', () => {
const expectedResult = { 'a': [1, 2], 'b': [1, 2] }
const result = { 'a': [1, 2], 'b': [1, 2, 3] }
// Jest Equivalent: expect(result).toEqual(expectedResult);
assert.deepStrictEqual(result, expectedResult);
运行此测试将输出以下内容
assert.deepStrictEqual(result, expectedResult)
| |
| Object{a:#Array#,b:#Array#}
Object{a:#Array#,b:#Array#}
+ expected - actual
"b": [
- 3
}
这告诉我们
result
和
expectedResult
都是包含名为
a
和
b
属性的数组对象。 我们还可以知道
result.b
数组包含第三个元素,其值为3,不应该出现。
这里的错误消息非常有用,但值得注意的是,您可以自定义
Power Assert
的行为以显示每个数组的实际值,而不是
#Array#
。 有一个
maxDepth
选项,用于确定在用通用标识符替换对象之前可以有多少级别的嵌套。 自定义行为需要在我们的测试中添加一些特定于
Power-Assert
的代码,但是当
Power Assert
没有像这样初始化时,我们可以优雅地回退到标准的
assert
模块。
import uncustomizedAssert from 'assert';
const assert = !uncustomizedAssert.customize ? uncustomizedAssert : (
uncustomizedAssert.customize({
output: {
maxDepth: 5,
这种自定义允许我们查看每个对象内部数组的实际值。
assert.deepEqual(result, expectedResult)
| |
| Object{a:[1,2],b:[1,2]}
Object{a:[1,2],b:[1,2,3]}
+ expected - actual