自动化 API 测试作为保障软件质量的有效途径,可助你深入理解目标行为及展示问题原因。尤其在 API 为行业普及的背景下,熟练掌握实现 API 单元测试以及索求优质 API 测验人选之技可能稍显复杂。
然而,无论是 API 开发阶段,抑或是新增功能开发过程,系统性地测试预设行为均能节约大量时间,更易发现问题。通过构建模拟 API 调用,便可运用宝贵的单元测试资源,规避实时 API 调用所带来的困扰。
Node.js 为行为驱动 JavaScript 提供多种自动化测试环境,其中 Jest 和 Jasmine 两大框架广受欢迎。本文将对其进行对比分析。首先,探讨行为驱动开发中运用 API 的最佳实践与技巧;其次,讲解如何在 Jasmine 或 Jest 中设定并运行 API 测试;最后,对两框架进行对比,协助你选择最符合需求的工具。
也许您在读这篇文章时会想:测试很棒,但为什么不测试实际的 API 呢?您可以使用 Axios 之类的库从网络发出 HTTP 请求。如果您正在开发 API,它可以帮助您在部署之前在本地进行规划。如果您的 API 已经部署,但您正在添加新功能,那么您肯定不想将未经测试的代码推送到实时版本。此外,您可能想要测试一个因过度使用而收费或具有不确定结果(例如来自数据库的动态数据)的 API。模拟 API 调用可让您在这些情况下进行控制,并加快后续开发速度。行为驱动开发的第一步是列出您的 API 在正常运行时应该做的所有事情。每一项都应该具体、可衡量且确定。如果您有写得很好的文档,这可能是您的起点。如果您刚刚开始,那么这个初步计划可能会变成您的文档。
对于每个步骤,列出一个示例,说明此请求的原始数据是什么样子以及响应应该是什么样子。例如:
为了在不连接网络的情况下测试这些行为,您应该将此逻辑与主要路由分开。这样,您就可以在测试中轻松导入 API 所依赖的相同函数。您可以创建一个函数来处理请求对象并返回一个承诺来模拟异步模拟 API 调用。它可能看起来像这样:
<strong>function</strong> <strong>simulateAsyncCall</strong>(request) {<br>
<strong>return</strong> <strong>new</strong> <strong>Promise</strong>((resolve, reject) => {<br>
setTimeout(() => {<br>
<strong>switch</strong> (request.method) {<br>
<strong>case</strong> 'get':<br>
<strong>const</strong> user = <strong>getUser</strong>(request);<br>
<strong>if</strong> (user) {<br>
<strong>resolve</strong>({ status: 200, posts: user.posts });<br>
} <strong>else</strong> {<br>
<strong>resolve</strong>({ status: 404, message: 'Not Found' });<br>
}<br>
<strong>break</strong>;<br>
<strong>case</strong> 'post':<br>
<strong>if</strong> (<strong>passwordIsValid</strong>(request)) {<br>
<strong>addToPosts</strong>(request);<br>
<strong>resolve</strong>({ status: 200, message: 'Added Post' });<br>
} <strong>else</strong> {<br>
<strong>resolve</strong>({ status: 401, message: 'Unauthorized' });<br>
}<br>
<strong>break</strong>;<br>
default:<br>
<strong>resolve</strong>({ status: 400, message: 'Bad Request' });<br>
}<br>
}, 300);<br>
});<br>
}
在安装任何 Node 模块之前,你应该确保你的根目录中有一个 package.json 文件。如果没有,你可以使用以下命令进行设置:npm init -y
。
让我们首先在你的项目目录中安装 Jasmine:npm install jasmine –save-dev
完成后,您可以使用以下命令配置 Jasmine:node node_modules/jasmine/bin/jasmine.js init
这将创建一个 spec 文件夹,其中包含一些具有默认设置的配置文件。请务必记住,我们编写的测试文件应以 “spec.js” 结尾
最后,我们需要在 package.json 文件中设置测试命令。打开此文件并向对象添加一个包含 Jasmine 模块路径的test
键。它应该如下所示:scripts
"scripts":{<br>
"test":"jasmine"<br>
}
让我们确保所有设置都正确。在spec
名为my-first-spec.js
paste this 的文件夹中创建一个名为的文件:
<strong>describe</strong>('My Jasmine Setup', <strong>function</strong> () {<br>
<strong>var</strong> a = true;<br>
<br>
<strong>it</strong>('tests if the value of a is true', <strong>function</strong> () {<br>
<strong>expect</strong>(a).<strong>toBe</strong>(true);<br>
});<br>
});
您可以通过运行我们在 package.json 中放入的测试脚本在终端中运行测试:npm run test
您的测试应该会通过!尝试将值更改a
为 false,然后再次运行它,看看测试失败时会是什么样子。请注意,当出现问题时,它会向您提供描述性消息。
Jasmine 测试套件的另一个很酷的功能是 “beforeEach” 和 “afterEach” 函数。这允许您在每个 “it” 块之前或之后执行某些操作。让我们在每次测试之前创建一个新的 MockAPI 类实例。
以下脚本可以在 Jasmine 或 Jest 中运行:
<strong>const</strong> <strong>MockAPI</strong>= require('../MockAPI.js');<br>
<br>
<strong>describe</strong>("Mock API",()=>{<br>
<strong>let</strong> mockAPI;<br>
<strong>let</strong> mockDatabase=<br>
{<br>
users:[<br>
{<br>
name:"Jack",<br>
passwordHash:"dasdKDKDJSLASDLASDJSAasdsdc123",<br>
posts:["I just bought some magic beans!"]<br>
},<br>
{<br>
name:"Jill",<br>
passwordHash:"dasdKDKDJSLASDLASDJSAasdsdc123"<br>
posts:["Jack fell down!"]<br>
},<br>
]<br>
};<br>
<br>
<strong>beforeEach</strong>(()=>{<br>
mockAPI= <strong>new</strong> <strong>MockAPI</strong>(mockDatabase)<br>
})<br>
<br>
<strong>it</strong>("returns a 400 bad request status if the request is invalid",()=>{<br>
<strong>const</strong> mockApiCall=mockAPI.<strong>simulateAsyncCall</strong>({})<br>
<strong>return</strong> mockApiCall.<strong>then</strong>(response=>{<br>
<strong>expect</strong>(response.status).<strong>toBe</strong>(400)<br>
})<br>
})<br>
<br>
<strong>describe</strong>("get requests",()=>{<br>
<strong>const</strong> validRequest={method:'get',body:{user:"Jack"}};<br>
<strong>const</strong> invalidRequest={method:'get',body:{user:"Tod"}};<br>
<br>
<strong>it</strong>("returns a 404 status if a user is not found",()=>{<br>
<strong>const</strong> mockApiCall=mockAPI.<strong>simulateAsyncCall</strong>(invalidRequest)<br>
<strong>return</strong> mockApiCall.<strong>then</strong>(response=>{<br>
<strong>expect</strong>(response.status).<strong>toBe</strong>(404)<br>
})<br>
});<br>
<br>
<strong>it</strong>("returns a 200 status with a user's posts",()=>{<br>
<strong>const</strong> mockApiCall=mockAPI.<strong>simulateAsyncCall</strong>(validRequest)<br>
<strong>return</strong> mockApiCall.<strong>then</strong>(response=>{<br>
<strong>expect</strong>(response.status).<strong>toBe</strong>(200)<br>
<strong>expect</strong>(response.posts).<strong>toEqual</strong>(["I just bought some magic beans!"])<br>
})<br>
});<br>
})<br>
<br>
<strong>describe</strong>("post requests",()=>{<br>
<strong>const</strong> validRequest={method:'post',body:{user:"Jill",password:'hill',post:"He broke his crown!"}}<br>
<strong>const</strong> invalidRequest={method:'post',body:{user:"Jill",password:'beanstock',post:"Jack is cool..."}}<br>
<br>
<strong>it</strong>("returns a 401 unauthorized status if the wrong credentials are sent",()=>{<br>
<strong>const</strong> mockApiCall=mockAPI.<strong>simulateAsyncCall</strong>(invalidRequest)<br>
<strong>return</strong> mockApiCall.<strong>then</strong>(response=>{<br>
<strong>expect</strong>(response.status).<strong>toBe</strong>(401)<br>
<strong>expect</strong>(mockAPI.db).<strong>toEqual</strong>(mockDatabase)<br>
})<br>
})<br>
<br>
<strong>it</strong>("returns a 200 status and adds the post to the database",()=>{<br>
<strong>const</strong> newDatabase={<br>
users:[<br>
{<br>
name:"Jack",<br>
passwordHash:"dasdKDKDJSLASDLASDJSAasdsdc123"<br>
posts:["I just bought some magic beans!"]<br>
},<br>
{<br>
name:"Jill",<br>
passwordHash:"dasdKDKDJSLASDLASDJSAasdsdc123"<br>
posts:["Jack fell down!","He broke his crown!"]<br>
},<br>
]<br>
}<br>
<strong>const</strong> mockApiCall=mockAPI.<strong>simulateAsyncCall</strong>(validRequest)<br>
<strong>return</strong> mockApiCall.<strong>then</strong>(response=>{<br>
<strong>expect</strong>(response.status).<strong>toBe</strong>(200)<br>
<strong>expect</strong>(mockAPI.db).<strong>toEqual</strong>(newDatabase)<br>
})<br>
})<br>
})<br>
})
要开始使用 Jest,你只需要安装它:npm install jest –save-dev
并在你的 package.json 文件中包含一个测试命令,如下所示:
"scripts":{<br>
"test":" jest"<br>
}
Jest 最初是 Jasmine 的一个分支,因此您可以执行我们上面描述的所有操作,甚至更多。“describe” 块的基本模式和包含一个或多个 “expect” 方法的 “it” 块在 Jest 中的工作原理相同。
到目前为止,我们一直在测试确定性函数(对于给定的输入,它们始终具有相同的输出)。但如果我们的 API 依赖于我们无法控制的东西,该怎么办?例如,如果我们的 API 使用第三方登录进行身份验证,该怎么办?
Jest 允许您创建模拟函数,该函数返回可预测的结果,并包含额外的方法来跟踪函数如何与 API 集成。使用 jest.fn 方法,我们可以做出如下断言:
<strong>describe</strong>('AJAX functions with Jest', () => {<br>
<strong>const</strong> mockUrl = '/api/users';<br>
<strong>const</strong> mockUsers = [{ name: 'jack', name: 'jill' }];<br>
<strong>const</strong> getUsers = jest.<strong>fn</strong>(url => mockUsers);<br>
<strong>it</strong>('returns returns users from an api call', () => {<br>
<strong>expect</strong>(<strong>getUsers</strong>(mockUrl)).<strong>toBe</strong>(mockUsers);<br>
console.<strong>log</strong>(getUsers);<br>
});<br>
<strong>it</strong>('called getUser with a mockUrl', () => {<br>
<strong>expect</strong>(getUsers).<strong>toHaveBeenCalledWith</strong>(mockUrl);<br>
});<br>
});
如果你运行这个测试并查看 console.log,你会注意到有很多方法与这个模拟函数相关联。这些方法允许你具体定义函数的调用方式、函数应返回的内容等等。
您还可以使用 模拟整个模块(用 jest mock 函数替换它们的方法)。例如,您可以导入 HTTP 库(例如 Axios)并像这样jest.mock()
设置其方法的返回值:.get()
<strong>const</strong> axios = require('axios');<br>
jest.<strong>mock</strong>('axios');<br>
<br>
<strong>class</strong> <strong>Users</strong> {<br>
<strong>static</strong> <strong>all</strong>() {<br>
<strong>return</strong> axios.<strong>get</strong>('/users.json').<strong>then</strong>(resp => resp.data);<br>
}<br>
}<br>
<strong>const</strong> mockUsers = [{ name: 'Jack' }];<br>
<strong>const</strong> mockResponse = { data: mockUsers };<br>
axios.get.<strong>mockResolvedValue</strong>(mockResponse);<br>
<br>
<strong>return</strong> <strong>Users</strong>.<strong>all</strong>().<strong>then</strong>(data => <strong>expect</strong>(data).<strong>toEqual</strong>(users));
Jasmine 还有一个用于模拟 AJAX 调用的插件 ( jasmine-ajax ),但它不像 Jest 那样灵活。它用自定义响应替换浏览器中的 XMLHttpRequest 对象。由于 XMLHttpRequest 存在于 DOM 中,因此您需要创建一个假 DOM(使用类似 jsdom 的东西)才能在后端运行它。
Jasmine 和 Jest 有很多相似之处。如果您的 API 主要由纯函数组成,那么 Jest 和 Jasmine 都是不错的选择,可确保您的 API 按预期运行。
Jasmine 比 Jest 更快、更轻量,但功能较少。在控制台中运行测试时,Jest 更具描述性,但如果您更注重简约,您可能更喜欢 Jasmine。我们欣赏 Jest 中模拟函数的灵活性,因此对于复杂的 API,我们建议使用 Jest 而不是 Jasmine。
但是,使用 Jest 和 Jasmine 等测试框架也有几个缺点。如果您有多个团队负责应用程序的不同部分(例如:前端与后端,或原生与 Web),您可能需要为每个单独的环境编写和更新测试。此外,由于您实际上并未部署到网络,因此 API 测试环境中可能会遗漏一些细节。
出于这些原因,如果您正在模拟仍在开发中的 API,那么在本地或云端生成模拟服务器会很有用。Stoplight 有一个开源模拟服务器,它可以根据 OpenAPI 文档生成。立即开始并在获得实时数据之前设置您的 API 测试,我们希望您发现这些 API 测试最佳实践很有帮助!