常见问题

  1. 总体概况
    1. Jasmine 的下一个版本何时发布?
    2. Jasmine 如何进行版本控制?
    3. 如何在 jasmine-browser-runner 中使用外部 URL 中的脚本?
    4. Jasmine 可以测试 ES 模块中的代码吗?
    5. 为什么 Jasmine 允许规范中出现多个期望故障?如何禁用该功能?
    6. 如何让 Jasmine 使不包含任何断言的规范失败?
    7. 如何在 TypeScript 项目中使用 Jasmine?
  2. 与 Jasmine 兼容的其他软件
    1. 我是否可以使用 Jasmine 5.x 与 Karma?
    2. 为什么 Karma 中没有更新的 Jasmine 功能?
    3. 我遇到有关 zone.js 的问题。您能提供帮助吗?
    4. 如何将 Jasmine 匹配器与 testing-library 的 waitFor 函数一起使用?
    5. 为什么 expect() 在 webdriver.io 中无法正常工作?
  3. 编写规范
    1. 我应当将普通函数还是箭头函数传递给 describeitbeforeEach 等?
    2. 如何在包含 describebeforeEach 之前运行代码?Jasmine 是否有相当于 rspec 的 let 的一项功能?
    3. 为什么 Jasmine 显示不带堆栈跟踪的异常?
    4. Jasmine 是否支持参数化测试?
    5. 如何向匹配器故障消息中添加更多信息?
  4. 异步测试
    1. 我应该使用哪种异步样式,为什么?
    2. 为什么某些异步规范故障会报告为套件故障或其他规范的故障?
    3. 如何阻止 Jasmine 并行运行我的规范?
    4. 为什么我无法编写既包含一个回调又返回 Promise(或是一个异步函数)的规范?我应当如何解决?
    5. 但我真的必须测试通过不同渠道发出成功和故障信号的代码。我无法(或者不想)更改它。我应当如何解决?
    6. 为什么我的异步函数不能多次调用 `done`?我应当如何解决?
    7. 为什么我无法将异步函数传递给 `describe`?如何利用异步加载的数据生成规范?
    8. 如何测试我不具备 Promise 或回调的异步行为,例如在异步获取数据后呈现某些内容的 UI 组件?
    9. 我需要对在正在测试的代码完成之前发生的对异步回调传递的参数进行断言。最佳做法是什么?
    10. 为什么当规范因拒绝 Promise 而失败时,Jasmine 并不总是显示堆栈跟踪?
    11. 我收到一个未处理的 Promise 拒绝错误,但我认为这是一个误报。
  5. 间谍
    1. 我如何模拟 AJAX 调用?
    2. 为什么我在某些浏览器中无法监视 localStorage 方法?我该怎么做?
    3. 我该如何监视模块的属性?我遇到了诸如“aProperty 没有访问类型 get”、“未声明为可写或没有 setter”或“未声明为可配置”的错误。
    4. 我该如何配置一个监视器来返回一个拒绝的 Promise,而不触发未处理的 Promise 拒绝错误?
  6. 对 Jasmine 做出贡献
    1. 我想要对 Jasmine 提供帮助。我该从哪里开始?
    2. Jasmine 用什么来测试自身?
    3. 为什么 Jasmine 有一个搞笑的手动模块系统?为什么不使用 Babel 和 Webpack?
    4. 我该如何开发一个依赖于一些受支持环境中缺失的内容的功能?

总体概况

Jasmine 的下一个版本何时发布?

这取决于贡献的速度和维护人员时间的可用性。

Jasmine 完全是一个志愿者的努力,这使得很难预测何时发布新版本,也不可能承诺一个时间表。过去,包含新功能的版本通常每 1-6 个月发布一次。当发现新的错误时,我们会尝试尽快发布一个修复。

返回常见问题解答索引

Jasmine 如何进行版本控制?

Jasmine 尽可能尝试遵循语义版本控制。这意味着我们会保留主要版本(1.0、2.0 等)用于破坏性更改或其他重要工作。大多数 Jasmine 版本最终都是次要版本(2.3、2.4 等)。主要版本很少见。

许多人通过jasmine 软件包(在 Node 中运行规范)或jasmine-browser-runner 软件包使用 Jasmine。出于历史原因,这些软件包有不同的版本控制策略

Jasmine 通常不会停止对浏览器或 Node 版本的支持,除非版本有重大变动。此规则的例外情况包括:生命周期已结束的 Node 版本、我们无法再在本地安装及/或在其 CI 构建环境中进行测试的浏览器、不再接受安全更新的浏览器以及仅在不再接收安全更新的操作系统上运行的浏览器。我们会尽合理努力确保 Jasmine 在这些环境中正常工作,但如果这些环境出现故障,不一定能提供重大版本更新。

返回常见问题解答索引

如何在 jasmine-browser-runner 中使用外部 URL 中的脚本?

您可以将脚本的 URL 添加到 jasmine-browser.jsonjasmine-browser.js 文件中的 srcFiles

  // ...
  srcFiles: [
    "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js",
    "**/*.js"
  ],
  // ...
返回常见问题解答索引

Jasmine 可以测试 ES 模块中的代码吗?

可以。具体流程取决于您使用 Jasmine 的方式

返回常见问题解答索引

为什么 Jasmine 允许规范中出现多个期望故障?如何禁用该功能?

有时需要多个预期来断言特定结果。在这种情况下,您可能希望在尝试让其中任何一个预期通过之前,先查看所有预期是否失败。当单一代码更改可能导致多个预期通过时,这会特别有用。

如果您希望每个规范在第一个预期失败时停止,则可以将 oneFailurePerSpec 选项设置为 true

请注意,与该规范关联的任何 afterEach 或 afterAll 函数仍可运行。

返回常见问题解答索引

如何让 Jasmine 使不包含任何断言的规范失败?

默认情况下,Jasmine 不需要规范包含任何期望。你可以通过将 failSpecWithNoExpectations 选项设为 true 来启用该行为。

我们不建议依赖 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。

返回常见问题解答索引

编写规范

我应当将普通函数还是箭头函数传递给 describeitbeforeEach 等?

对于 describe,这没关系。对于 itbeforeEachafterEach,你可能更喜欢使用常规函数。Jasmine 会创建一个 用户上下文,并将其作为 this 传递给每个 itbeforeEachafterEach 函数。这允许你在这些函数之间轻松传递变量,并确保它们在每个规范后被清除。但是,这不适用于箭头函数,因为箭头函数中的 this 在词法上是绑定的。因此,如果你想要使用用户上下文,你必须坚持使用常规函数。

返回常见问题解答索引

如何在包含 describebeforeEach 之前运行代码?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

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。这里有一个使用 enzymejasmine-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(例如 axiosfetch)很容易使用 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”或“未声明为可配置”的错误。

注意:此常见问题解答涉及一个变化迅速的领域,可能会过时。它最近更新于 2023 年 9 月。

此错误意味着某些内容(可能是转换器,但可能是 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 上的功能。

这具有以下优点

如果你好奇这是如何设置的,请参阅 requireCore.jsdefineJasmineUnderTest.js

返回常见问题解答索引

为什么 Jasmine 有一个搞笑的手动模块系统?为什么不使用 Babel 和 Webpack?

简而言之,Jasmine 早于 Babel 和 Webpack,而转换为这些工具将是大量工作,而回报却很小,因为当 Jasmine 放弃对 Internet Explorer 等非 ES2017 环境的支持时,这种回报就基本上消失了。尽管很多 Jasmine 仍然使用 ES5 编写,但现在可以使用较新的语言功能。

Jasmine 的大部分生命周期都在不支持较新 JavaScript 功能的浏览器上运行。这意味着编译后的代码无法使用较新的语法和库功能,例如箭头函数、async/awaitPromiseSymbolMapSet。因此,它是使用 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* 方法,了解如何安全检查对象是否是可能不存在类型的实例的示例。

返回常见问题解答索引

我们有遗漏的问题吗?

请打开 问题Pull 请求