Logo
Section 1 / 4

使用 async 和 await 测试 Promise

学习如何使用 async 和 await 测试 Promise。

导入

使用 async 和 await 测试 Promise

学习如何使用 async 和 await 测试 Promise。

在日常工作中,我们经常需要处理 Promise(MDN 文章)。Jasmine 提供了一种简单的方式来使用 async 和 await 测试这些异步操作(MDN 文章)。

ArticleDelete 类

接下来是一个简化示例,展示如何在浏览器应用中处理文章删除逻辑。该类接收输入,并在满足条件时向服务器 API 发起请求。

本课程中,ArticleAPI 的方法被视为返回 Promise 实例。因此,delete 方法也返回一个 Promise。

运行下方代码沙盒并查看详细说明。

No configuration found for ID: 0b2d26ff937111f4f6d003edc0f2f290777c0013497d8fb345a460e6ff22a854

ArticleDelete 类详解

ArticleDelete 类依赖于 ArticleAPI 类来执行 DELETE /api/article/:id 请求,前提是条件满足。

  • ArticleAPI 是一个实现服务器 API 调用的类。由于超出当前课程范围,此处为空。测试中我们将对其响应进行模拟。

  • ArticleDelete 是主类,其结构如下:

    • 1import { ArticleAPI } from './article-api.mjs';

      这行代码从当前文件夹中的相邻模块 article-api.mjs 导入 ArticleAPI 依赖。

    • 1export class ArticleDelete {

      这行代码声明并导出该类,以便在其他模块中使用。

    • 1 /** @type {ArticleAPI} */ 2 articleAPI; 3 4 /** 5 * @param { ArticleAPI } article the article api 6 */ 7 constructor(articleApi) { 8 this.articleAPI = articleApi; 9 }

      这些代码声明了 articleAPI 属性,并在创建类实例时赋值。换句话说,创建 ArticleDelete 实例时必须传入一个 ArticleAPI 实例。

    • 1 async delete(a) { 2 if (a != null && a.id != null) { 3 try { 4 await this.articleAPI.delete(a.id); 5 return `article deleted: "${a.title}"`; 6 } catch(e) { 7 //... 省略 8 } 9 } 10 11 return 'no article to delete'; 12 }

      delete 方法调用 ArticleAPI 的 delete 方法,并等待其结果,然后返回成功信息。try-catch 块包裹此逻辑。如果 delete 方法返回的 Promise 被拒绝,则会触发 catch。

      delete 方法是一个 异步函数。 这意味着我们可以使用 await 关键字等待 Promise 的解析(或拒绝),然后再继续执行后续逻辑。

    • 1catch (e) { 2 if (e && e.status === 404) { 3 return `It looks like article "${a.title}" has already been deleted`; 4 } 5 return `Unknown error trying to delete "${a.title}". Please try again.`; 6}

      catch 块的逻辑:

      • 当响应状态为 404(未找到)时,通知调用者该文章已被删除。
      • 否则,提示删除失败,建议重试。

测试详解

查看 spec/article-delete.spec.mjs 文件,内容如下:

  • 1import { ArticleDelete } from '../__src__/article-delete.mjs'; 2 3describe('ArticleDelete', () => {

    首先导入 ArticleDelete 类,并为其定义一个测试套件,即 describe('ArticleDelete', () => {。

  • 1let article; 2let articleAPI; 3 4beforeEach(() => { 5 articleAPI = jasmine.createSpyObj('article API', ['delete']); 6 article = new ArticleDelete(articleAPI); 7});

    由于需要 ArticleAPI 的实例,我们创建一个带有 delete 方法的模拟对象,因为这是 ArticleDelete 类中唯一使用的接口。接着实例化 ArticleDelete。这两个实例会在每个测试前通过 beforeEach 钩子重新创建,确保每个测试彼此独立,互不影响。

正常流程测试详解

在测试用例 should call the articleAPI delete and return the deleted article title 中,我们关注所谓的“正常流程”测试,也称“快乐路径”(维基百科文章了解更多详情)。即测试方法接收到预期输入并按预期工作的情况。

  • 1it(`should call the articleAPI delete and return the deleted article title`, async () => {

    注意 it 的第二个参数是一个 async 函数。Jasmine 知道如何处理这类函数。本质上,该函数返回一个 Promise,Jasmine 会等待其解析;而函数本身也会等待所有 await ... 表达式完成。

  • 1// Arrange 2articleAPI.delete.and.returnValue(Promise.resolve({ id: 1 }));

    利用模拟功能,确保当 ArticleAPI 的 delete 方法被调用时,返回一个 Promise。Promise.resolve 返回一个以指定值解析的 Promise。此处为 {id: 1}。

  • 1// Act 2const result = await article.delete({ id: 1, title: 'the first article' });

    调用 delete 方法,并等待其响应解析。

  • 1// Assert 2expect(articleAPI.delete).toHaveBeenCalledOnceWith(1); 3expect(result).toEqual('article deleted: "the first article"'); 4});

    最后断言:delete 方法被调用,且返回结果符合预期。

404 错误测试详解

在测试用例 when the articleAPI delete returns status 404 it should return - already been deleted 中,我们验证当服务器返回 404 错误时,ArticleDelete.delete 方法的行为是否符合预期。

  • 1it(`when the articleAPI delete returns status 404 it should return - already been deleted`, async () => {

    注意 it 的第二个参数是一个 async 函数,利用 Jasmine 对 Promise 的处理能力。

  • 1// Arrange 2articleAPI.delete.and.returnValue(Promise.reject({ status: 404, message: 'not found' }));

    利用模拟功能,确保当 ArticleAPI 的 delete 方法被调用时,返回一个被拒绝的 Promise。Promise.reject 返回一个以指定值拒绝的 Promise。此处为 {status: 404, message: 'not found'}。

    注意:这只是 rejected Promise 的一种可能形式示例。具体 API 或库的错误处理方式可能不同。

  • 1// Act 2const result = await article.delete({ id: 1, title: 'the first article' });

    接着调用 delete 方法,并等待其响应解析。

  • 1// Assert 2expect(result).toEqual('It looks like article "the first article" has already been deleted');

    最后断言:返回结果符合 404 错误的预期行为。

练习

本练习聚焦于代码沙盒中的 src/article-create.mjs 文件。ArticleCreate 类与 ArticleDelete 类非常相似,主要区别在于 create 方法:

  • 1async create(title, content) { 2 if (typeof title === 'string' && typeof content === 'string') { 3 try { 4 await this.articleAPI.create(title, content); 5 return `article created: "${title}" with content "${content}"`; 6 } catch (e) { 7 if (e && e.status === 409) { 8 return `it appears that an article with that title "${title}" already exists` 9 } 10 11 return `failed creating article ${title} - please try again later` 12 } 13 } 14 15 return `expected string title and content but recived title: "${title}" content: "${content}"`; 16}
  • 它是一个 async 方法。

  • 接收两个参数 title 和 content,用于创建文章,并调用 ArticleAPI 的 create 方法。

  • 返回的字符串确认文章已创建,或提示错误信息。

  • 若返回状态为 409 冲突,则提示已有相同标题的文章存在。

请为上述逻辑编写三个测试用例:

  • 当传入标题和内容时,应调用 ArticleAPI 的 create 方法。
  • 当 API 返回 409 冲突时,应告知调用者该标题已存在。
  • 当未传入标题或内容时,应返回错误说明。

注意:ArticleDelete 的测试也包含在代码沙盒中。

No configuration found for ID: 68ceb17cf82c4f3aaae62a58

1it(`should call the articleAPI create and return the created article title and content`, async () => { 2 // Arrange 3 articleAPI.create.and.returnValue(Promise.resolve({id: 1})); 4 // Act 5 const res = await article.create('My new title', `## Heading 6 7 Paragraph 1 with some content 8 9 ## heading 2 10 11 Paragraph 2 with some more content 12 `); 13 // Assert 14 expect(res).toEqual(`article created: "My new title" with content "## Heading 15 16 Paragraph 1 with some content 17 18 ## heading 2 19 20 Paragraph 2 with some more content 21 "`) 22});
1it(`should call the articleAPI create and return the created article title and content`, async () => { 2 // Arrange 3 articleAPI.create.and.returnValue(Promise.resolve({id: 1})); 4 // Act 5 const res = await article.create('My new title', `## Heading 6 7 Paragraph 1 with some content 8 9 ## heading 2 10 11 Paragraph 2 with some more content 12 `); 13 // Assert 14 expect(res).toEqual(`article created: "My new title" with content "## Heading 15 16 Paragraph 1 with some content 17 18 ## heading 2 19 20 Paragraph 2 with some more content 21 "`) 22});
1it(`when the API returns a 409 Conflict it should advise caller such a title already exists`, async () => { 2 // Arrange 3 articleAPI.create.and.returnValue(Promise.reject({status: 409})); 4 // Act 5 const res = await article.create('My new title', 'content'); 6 // Assert 7 expect(res).toEqual(`it appears that an article with that title "My new title" already exists`) 8});
1it(`when the API returns a 409 Conflict it should advise caller such a title already exists`, async () => { 2 // Arrange 3 articleAPI.create.and.returnValue(Promise.reject({status: 409})); 4 // Act 5 const res = await article.create('My new title', 'content'); 6 // Assert 7 expect(res).toEqual(`it appears that an article with that title "My new title" already exists`) 8});
1it(`should return what was expected and what was received when missing parameters or not string`, async () => { 2 // Arrange 3 // Act 4 const result = await article.create(); 5 // Assert 6 expect(result).toEqual('expected string title and content but recived title: "undefined" content: "undefined"'); 7});
1it(`should return what was expected and what was received when missing parameters or not string`, async () => { 2 // Arrange 3 // Act 4 const result = await article.create(); 5 // Assert 6 expect(result).toEqual('expected string title and content but recived title: "undefined" content: "undefined"'); 7});

使用 Jasmine 测试 JavaScript

使用 Done 或 Await 测试隐藏的异步操作