常见问题
- 总体概况
- 与 Jasmine 兼容的其他软件
- 编写规范
-
异步测试
- 我应该使用哪种异步样式,为什么?
- 为什么某些异步规范故障会报告为套件故障或其他规范的故障?
- 如何阻止 Jasmine 并行运行我的规范?
- 为什么我无法编写既包含一个回调又返回 Promise(或是一个异步函数)的规范?我应当如何解决?
- 但我真的必须测试通过不同渠道发出成功和故障信号的代码。我无法(或者不想)更改它。我应当如何解决?
- 为什么我的异步函数不能多次调用 `done`?我应当如何解决?
- 为什么我无法将异步函数传递给 `describe`?如何利用异步加载的数据生成规范?
- 如何测试我不具备 Promise 或回调的异步行为,例如在异步获取数据后呈现某些内容的 UI 组件?
- 我需要对在正在测试的代码完成之前发生的对异步回调传递的参数进行断言。最佳做法是什么?
- 为什么当规范因拒绝 Promise 而失败时,Jasmine 并不总是显示堆栈跟踪?
- 我收到一个未处理的 Promise 拒绝错误,但我认为这是一个误报。
- 间谍
- 对 Jasmine 做出贡献
总体概况
Jasmine 的下一个版本何时发布?
这取决于贡献的速度和维护人员时间的可用性。
Jasmine 完全是一个志愿者的努力,这使得很难预测何时发布新版本,也不可能承诺一个时间表。过去,包含新功能的版本通常每 1-6 个月发布一次。当发现新的错误时,我们会尝试尽快发布一个修复。
Jasmine 如何进行版本控制?
Jasmine 尽可能尝试遵循语义版本控制。这意味着我们会保留主要版本(1.0、2.0 等)用于破坏性更改或其他重要工作。大多数 Jasmine 版本最终都是次要版本(2.3、2.4 等)。主要版本很少见。
许多人通过jasmine
软件包(在 Node 中运行规范)或jasmine-browser-runner
软件包使用 Jasmine。出于历史原因,这些软件包有不同的版本控制策略
jasmine
主要版本和次要版本与jasmine-core
相匹配,这样当你更新jasmine
依赖项时,你也会获得最新的jasmine-core
。修补版本单独处理:jasmine-core
的修补版本不需要相应地修补jasmine
版本,反之亦然。jasmine-browser-runner
版本号与jasmine-core
版本号无关。它将jasmine-core
声明为对等依赖项。yarn
和npm
会自动为你安装一个兼容的jasmine-core
版本,或者你可以通过将其添加为软件包的直接依赖项来指定版本。
Jasmine 通常不会停止对浏览器或 Node 版本的支持,除非版本有重大变动。此规则的例外情况包括:生命周期已结束的 Node 版本、我们无法再在本地安装及/或在其 CI 构建环境中进行测试的浏览器、不再接受安全更新的浏览器以及仅在不再接收安全更新的操作系统上运行的浏览器。我们会尽合理努力确保 Jasmine 在这些环境中正常工作,但如果这些环境出现故障,不一定能提供重大版本更新。
如何在 jasmine-browser-runner 中使用外部 URL 中的脚本?
您可以将脚本的 URL 添加到 jasmine-browser.json
或 jasmine-browser.js
文件中的 srcFiles
// ...
srcFiles: [
"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js",
"**/*.js"
],
// ...
Jasmine 可以测试 ES 模块中的代码吗?
可以。具体流程取决于您使用 Jasmine 的方式
- 如果您正在使用独立发行版或任何其他您可以控制 HTML 标记的浏览器内设置,请使用
<script type="module">
。 - 如果您正在使用 jasmine NPM 包,那么您的脚本将使用 dynamic import 进行加载。这意味着如果文件包含在
package.json
中 (其中"type": "module"
) 或其名称以.mjs
结尾,则这些文件将被视为 ES 模块。 - 如果其名称以
.mjs
结尾,jasmine-browser-runner 将加载脚本,并将它们视为 ES 模块。您可以通过esmFilenameExtension
配置属性对此进行覆盖。 - 如果您正在使用 Karma 等第三方工具运行 Jasmine,请查阅该工具的文档。
为什么 Jasmine 允许规范中出现多个期望故障?如何禁用该功能?
有时需要多个预期来断言特定结果。在这种情况下,您可能希望在尝试让其中任何一个预期通过之前,先查看所有预期是否失败。当单一代码更改可能导致多个预期通过时,这会特别有用。
如果您希望每个规范在第一个预期失败时停止,则可以将 oneFailurePerSpec
选项设置为 true
- 如果您正在使用独立发行版,请依次单击“选项”和“预期失败时停止规范”,或编辑
boot.js
以永久设置此选项。 - 如果您正在使用
jasmine
NPM 包,请在您的配置文件中将stopSpecOnExpectationFailure
设置为true
(通常是spec/support/jasmine.json
)。 - 如果您正在使用封装 jasmine-core 的第三方工具,请查阅该工具的文档,了解如何传递配置选项。
- 如果您正在直接使用 jasmine-core,请将其添加到您传递给 Env#configure 的对象中。
请注意,与该规范关联的任何 afterEach 或 afterAll 函数仍可运行。
如何让 Jasmine 使不包含任何断言的规范失败?
默认情况下,Jasmine 不需要规范包含任何期望。你可以通过将 failSpecWithNoExpectations
选项设为 true
来启用该行为。
- 如果你在使用独立发行版,则请在
lib/jasmine-<VERSION>/boot.js
中将它添加到config
对象。 - 如果你在使用
jasmine
NPM 包,则请将它添加到你的配置文件(通常为spec/support/jasmine.json
)。 - 如果您正在使用封装 jasmine-core 的第三方工具,请查阅该工具的文档,了解如何传递配置选项。
- 如果您正在直接使用 jasmine-core,请将其添加到您传递给 Env#configure 的对象中。
我们不建议依赖 failSpecWithNoExpectations
选项。它确保的只是每个规范都至少有一个期望,而不是规范将在出现预期行为错误时由于正确的理由真正失败。确保规范实际上是正确的唯一方法是同时尝试这两种方式,并了解在被测代码工作时通过并好在被测代码中断时按预期失败。
如何在 TypeScript 项目中使用 Jasmine?
有两种常见的方法来同时使用 Jasmine 和 TypeScript。
第一种是使用 @babel/register
,以随文件导入即时编译 TypeScript 文件到 JavaScript。请参见 使用 Jasmine NPM 测试 React 应用,了解一个示例。这种方法易于设置,并提供尽可能最快的编辑编译运行规范的循环,但默认情况下不提供类型检查。你可以通过为你的规范创建一个单独的 TypeScript 配置文件来添加类型检查,将 noEmit
设为 true
,并在此之前或之后运行 tsc
。
第二种方法是,将你的 TypeScript 规范文件编译到磁盘中的 JavaScript 文件,然后配置 Jasmine 运行编译的 TypeScript 文件。这通常会带来较慢的编辑编译运行规范的循环,但对于习惯了编译语言的人而言,这是一个更为熟悉的工作流。如果你想用 TypeScript 编写规范并以浏览器运行规范,那么这也是唯一的选择。
与 Jasmine 兼容的其他软件
我是否可以使用 Jasmine 5.x 与 Karma?
可能是的。karma-jasmine 5.1(截至编写本文时的最新版本,也是可能的最终版本)似乎与 jasmine-core 5.x 兼容。你应该可以在 package.json
中使用 NPM 覆盖来覆盖 karma-jasmine 的依赖项规范
{
// ...
"overrides": {
"karma-jasmine": {
"jasmine-core": "^5.0.0"
}
}
}
为什么 Karma 中没有更新的 Jasmine 功能?
你可能在使用比你认为的更旧的 jasmine-core 版本。karma-jasmine 声明了对 jasmine-core 4.x 的依赖。因此,即使你安装了一个更新的版本,Karma 也会使用 jasmine-core 4.x。你可能可以通过添加 NPM 覆盖来解决这个问题,如 前一个问题 中所述。
我遇到有关 zone.js 的问题。您能提供帮助吗?
请将所有与 zone.js 相关的事件报告给 Angular 项目。
Zone.js 会大量修改 Jasmine,用自己的实现替换了许多关键内部函数。大多数情况下都能正常工作。但它造成的任何问题,根据定义,都是 zone.js 中的错误,而非 Jasmine 中的错误。
如何将 Jasmine 匹配器与 testing-library 的 waitFor 函数一起使用?
使用 throwUnless
而不是 expect
await waitFor(function() {
throwUnless(myDialogElement).toHaveClass('open');
});
为什么 expect() 在 webdriver.io 中无法正常工作?
@wdio/jasmine-framework
用另一个不兼容 Jasmine 的 expect
替换了 Jasmine 的 expect
。请参阅 Webdriver.IO 文档 了解其 expect
API 的信息。
除了替换 expect
之外,Webdriver.IO 修改了一些 Jasmine 内部函数。仅当 Webdriver.IO 存在时发生的错误应报告给 Webdriver.IO,而不是 Jasmine。
编写规范
我应当将普通函数还是箭头函数传递给 describe
、it
、beforeEach
等?
对于 describe
,这没关系。对于 it
、beforeEach
和 afterEach
,你可能更喜欢使用常规函数。Jasmine 会创建一个 用户上下文,并将其作为 this
传递给每个 it
、beforeEach
和 afterEach
函数。这允许你在这些函数之间轻松传递变量,并确保它们在每个规范后被清除。但是,这不适用于箭头函数,因为箭头函数中的 this
在词法上是绑定的。因此,如果你想要使用用户上下文,你必须坚持使用常规函数。
如何在包含 describe
的 beforeEach
之前运行代码?Jasmine 是否有相当于 rspec 的 let
的一项功能?
简短的答案是,你不能,你应该重构你的测试设置,以便内部 describe
不需要撤消或覆盖由外部 describe
完成的设置。
当人们尝试编写如下所示的套件时,通常会出现此问题
// DOES NOT WORK
describe('When the user is logged in', function() {
let user = MyFixtures.anyUser
beforeEach(function() {
// Do something, potentially complicated, that causes the system to run
// with `user` logged in.
});
it('does some things that apply to any user', function() {
// ...
});
describe('as an admin', function() {
beforeEach(function() {
user = MyFixtures.adminUser;
});
it('shows the admin controls', function() {
// ...
});
});
describe('as a non-admin', function() {
beforeEach(function() {
user = MyFixtures.nonAdminUser;
});
it('does not show the admin controls', function() {
// ...
});
});
});
这行不通,部分原因是内部 beforeEach
函式在用户已登录后才会执行。一些测试框架提供了一种重新对测试设置进行排序的方式,这样,内部 describe
中的部分设置就可以在外部 describe
中的部分设置之前执行。RSpec 的 let
块就是这种方式的示例。Jasmine 不提供这种功能。我们通过经验了解到,让设置流程控制在内部和外部 describe
之间反弹,会导致难以理解和修改套件。相反,尝试重构设置代码,以便每一部分都发生在其所依赖的所有设置之后。通常,这意味着取外部 beforeEach
的内容,并将其内联到内部规范或 beforeEach
中。如果这会导致过多的代码重复,那么可以使用常规函数进行处理,就像在非测试代码中一样
describe('When the user is logged in', function() {
it('does some things that apply to any user', function() {
logIn(MyFixtures.anyUser);
// ...
});
describe('as an admin', function() {
beforeEach(function() {
logIn(MyFixtures.adminUser);
});
it('shows the admin controls', function() {
// ...
});
});
describe('as a non-admin', function() {
beforeEach(function() {
logIn(MyFixtures.nonAdminUser);
});
it('does not show the admin controls', function() {
// ...
});
});
function logIn(user) {
// Do something, potentially complicated, that causes the system to run
// with `user` logged in.
}
});
为什么 Jasmine 显示不带堆栈跟踪的异常?
JavaScript 允许你用任何值抛出或拒绝包含任何值的承诺。但是,只有 Error
对象具有堆栈轨迹。因此,如果抛出一个非 Error
对象或用一个非 Error
的内容拒绝一个承诺,Jasmine 就无法显示 堆栈轨迹,因为没有可显示的堆栈轨迹。
这种行为受 JavaScript 运行时控制,而 Jasmine 无法更改。
// NOT RECOMMENDED
describe('Failures that will not have stack traces', function() {
it('throws a non-Error', function() {
throw 'nope';
});
it('rejects with a non-Error', function() {
return Promise.reject('nope');
});
});
// RECOMMENDED
describe('Failures that will have stack traces', function() {
it('throws an Error', function() {
throw new Error('nope');
});
it('rejects with an Error', function() {
return Promise.reject(new Error('nope'));
});
});
Jasmine 是否支持参数化测试?
不完全是。但是,测试套件仅仅是 JavaScript,所以你无论如何都可以做到。
function add(a, b) {
return a + b;
}
describe('add', function() {
const cases = [
{first: 3, second: 3, sum: 6},
{first: 10, second: 4, sum: 14},
{first: 7, second: 1, sum: 8}
];
for (const {first, second, sum} of cases) {
it(`returns ${sum} for ${first} and ${second}`, function () {
expect(add(first, second)).toEqual(sum);
});
}
});
如何向匹配器故障消息中添加更多信息?
当规范具有多个相似的预期时,可能很难判断哪个失败对应于哪个预期
it('has multiple expectations', function() {
expect(munge()).toEqual(1);
expect(spindle()).toEqual(2);
expect(frobnicate()).toEqual(3);
});
Failures:
1) has multiple expectations
Message:
Expected 0 to equal 1.
Stack:
Error: Expected 0 to equal 1.
at <Jasmine>
at UserContext.<anonymous> (withContextSpec.js:2:19)
at <Jasmine>
Message:
Expected 0 to equal 2.
Stack:
Error: Expected 0 to equal 2.
at <Jasmine>
at UserContext.<anonymous> (withContextSpec.js:3:21)
at <Jasmine>
有三种方法可以使类似规范的输出更加清晰
- 将每个期望放在其自己的规范中。(这有时是个好主意,但不是一直如此。)
- 撰写 自定义匹配器。(这有时是值得的,但不是一直如此。)
- 使用 withContext 向匹配器失败消息添加额外文本。
下面是与上面相同的规范,但已修改为使用 withContext
it('has multiple expectations with some context', function() {
expect(munge()).withContext('munge').toEqual(1);
expect(spindle()).withContext('spindle').toEqual(2);
expect(frobnicate()).withContext('frobnicate').toEqual(3);
});
Failures:
1) has multiple expectations with some context
Message:
munge: Expected 0 to equal 1.
Stack:
Error: munge: Expected 0 to equal 1.
at <Jasmine>
at UserContext.<anonymous> (withContextSpec.js:8:40)
at <Jasmine>
Message:
spindle: Expected 0 to equal 2.
Stack:
Error: spindle: Expected 0 to equal 2.
at <Jasmine>
at UserContext.<anonymous> (withContextSpec.js:9:44)
at <Jasmine>
异步测试
我应该使用哪种异步样式,为什么?
应该优先选择 async
/await
样式。大多数开发人员使用该样式编写无错误规范会容易得多。返回承诺的规范稍难编写,但在更复杂的场景中可能有用。回调样式规范非常容易出错,应尽可能避免。
回调式规范有两个主要缺点。第一点是执行流很难可视化。这使得很容易写出一个在实际完成之前调用 done
回调的规范。第二点是很难正确地处理错误。考虑此规范
it('sometimes fails to finish', function(done) {
doSomethingAsync(function(result) {
expect(result.things.length).toEqual(2);
done();
});
});
如果 result.things
尚未定义,则访问 result.things.length
会引发错误,从而阻止调用 done
。该规范最终将会超时,但仅在经过一段显著的延迟之后。将会报告错误。但由于浏览器和 Node 公布有关未处理异常的信息的方式,它将不包括堆栈跟踪或任何指示错误源的其他信息。
要解决此问题,需要用 try-catch 包装每个回调
it('finishes and reports errors reliably', function(done) {
doSomethingAsync(function(result) {
try {
expect(result.things.length).toEqual(2);
} catch (err) {
done.fail(err);
return;
}
done();
});
});
这样做很繁琐,容易出错,而且很容易忘记。通常最好将该回调转换成 promise
it('finishes and reports errors reliably', async function() {
const result = await new Promise(function(resolve, reject) {
// If an exception is thrown from here, it will be caught by the Promise
// constructor and turned into a rejection, which will fail the spec.
doSomethingAsync(resolve);
});
expect(result.things.length).toEqual(2);
});
回调式规范在某些情况下仍然很有用。一些基于回调的界面很难转换为 promise,或从转换为 promise 中没有得到多少好处。但在大多数情况下,使用 async
/await
或至少 promise 来编写更可靠的规范会更容易。
为什么某些异步规范故障会报告为套件故障或其他规范的故障?
当异步代码引发异常或出现未处理的 promise 拒绝时,导致此情况的规范将不再位于调用堆栈中。因此,Jasmine 没有可靠的方式确定错误来自何处。Jasmine 能做的最好的事情是将错误与在错误发生时正在运行的规范或套件关联起来。这通常是正确的答案,因为编写正确的规范不会在发出完成信号后触发错误(或执行其他任何操作)。
当规范在实际完成之前发出完成信号时,它就会成为一个问题。考虑这两个示例,它们都测试了一个在完成时调用回调的 doSomethingAsync
函数
// WARNING: does not work correctly
it('tries to be both sync and async', function() {
// 1. doSomethingAsync() is called
doSomethingAsync(function() {
// 3. The callback is called
doSomethingThatMightThrow();
});
// 2. Spec returns, which tells Jasmine that it's done
});
// WARNING: does not work correctly
it('is async but signals completion too early', function(done) {
// 1. doSomethingAsync() is called
doSomethingAsync(function() {
// 3. The callback is called
doSomethingThatThrows();
});
// 2. Spec calls done(), which tells Jasmine that it's done
done();
});
在这两种情况下,规范都会发出完成信号,但仍会继续执行,后来会引发错误。在发生错误时,Jasmine 已经报告了该规范通过,并开始执行下一个规范。在错误发生之前,Jasmine 甚至可能已退出。如果发生这种情况,根本不会报告该错误。
解决办法是确保在规范实际完成之前,不要发出完成信号。这可以使用回调来完成
it('signals completion at the right time', function(done) {
// 1. doSomethingAsync() is called
doSomethingAsync(function() {
// 2. The callback is called
doSomethingThatThrows();
// 3. If we get this far without an error being thrown, the spec calls
// done(), which tells Jasmine that it's done
done();
});
});
但使用 async
/await
或 promise 来编写可靠的异步规范更容易,因此我们建议在大多数情况下这样操作
it('signals completion at the right time', async function() {
await doSomethingAsync();
doSomethingThatThrows();
});
如何阻止 Jasmine 并行运行我的规范?
只有当您使用至少 5.0 版 jasmine
NPM 软件包并传递 --parallel
命令行参数时,Jasmine 才并行运行规范。在所有其他配置中,它一次运行一个规范(或 before/after)函数。即使是并行配置也会按顺序运行每个套件内的规范和 before/after 函数。
但是,Jasmine 依赖这些用户提供的函数来指示何时完成。如果一个函数在实际完成之前发出完成信号,那么下一个规范的执行将与之交错。为了解决此问题,请确保每个异步函数仅在其完全完成后调用其回调或解析或拒绝返回的 Promise。有关详细信息,请参见 async 教程。
为什么我无法编写既包含一个回调又返回 Promise(或是一个异步函数)的规范?我应当如何解决?
Jasmine 需要知道每个异步规范何时完成,以便它可以在适当的时候继续下一个规范。如果某个规范使用 done
回调,则意味着“当我调用回调时,我就完成了”。如果一个规范返回一个 Promise,无论是显式地还是通过使用 async
关键字,则意味着“返回的 Promise 被解析或拒绝时,我就完成了”。这两件事不可能同时为真,并且 Jasmine 无法解决歧义。未来的读者也可能难以理解规范的意图。
通常,提出这个问题的人会遇到两种情况之一。他们要么仅使用 async
来能够 await
而不向 Jasmine 发出完成信号,要么试图测试混合了多种异步样式的代码。
第一种情况:当一个规范是 async
只是为了能够 await
// WARNING: does not work correctly
it('does something', async function(done) {
const something = await doSomethingAsync();
doSomethingElseAsync(something, function(result) {
expect(result).toBe(/*...*/);
done();
});
});
在这种情况下,意图是当回调被调用时规范就完成了,而从规范隐式返回的 Promise 是没有意义的。最好的解决方法是更改基于回调的函数,使其返回一个 Promise,然后 await
Promise
it('does something', async function(/* Note: no done param */) {
const something = await doSomethingAsync();
const result = await new Promise(function(resolve, reject) {
doSomethingElseAsync(something, function(r) {
resolve(r);
});
});
expect(result).toBe(/*...*/);
});
如果您想坚持使用回调,则可以将 async
函数包装在 IIFE 中
it('does something', function(done) {
(async function () {
const something = await doSomethingAsync();
doSomethingElseAsync(something, function(result) {
expect(result).toBe(/*...*/);
done();
});
})();
});
或用 then
替换 await
it('does something', function(done) {
doSomethingAsync().then(function(something) {
doSomethingElseAsync(something, function(result) {
expect(result).toBe(170);
done();
});
});
});
第二种情况:以多种方式发出完成信号的代码
// in DataLoader.js
class DataLoader {
constructor(fetch) {
// ...
}
subscribe(subscriber) {
// ...
}
async load() {
// ...
}
}
// in DataLoaderSpec.js
// WARNING: does not work correctly
it('provides the fetched data to observers', async function(done) {
const fetch = function() {
return Promise.resolve(/*...*/);
};
const subscriber = function(result) {
expect(result).toEqual(/*...*/);
done();
};
const subject = new DataLoader(fetch);
subject.subscribe(subscriber);
await subject.load(/*...*/);
});
与第一个场景类似,本规范的问题在于,它以两种不同方式发出完成信号:通过确定(解决或拒绝)隐式返回的承诺,以及通过调用done
回调。这反映了DataLoader
类的潜在设计问题。通常人们编写这样的规范,因为无法依靠正在测试的代码以一致的方式发出完成信号。调用订阅者及确定返回的承诺的顺序可能难以预料。更糟糕的是,DataLoader
可能只会使用返回的承诺来发出失败信号,让它在成功情况下挂起。对有这种问题的代码编写可靠的规范比较困难。
解决方法是更改正在测试的代码,使其始终以一致的方式发出完成信号。在此情况下,意味着确保DataLoader
在成功和失败情况下执行的最后操作是解决或拒绝返回的承诺。之后可以像这样对其进行可靠测试
it('provides the fetched data to observers', async function(/* Note: no done param */) {
const fetch = function() {
return Promise.resolve(/*...*/);
};
const subscriber = jasmine.createSpy('subscriber');
const subject = new DataLoader(fetch);
subject.subscribe(subscriber);
// Await the returned promise. This will fail the spec if the promise
// is rejected or isn't resolved before the spec timeout.
await subject.load(/*...*/);
// The subscriber should have been called by now. If not,
// that's a bug in DataLoader, and we want the following to fail.
expect(subscriber).toHaveBeenCalledWith(/*...*/);
});
但我真的必须测试通过不同渠道发出成功和故障信号的代码。我无法(或者不想)更改它。我应当如何解决?
如果它们不是承诺,你可以将两边都转换成承诺。之后使用Promise.race
等待第一个解决或拒绝的承诺
// in DataLoader.js
class DataLoader {
constructor(fetch) {
// ...
}
subscribe(subscriber) {
// ...
}
onError(errorSubscriber) {
// ...
}
load() {
// ...
}
}
// in DataLoaderSpec.js
it('provides the fetched data to observers', async function() {
const fetch = function() {
return Promise.resolve(/*...*/);
};
let resolveSubscriberPromise, rejectErrorPromise;
const subscriberPromise = new Promise(function(resolve) {
resolveSubscriberPromise = resolve;
});
const errorPromise = new Promise(function(resolve, reject) {
rejectErrorPromise = reject;
});
const subject = new DataLoader(fetch);
subject.subscribe(resolveSubscriberPromise);
subject.onError(rejectErrorPromise);
const result = await Promise.race([subscriberPromise, errorPromise]);
expect(result).toEqual(/*...*/);
});
请注意,这假定正在测试的代码会发出成功信号或发出失败信号,但绝不会两者都做。通常无法对在失败后可能发出成功和失败信号的异步代码编写可靠的规范。
为什么我的异步函数不能多次调用 `done`?我应当如何解决?
在 Jasmine 2.x 和 3.x 中,基于回调的异步函数可以多次调用其done
回调,只有第一次调用才有效果。之所以这样做,是为了防止 Jasmine 在done
被多次调用时破坏其内部状态。
从那时起,我们了解到异步函数仅在实际完成后才发出完成信号很重要。当规范在告诉 Jasmine 其已完成之后继续运行时,它会与其他规范的执行交织在一起。这会引起间歇性测试失败、未报告失败,或者在错误的规范上报告失败等问题。多年来,此类问题一直是用户混淆和错误报告的常见来源。Jasmine 4 尝试通过报告在异步函数多次调用done
时发生的任何错误,让它们更易于诊断。
如果你的规范多次调用 done
,那么最佳做法是将它重写成仅调用 done
一次。有关规范多次发出完成信号以及解决建议的一些常见情况,请参阅 此相关 FAQ。
如果你真的无法消除多余的 done 调用,你可以通过将 done
包装在一个函数中来实现 Jasmine 2-3 行为,此函数只忽略第一个调用,如下所示。但请注意,这样做依然会导致规范出现问题,并可能引起上述问题。
function allowUnsafeMultipleDone(fn) {
return function(done) {
let doneCalled = false;
fn(function(err) {
if (!doneCalled) {
done(err);
doneCalled = true;
}
});
}
}
it('calls done twice', allowUnsafeMultipleDone(function(done) {
setTimeout(done);
setTimeout(function() {
// This code may interleave with subsequent specs or even run after Jasmine
// has finished executing.
done();
}, 50);
}));
为什么我无法将异步函数传递给 `describe`?如何利用异步加载的数据生成规范?
同步函数无法调用异步函数,而 describe
必须是同步的,因为它用在同步上下文中,比如通过脚本标签加载的脚本。如果不这样做,会破坏所有使用 Jasmine 的现有代码,并且会让 Jasmine 在最流行的环境中不可用。
但是,如果你使用 ES 模块,则可以在调用顶层 describe
之前异步获取数据。请勿执行以下操作
// WARNING: does not work
describe('Something', async function() {
const scenarios = await fetchSceanrios();
for (const scenario of scenarios) {
it(scenario.name, function() {
// ...
});
}
});
执行以下操作
const scenarios = await fetchSceanrios();
describe('Something', function() {
for (const scenario of scenarios) {
it(scenario.name, function() {
// ...
});
}
});
要使用顶层 await
,指定文件必须是 ES 模块。如果你在浏览器中运行规范,则需要使用 jasmine-browser-runner
2.0.0 或更高版本,并在配置文件中添加 "enableTopLevelAwait": true
。
如何测试我不具备 Promise 或回调的异步行为,例如在异步获取数据后呈现某些内容的 UI 组件?
有两种基本方法来解决这个问题。第一个方法是让异步行为立即完成(或尽可能接近立即),然后在规范中 await
。这里有一个使用 enzyme 和 jasmine-enzyme 库测试 React 组件的方法示例
describe('When data is fetched', () => {
it('renders the data list with the result', async () => {
const payload = [/*...*/];
const apiClient = {
getData: () => Promise.resolve(payload);
};
// Render the component under test
const subject = mount(<DataLoader apiClient={apiClient} />);
// Wait until after anything that's already queued
await Promise.resolve();
subject.update();
const dataList = subject.find(DataList);
expect(dataList).toExist();
expect(dataList).toHaveProp('data', payload);
});
});
请注意,该规范 await 的 promise 与传递给测试代码的 promise 无关。人们通常在这两个地方使用相同的 promise,但只要传递给测试代码的 promise 已解决,这不重要。重要的是,规范中的 await
调用在测试代码中的调用之后发生。
这种方法简单、高效,并且在出现问题时会快速失败。但在测试代码执行多个 await
或 .then()
时,很难正确安排调度。测试代码中的异步操作发生更改可能会轻松破坏该规范,从而需要添加额外的 await
。
另一种方法是在所需的行为发生之前轮询
describe('When data is fetched', () => {
it('renders the data list with the result', async () => {
const payload = [/*...*/];
const apiClient = {
getData: () => Promise.resolve(payload);
};
// Render the component under test
const subject = mount(<DataLoader apiClient={apiClient} />);
// Wait until the DataList is rendered
const dataList = await new Promise(resolve => {
function poll() {
subject.update();
const target = subject.find(DataList);
if (target.exists()) {
resolve(target);
} else {
setTimeout(poll, 50);
}
}
poll();
});
expect(dataList).toHaveProp('data', payload);
});
});
起初,这有点复杂,而且效率稍低。如果预期组件未渲染,它还会超时(默认 5 秒后超时),而不会立即失败。但它更能应对变化。如果对正在测试的代码添加更多 await
或 .then()
调用,它仍然会通过。
在以第二种方式编写规范时,您可能会发现 DOM 测试库 或 React 测试库 有帮助。这两个库中的 findBy*
和 findAllBy*
查询实现了上面所示的轮询行为。
我需要对在正在测试的代码完成之前发生的对异步回调传递的参数进行断言。最佳做法是什么?
考虑 DataFetcher
类,该类获取数据、调用任何已注册的回调、执行一些清理,然后最终解析返回的 Promise。撰写规范的最佳方法是先在回调中保存参数,然后在发出完成信号之前断言它们具有合适的值,以验证回调的参数
it("calls the onData callback with the expected args", async function() {
const subject = new DataFetcher();
let receivedData;
subject.onData(function(data) {
receivedData = data;
});
await subject.fetch();
expect(receivedData).toEqual(expectedData);
});
您还可以使用 spy 获得更好的错误消息
it("calls the onData callback with the expected args", async function() {
const subject = new DataFetcher();
const callback = jasmine.createSpy('onData callback');
subject.onData(callback);
await subject.fetch();
expect(callback).toHaveBeenCalledWith(expectedData);
});
很诱人去写这样的东西
// WARNING: Does not work
it("calls the onData callback with the expected args", async function() {
const subject = new DataFetcher();
subject.onData(function(data) {
expect(data).toEqual(expectedData);
});
await subject.fetch();
});
但如果从未调用 onData
回调,这样做会错误地通过,因为期望从未运行。下面是另一种常见但错误的方法
// WARNING: Does not work
it("calls the onData callback with the expected args", function(done) {
const subject = new DataFetcher();
subject.onData(function(data) {
expect(data).toEqual(expectedData);
done();
});
subject.fetch();
});
在该版本中,规范会在正在测试的代码实际完成运行之前发出完成信号。这会导致规范的执行与其他规范交错,进而会导致 错误路由和其它问题。
为什么当规范因拒绝 Promise 而失败时,Jasmine 并不总是显示堆栈跟踪?
这类似于 为什么 Jasmine 显示的异常没有堆栈跟踪?。如果 Promise 被拒绝,原因是 Error
对象,例如 Promise.reject(new Error("out of cheese"))
,那么 Jasmine 将显示与该错误关联的堆栈跟踪。如果 Promise 无理由地被拒绝,或原因不是 Error
,那么 Jasmine 没有可显示的堆栈跟踪。
我收到一个未处理的 Promise 拒绝错误,但我认为这是一个误报。
理解 JavaScript 运行时决定哪些 Promise 拒绝被视为未处理非常重要,而不是 Jasmine。Jasmine 只是响应 JavaScript 运行时发出的未处理拒绝事件。
如果在将控制权返回 JavaScript 运行时之前不附加拒绝处理程序,仅仅创建一个已拒绝的 Promise 通常足以触发未处理的 Promise 拒绝事件。即使您不对 Promise 采取任何操作,情况也是如此。Jasmine 将未处理的拒绝转换为失败,因为它们几乎总是意味着出了意外,并且没有办法区分“真正的”未处理拒绝和将来最终会被处理的拒绝。
考虑此规范
it('causes an unhandled rejection', async function() {
const rejected = Promise.reject(new Error('nope'));
await somethingAsync();
try {
await rejected;
} catch (e) {
// Do something with the error
}
});
最终将通过 try
/catch
处理拒绝。但在规范的这一部分运行之前,JS 运行时会检测到未处理的拒绝。这是因为 await somethingAsync()
调用将控制权返回给了 JS 运行时。不同的 JS 运行时会以不同方式检测到未处理的拒绝,但通常的做法是,如果在将控制权返回给运行时前为此拒绝附加了 catch 处理程序,则不会将此拒绝视为未处理的拒绝。在大多数情况下,可以通过对代码进行一些重新排序来实现这一点
it('causes an unhandled rejection', async function() {
const rejected = Promise.reject(new Error('nope'));
let rejection;
try {
await rejected;
} catch (e) {
rejection = e;
}
await somethingAsync();
// Do something with `rejection`
});
作为最后的手段,您可以附加一个 no-op catch 处理程序来抑制未处理的拒绝
it('causes an unhandled rejection', async function() {
const rejected = Promise.reject(new Error('nope'));
rejected.catch(function() { /* do nothing */ });
await somethingAsync();
let rejection;
try {
await rejected;
} catch (e) {
rejection = e;
}
// Do something with `rejection`
});
另请参见 如何配置一个 spy 以返回被拒绝的 Promise,同时不触发未处理的 Promise 拒绝错误? 以了解如何避免在配置 spy 时出现未处理拒绝的情况。
如上所述,Jasmine 无法确定哪些拒绝计为未处理的拒绝。请不要打开相关议题,要求我们进行更改。
间谍
我如何模拟 AJAX 调用?
如果您正在使用 XMLHttpRequest
或任何在底层使用它的库,则 jasmine-ajax 是一个不错的选择。它关注模拟 XMLHttpRequest
的一些复杂细节,并提供了用于验证请求和截断响应的简便 API。
与 XMLHttpRequest
不同,较新的 HTTP 客户端 API(例如 axios 或 fetch)很容易使用 Jasmine spy 手动模拟。只需将 HTTP 客户端注入到待测代码中
async function loadThing(thingId, thingStore, fetch) {
const url = `http://example.com/api/things/{id}`;
const response = await fetch(url);
thingStore[thingId] = response.json();
}
// somewhere else
await loadThing(thingId, thingStore, fetch);
然后,在规范中注入一个 spy
describe('loadThing', function() {
it('fetches the correct URL', function() {
const fetch = jasmine.createSpy('fetch')
.and.returnValue(new Promise(function() {}));
loadThing(17, {}, fetch);
expect(fetch).toHaveBeenCalledWith('http://example.com/api/things/17');
});
it('stores the thing', function() {
const payload = return {
id: 17,
name: 'the thing you requested'
};
const response = {
json: function() {
return payload;
}
};
const thingStore = {};
const fetch = jasmine.createSpy('fetch')
.and.returnValue(Promise.resolve(response));
loadThing(17, thingStore, fetch);
expect(thingStore[17]).toEqual(payload);
});
});
为什么我在某些浏览器中无法监视 localStorage 方法?我该怎么做?
这在某些浏览器中会通过,但在 Firefox 和 Safari 17 中会失败
it('sets foo to bar on localStorage', function() {
spyOn(localStorage, 'setItem');
localStorage.setItem('foo', 'bar');
expect(localStorage.setItem).toHaveBeenCalledWith('foo', 'bar');
});
作为一项安全措施,Firefox 和 Safari 17 不允许覆盖 localStorage
的属性。为其赋值(这是 spyOn
在底层所做的)是一个 no-op 操作。这是浏览器施加的一项限制,Jasmine 无法解决。
一个备选方案是检查 localStorage
的状态,而不是验证对它的调用
it('sets foo to bar on localStorage', function() {
localStorage.setItem('foo', 'bar');
expect(localStorage.getItem('foo')).toEqual('bar');
});
另一种选择是在 localStorage
周围创建一个包装器,并模拟该包装器。
我该如何监视模块的属性?我遇到了诸如“aProperty 没有访问类型 get”、“未声明为可写或没有 setter”或“未声明为可配置”的错误。
此错误意味着某些内容(可能是转换器,但可能是 JavaScript 运行时)已将模块的导出属性标记为只读。ES 模块规范要求导出的模块属性为只读,一些转换器即使在发出 CommonJS 模块时也会遵循此要求。如果某个属性被标记为只读,则 Jasmine 无法用 spy 替换它。
无论处在什么环境下,都可以通过将依赖注入用于需要模拟的内容,并从规范中注入 spy 或 mock 对象来避免该问题。该方法通常会改进规范和被测代码的可维护性。经常需要模拟模块表明代码耦合紧密,与采用测试工具解决此问题相比,修复耦合通常更明智。
根据所处环境,有可能启用模块模拟。有关更多信息,请参阅模块模拟指南。
我该如何配置一个监视器来返回一个拒绝的 Promise,而不触发未处理的 Promise 拒绝错误?
理解 JavaScript 运行时决定哪些 Promise 拒绝被视为未处理非常重要,而不是 Jasmine。Jasmine 只是响应 JavaScript 运行时发出的未处理拒绝事件。
只需创建一个 rejected promise,如果允许控制权返回到 JavaScript 运行时而未附加 rejection handler,就足以在 Node 和大多数浏览器中触发未处理的 rejection 事件。即便不使用该 promise,这一情况也适用。Jasmine 将未处理的 rejection 转换为失败,因为这几乎总是意味着发生了一些意外的错误。(另请参阅:出现未处理的 promise rejection 错误,但我认为这是误报。)
考虑此规范
it('might cause an unhandled promise rejection', async function() {
const foo = jasmine.createSpy('foo')
.and.returnValue(Promise.reject(new Error('nope')));
await expectAsync(doSomething(foo)).toBeRejected();
});
该规范创建了一个 rejected promise。如果一切正常,它将最终由 async 匹配器进行处理。但如果 doSomething
调用 foo
失败或将 rejection 传递失败,浏览器或 Node 将触发未处理的 promise rejection 事件。Jasmine 会将此视为在该事件发生时运行的套件或规范的失败。
一种修复方法仅在实际调用 spy 时创建 rejected promise
it('does not cause an unhandled promise rejection', async function() {
const foo = jasmine.createSpy('foo')
.and.callFake(() => Promise.reject(new Error('nope')));
await expectAsync(doSomething(foo)).toBeRejected();
});
通过使用 rejectWith spy 策略可以使此问题更为明确
it('does not cause an unhandled promise rejection', async function() {
const foo = jasmine.createSpy('foo')
.and.rejectWith(new Error('nope'));
await expectAsync(doSomething(foo)).toBeRejected();
});
如上所述,Jasmine 无法确定哪些拒绝计为未处理的拒绝。请不要打开相关议题,要求我们进行更改。
贡献
我想要对 Jasmine 提供帮助。我该从哪里开始?
感谢您的帮助!Jasmine 团队用于开发 Jasmine 的时间有限,因此我们非常感谢社区提供的帮助。
Github Issues
当报告的 github issues 看起来像 Jasmine 所能支持的问题时,我们会使用“help needed”标记该 issue。该标记表示我们认为对话中包含足够的信息,供其他人自行实现。(我们并不总是正确。如果您有其他问题,请提问)。
新想法
您是否有未在 GitHub issue 中包含的想法?您可以随时提出建议。我们建议(但不要求)您在提交请求之前打开一个 issue 来讨论您的想法。我们不会同意每一个建议,因此最好在投入大量工作之前先询问。
Jasmine 用什么来测试自身?
Jasmine 使用 Jasmine 来测试 Jasmine。
Jasmine 的测试套件加载了两个 Jasmine 副本。第一个从 lib/
中的已编译文件加载。第二个,称为 jasmineUnderTest
,直接从 src/
中的源文件加载。第一个 Jasmine 用于运行规范,而这些规范又调用 jasmineUnderTest
上的功能。
这具有以下优点
- 开发人员通过使用它来开发 Jasmine,从而对 Jasmine 的设计得到反馈。
- 开发人员可以选择针对 Jasmine 的最后一个提交版本(通过不采取任何操作)或针对当前代码(首先进行编译)进行测试。
- Jasmine 的测试无法运行,因为 Jasmine 中新引入了一个 bug,因此不可能陷入这种状态。开发人员可以通过在规范为绿色之前不构建它来避免这种情况,并且只需运行
git checkout lib
即可摆脱这种情况。 - 由于不需要构建步骤,因此从保存文件到看到测试运行结果可能只需不到两秒钟。
如果你好奇这是如何设置的,请参阅 requireCore.js 和 defineJasmineUnderTest.js。
为什么 Jasmine 有一个搞笑的手动模块系统?为什么不使用 Babel 和 Webpack?
简而言之,Jasmine 早于 Babel 和 Webpack,而转换为这些工具将是大量工作,而回报却很小,因为当 Jasmine 放弃对 Internet Explorer 等非 ES2017 环境的支持时,这种回报就基本上消失了。尽管很多 Jasmine 仍然使用 ES5 编写,但现在可以使用较新的语言功能。
Jasmine 的大部分生命周期都在不支持较新 JavaScript 功能的浏览器上运行。这意味着编译后的代码无法使用较新的语法和库功能,例如箭头函数、async
/await
、Promise
、Symbol
、Map
和 Set
。因此,它是使用 ES5 语法编写的,除了在异步匹配器等特定狭窄上下文中之外,不使用任何不可移植的库功能。
那么为什么不采用 Babel 和 Webpack 呢?部分原因是 Jasmine 处于一个奇怪的空间,破坏了这些工具做出的部分假设:它既是应用程序,也是库,即使当它作为应用程序运行时,它也不能安全地修改 JavaScript 运行时环境。如果 Jasmine 为缺失的库功能添加填充,可能会导致依赖于这些功能的代码规范在没有这些功能的浏览器上错误通过。我们尚未找到以确保不会引入任何填充的方式来配置 Babel 和 Webpack(或任何其他打包工具)。即使我们做到了,回报也很小。编写 ES5 语法而不是 ES6 是支持多种浏览器的简单部分。困难的部分,主要是处理缺失的库功能和其他不兼容性,仍然需要手工解决。
Jasmine 现有的构建工具具有简单、快速且极低维护性的优点。如果变更带来重大改进,我们并不反对切换到更新的内容。但到目前为止,在该领域保持保守让我们得以跳过相当多的前端构建工具变更,并利用这些时间致力于对用户有益的工作。
我该如何开发一个依赖于一些受支持环境中缺失的内容的功能?
我们尝试让 Jasmine 的所有功能都可以在所有受支持的浏览器和 Node 版本中使用,但有时这没有意义。例如,对返回 Promise 规范的支持已在 2.7.0 中添加,即使 Jasmine 仍在 4.0.0 之前缺乏 Promise 的环境中运行。要编写在所有环境中都不可用的规范,请检查是否存在所需的语言/运行时功能,如果不存在,请将规范标记为挂起。请参阅 spec/helpers/checkForUrl.js 及其对 requireUrls
函数的使用,它定义了如何执行此操作的示例。
参见 src/core/base.js 中的 is* 方法,了解如何安全检查对象是否是可能不存在类型的实例的示例。