介绍
今天将向你展示一种令人难以置信的技术,可以提高你的工作效率,同时提高应用程序交付的质量。
我上周看到了一条推文,作者说没有人需要 Postman/Insomnia 来测试端点。 我 100% 同意他的观点,在这篇文章中,我将通过实际示例和可以在日常工作中复制的项目向您展示原因。
为什么不用Postman/Insomnia测试端点
Postman 和 Insomnia 是非常有用的工具,可以帮助我们使用 Web API。 在日常工作中,通常会输入正在开发的项目的 URL 来验证应用程序返回的内容。
问题是这个过程变得重复。 更改代码,重新启动服务器,转到 Postman,更改将发送到 Web API 的参数,发送请求,然后检查结果。
然后,注意到 API 返回了错误的结果,与想要的结果非常不同。 因此,需要修改程序并重复整个过程。
这种恶性循环会让你浪费很多时间,因为重启服务器、更换工具和发送请求之间有几个步骤。
有一种更好的方法也可以提高软件质量。 你可以创建自动化测试,而不是使用这些工具手动测试应用程序。
通过自动化测试,这个过程就不会那么痛苦了。 定义你期望端点返回的内容,并且对于在代码中所做的每一个更改,测试套件都会重新启动,并且会立即在屏幕上看到结果
你不仅可以节省时间,还可以有其他的可能性。 因为所有端点都经过了正确测试,因此加入到持续集成流水线中。
持续集成是一个极其重要且广泛使用的过程。 想象一下以下情况:应用程序已经运行了几个月,一些疲惫的开发人员去了那里并上传了损坏的代码。
持续集成流水线将运行已经创建的测试并确认出现问题,从而阻止更新。
因此,你的用户将永远不会遇到应用程序中的严重错误或红屏,你可以睡个好觉,并且软件可以健康成长。
这就是我现在要向你展示的内容。 为了使其更具挑战性,我将采用带有一些端点的功能已经完备的应用程序并实施新的路线。
我将向你展示这个过程是如何在 Postman 中完成的,以及这个尝试和错误是多么痛苦。 我还将向你展示测试将如何提高你作为开发人员的生活质量。
重要提示:我今天要向你展示的测试技术称为端到端测试。 这意味着你将从不了解应用程序内部实现的用户的角度来测试应用程序。
这对于验证整个应用程序非常有用,但随着时间的推移,测试自动化可能需要很长时间才能运行,因此平衡它与单元测试非常重要。
使用 Postman - 了解问题
问题不在于使用 Postman 本身,正如我之前所说,这里的想法是让你可以在其中编写代码、获得即时反馈,并且仍然提供可维护的代码库。
使用 Postman 和类似工具时的工作流程
当我们使用此类工具时,我们通常遵循以下步骤:
- 编写服务器和一些Web API路由
- 转到postman并发出 HTTP 请求
- 修改后台代码
- 重新加载服务器
- 返回 Postman 并发送另一个请求
为了节省时间,可以在 Postman 上创建一些测试脚本来保存令牌并通过环境变量将其放在每个请求上,例如:
当你需要请求私有路由时,可以引用请求标头中的变量,例如:
不过,随着项目的增长,它会变得混乱,并且定义一堆 URL 和变量。
在应用程序界面来回检查日志、调试代码结果和验证工作流程让我们浪费了大量时间。
Postman 不是进行 E2E 测试的工具,据我所知,无法使用它来运行持续集成流程。
如果可以直接从软件开发环境立即看到结果并且仍然节省时间,该怎么办?
入门
我创建了一个小型代码模板,其中包含一些测试用例来模拟如何在现有应用程序上实现测试。
重要提示:这篇文章不关注最佳实践,目的是向你展示如何提高生产力并在现有应用程序上编写测试。
去那里克隆这个仓库(https://github.com/ErickWendel/postman-is-slowing-you-down/tree/template)。 该项目使用 Node.js v19,因此你将使用最新的 Node.js 功能来测试和监视热重载。
确保模板没问题
使用 npm ci 恢复依赖关系并确保在 v19.8 中使用 Node.js 后,使用 node -v run npm run test 确保一切都按预期工作。
你应该可以看到下面的输出:
npm run dev
> app@0.0.1 dev
> node --watch api.js
(node:20514) ExperimentalWarning: Watch mode is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
listening to 3000
让我们看看里面有什么。
// api.js
async function loginRoute(request, response) {
const { user, password } = JSON.parse(await once(request, "data"))
if (user !== VALID.user || password !== VALID.password) {
response.writeHead(400)
response.end(JSON.stringify({ error: 'user invalid!' }))
return
}
const token = jsonwebtoken.sign({ user, message: 'heyduude' }, TOKEN_KEY)
response.end(JSON.stringify({ token }))
}
该模板将对用户进行身份验证并返回 JWT 令牌,以便消费者使用它向私有路由发出请求。
查看模板上的代码,你会发现 /login 中定义了两条路由,并且尝试访问的任何其他路由将要么返回“未找到”(如果使用有效令牌请求),要么返回“无效令牌”错误(如果相反)。
// api.js
async function handler(request, response) {
if (request.url === '/login' && request.method === "POST") {
return loginRoute(request, response)
}
if (!validateHeaders(request.headers)) {
response.writeHead(400)
return response.end("invalid token!")
}
response.writeHead(404)
response.end('not found!')
}
你将为所有现有代码编写测试,然后实施一条新路线来创建产品。
了解业务需求
为了让事情变得更有趣,我将在下面定义你要实现的路由的要求:
POST /product 将从请求中接收产品的描述和价格,并返回该产品所属的类别。
如果产品价格高于 100,终端将响应高级类别
如果产品价格在50到99之间,终端将响应常规类别
如果产品价格低于50,终端将回复基本类别
产品路由是私有路由,这意味着如果消费者想要访问它,他们必须在请求标头上发送有效的 JWT 令牌。
设置测试环境
在 package.json 文件所在的同一文件夹中创建一个名为 api.test.js 的空文件。
前往 package.json 并添加脚本,如下所示:
"scripts": {
"dev": "node --watch api.js",
"test:dev": "node --watch --test api.test.js",
"test": "node --test api.test.js"
},
转到终端并运行 npm run test:dev 输出应如下所示:
npm run test:dev
> app@0.0.1 test:dev
> node --watch --test api.test.js
✔ /Users/erickwendel/Downloads/projetos/postman-is-slowing-you-down/template/api.test.js (38.097208ms)
准备就绪,可以开始创建测试了。
你需要在此测试中导入 app.js 文件,以便启动服务器并开始发出请求。
请注意,这里将使用监视模式,这意味着必须确保在重新启动代码之前停止服务器并防止两个程序实例尝试在同一端口中运行时出现错误。
在 api.test.js 文件中,粘贴以下代码:
import { describe, before, after, it } from 'node:test'
import { deepEqual, deepStrictEqual, ok } from 'node:assert'
const BASE_URL = `http://localhost:3000`
describe('API Products Test Suite', () => {
let _server = {}
let _globalToken = ''
before(async () => {
_server = (await import('./api.js')).app
await new Promise(resolve => _server.once('listening', resolve))
})
after(done => _server.close(done))
})
上面的代码将导入 API.js 文件并等待服务器准备好使用 before 函数接收请求。
使用 after 函数,你可以确保在执行整个测试套件后它将关闭服务器。
终端现在应该类似于以下输出:
npm run test:dev
> app@0.0.1 test:dev
> node --watch --test api.test.js
✔ /Users/erickwendel/Downloads/projetos/postman-is-slowing-you-down/template/api.test.js (38.097208ms)
ℹ listening to 3000
✔ API Products Test Suite (54.460625ms)
这意味着服务器正在启动并且一切都按预期工作。
编写端到端测试
由于除了登录路由之外的所有路由都需要通过 JWT 令牌,因此我们将添加一个新的之前步骤,该步骤将请求 API、获取令牌并将其存储在 _globalToken 变量中。
在描述块内粘贴以下代码:
async function makeRequest(url, data) {
const request = await fetch(url, {
method: 'POST',
body: JSON.stringify(data),
headers: {
authorization: _globalToken
}
})
deepEqual(request.status, 200)
return request.json()
}
async function setToken() {
const input = {
user: 'erickwendel',
password: '123'
}
const data = await makeRequest(`${BASE_URL}/login`, input)
ok(data.token, 'token should be present')
_globalToken = data.token
}
你正在使用 fetch,这是一个用于发出请求的全局函数。
过去,我们通常必须安装第三个库,例如 supertest,但不需要像仅使用 fetch 模块那样安装任何库。
在现有的 before 函数调用之后,将添加一个新的 before 函数来调用刚刚粘贴的 setToken 函数。
文件的末尾将如下所示:
before(async () => {
_server = (await import('./api.js')).app
await new Promise(resolve => _server.once('listening', resolve))
})
before(async () => setToken())
after(done => _server.close(done))
这将确保在到达端点之前始终填充 _globalToken 变量。
你还没有实现 /product 路线。 在实现之前,我们先进行测试。
it('it should create a premium product', async () => {
const input = {
description: "tooth brush",
price: 101
}
const data = await makeRequest(`${BASE_URL}/products`, input)
deepStrictEqual(data.category, "premium")
})
终端现在应该显示错误,例如:
▶ API Products Test Suite
✖ it should create a premium product (6.095959ms)
AssertionError: Expected values to be loosely deep-equal:
404
should loosely deep-equal
200
at makeRequest (file:///Users/erickwendel/Downloads/projetos/postman-is-slowing-you-down/template/api.test.js:15:5)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async TestContext.<anonymous> (file:///Users/erickwendel/Downloads/projetos/postman-is-slowing-you-down/template/api.test.js:39:18)
at async Test.run (node:internal/test_runner/test:548:9)
at async Promise.all (index 0)
at async Suite.run (node:internal/test_runner/test:801:7)
at async startSubtest (node:internal/test_runner/harness:192:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 404,
expected: 200,
operator: 'deepEqual'
}
▶ API Products Test Suite (86.56325ms)
现在将实施它! 让我们回到 api.js 文件并实现此路由。
首先,让我们创建具有上一节中列出的所有业务需求的函数:
// api.js
async function createProductRoute(request, response) {
const { description, price } = JSON.parse(await once(request, "data"))
const categories = {
premium: {
from: 101,
to: 500
},
regular: {
from: 51,
to: 100
},
basic: {
from: 0,
to: 50
},
}
const category = Object.keys(categories).find(key => {
const category = categories[key]
return price >= category.from && price app@0.0.1 test:dev
> node --watch --test api.test.js
ℹ listening to 3000
▶ API Products Test Suite
✔ it should create a premium product (4.854667ms)
▶ API Products Test Suite (79.295167ms)
现在是最简单的部分。 让我们对剩余的业务逻辑实现所有测试。 跳转到 api.test.js 并在最后一个之后粘贴下面的测试用例。
it('it should create a regular product', async () => {
const input = {
description: "escova de dente",
price: 70
}
const data = await makeRequest(`${BASE_URL}/products`, input)
deepStrictEqual(data.category, "regular")
})
it('it should create a basic product', async () => {
const input = {
description: "fio",
price: 2
}
const data = await makeRequest(`${BASE_URL}/products`, input)
deepStrictEqual(data.category, "basic")
})
所有测试都应该通过
▶ API Products Test Suite
✔ it should create a premium product (12.418709ms)
✔ it should create a regular product (3.384458ms)
✔ it should create a basic product (2.473792ms)
▶ API Products Test Suite (100.180334ms)
如果你在某些方面遇到困难,这里是 api.js 文件的完整代码
import jsonwebtoken from 'jsonwebtoken'
import { once } from 'node:events'
import { createServer } from 'node:http'
const VALID = {
user: 'erickwendel',
password: '123'
}
const TOKEN_KEY = "abc123"
async function loginRoute(request, response) {
const { user, password } = JSON.parse(await once(request, "data"))
if (user !== VALID.user || password !== VALID.password) {
response.writeHead(400)
response.end(JSON.stringify({ error: 'user invalid!' }))
return
}
const token = jsonwebtoken.sign({ user, message: 'heyduude' }, TOKEN_KEY)
response.end(JSON.stringify({ token }))
}
function validateHeaders(headers) {
try {
const auth = headers.authorization.replace(/bearer\s/ig, '')
jsonwebtoken.verify(auth, TOKEN_KEY)
return true
} catch (error) {
return false
}
}
async function createProductRoute(request, response) {
const { description, price } = JSON.parse(await once(request, "data"))
const categories = {
premium: {
from: 101,
to: 500
},
regular: {
from: 51,
to: 100
},
basic: {
from: 0,
to: 50
},
}
const category = Object.keys(categories).find(key => {
const category = categories[key]
return price >= category.from && price console.log('listening to 3000'))
export { app }
还有 api.test.js 文件
import { describe, before, after, it } from 'node:test'
import { deepEqual, deepStrictEqual, ok } from 'node:assert'
const BASE_URL = `http://localhost:3000`
describe('API Products Test Suite', () => {
let _server = {}
let _globalToken = ''
async function makeRequest(url, data) {
const request = await fetch(url, {
method: 'POST',
body: JSON.stringify(data),
headers: {
authorization: _globalToken
}
})
deepEqual(request.status, 200)
return request.json()
}
async function setToken() {
const input = {
user: 'erickwendel',
password: '123'
}
const data = await makeRequest(`${BASE_URL}/login`, input)
ok(data.token, 'token should be present')
_globalToken = data.token
}
before(async () => {
_server = (await import('./api.js')).app
await new Promise(resolve => _server.once('listening', resolve))
})
before(async () => setToken())
it('it should create a premium product', async () => {
const input = {
description: "tooth brush",
price: 101
}
const data = await makeRequest(`${BASE_URL}/products`, input)
deepStrictEqual(data.category, "premium")
})
it('it should create a regular product', async () => {
const input = {
description: "escova de dente",
price: 70
}
const data = await makeRequest(`${BASE_URL}/products`, input)
deepStrictEqual(data.category, "regular")
})
it('it should create a basic product', async () => {
const input = {
description: "fio",
price: 2
}
const data = await makeRequest(`${BASE_URL}/products`, input)
deepStrictEqual(data.category, "basic")
})
after(done => _server.close(done))
})
总结
你将开始看到收益,因为对代码所做的任何更改都会立即反映在你的终端上。 不需要来回去找postman或类似的工具。设置测试环境后,可以复制并粘贴测试用例并更改输入和预期结果。你可以节省大量时间,任何想要了解哪些端点以及如何访问它们的新开发人员都可以访问测试环境并测试整个代码。当你编写测试时,需要添加一个持续集成步骤,例如 GitHub Actions。这对于测试所有端点并检查新代码或新版本是否会破坏应用程序的任何部分并防止它在用户面前崩溃非常重要。