使用 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});