HTTP 测试

HTTP 测试

HTTP 测试指的是通过向应用端点发起真实的 HTTP 请求,并对响应体、请求头、cookie、会话等进行断言,从而测试这些端点。

HTTP 测试使用 Japa 的 API client 插件 来执行。API client 插件是一个无状态的请求库,类似于 Axiosfetch,但更适合用于测试。

如果你想在真实的浏览器中测试你的 Web 应用并以编程方式与之交互,我们建议使用基于 Playwright 进行测试的 Browser client

安装配置

第一步是从 npm 包注册表安装以下软件包。

npm i -D @japa/api-client

注册插件

在继续之前,请在 tests/bootstrap.ts 文件中注册该插件。

tests/bootstrap.ts
import { apiClient } from '@japa/api-client'
export const plugins: Config['plugins'] = [
assert(),
apiClient(),
pluginAdonisJS(app),
]

apiClient 方法可选地接受服务器的 baseURL。如果未提供,它将使用 HOSTPORT 环境变量。

import env from '#start/env'
export const plugins: Config['plugins'] = [
apiClient({
baseURL: `http://${env.get('HOST')}:${env.get('PORT')}`
})
]

基础示例

一旦注册了 apiClient 插件,你就可以从 TestContext 访问 client 对象来发起 HTTP 请求。

HTTP 测试必须编写在为 functional 测试套件配置的文件夹内。你可以使用以下命令创建一个新的测试文件。

node ace make:test users/list --suite=functional
import { test } from '@japa/runner'
test.group('Users list', () => {
test('get a list of users', async ({ client }) => {
const response = await client.get('/users')
response.assertStatus(200)
response.assertBody({
data: [
{
id: 1,
email: '[email protected]',
}
]
})
})
})

要查看所有可用的请求和断言方法,请务必通读 Japa 文档

Open API 测试

断言插件和 API client 插件允许你使用 Open API 规范文件来编写断言。你可以使用规范文件来测试 HTTP 响应的结构,而不必手动针对固定的 payload 测试响应。

这是一种保持 Open API 规范与服务器响应同步的好方法。因为如果你从规范文件中移除了某个端点,或更改了响应的数据结构,你的测试就会失败。

注册 schema

AdonisJS 不提供从代码生成 Open API schema 文件的工具。你可以手写,也可以使用图形化工具来创建。

一旦有了规范文件,将其保存在 resources 目录中(如果该目录不存在则创建它),并在 tests/bootstrap.ts 文件中使用 openapi-assertions 插件注册它。

npm i -D @japa/openapi-assertions
tests/bootstrap.ts
import app from '@adonisjs/core/services/app'
import { openapi } from '@japa/openapi-assertions'
export const plugins: Config['plugins'] = [
assert(),
openapi({
schemas: [app.makePath('resources/open_api_schema.yaml')]
})
apiClient(),
pluginAdonisJS(app)
]

编写断言

一旦注册了 schema,你就可以使用 response.assertAgainstApiSpec 方法针对 API 规范进行断言。

test('paginate posts', async ({ client }) => {
const response = await client.get('/posts')
response.assertAgainstApiSpec()
})
  • response.assertAgainstApiSpec 方法将使用 请求方法端点 以及 响应状态码 来查找预期的响应 schema。
  • 当找不到响应 schema 时会抛出异常。否则,响应体将根据该 schema 进行校验。

它只测试响应的结构,而不测试实际的值。因此,你可能需要编写额外的断言。例如:

// Assert that the response is as per the schema
response.assertAgainstApiSpec()
// Assert for expected values
response.assertBodyContains({
data: [{ title: 'Adonis 101' }, { title: 'Lucid 101' }]
})

你可以使用 withCookie 方法在 HTTP 请求期间发送 cookie。该方法接受 cookie 名称作为第一个参数,值作为第二个参数。

await client
.get('/users')
.withCookie('user_preferences', { limit: 10 })

withCookie 方法定义的是一个签名 cookie。此外,你还可以使用 withEncryptedCookiewithPlainCookie 方法向服务器发送其他类型的 cookie。

await client
.get('/users')
.withEncryptedCookie('user_preferences', { limit: 10 })
await client
.get('/users')
.withPlainCookie('user_preferences', { limit: 10 })

你可以使用 response.cookies 方法访问 AdonisJS 服务器设置的 cookie。该方法以键值对对象的形式返回 cookie。

const response = await client.get('/users')
console.log(response.cookies())

你可以使用 response.cookie 方法通过名称访问单个 cookie 的值。或者使用 assertCookie 方法断言 cookie 的值。

const response = await client.get('/users')
console.log(response.cookie('user_preferences'))
response.assertCookie('user_preferences')

填充会话存储

如果你使用 @adonisjs/session 包在应用中读写会话数据,那么在编写测试时你可能还想使用 sessionApiClient 插件来填充会话存储。

安装配置

第一步是在 tests/bootstrap.ts 文件中注册该插件。

tests/bootstrap.ts
import { sessionApiClient } from '@adonisjs/session/plugins/api_client'
export const plugins: Config['plugins'] = [
assert(),
pluginAdonisJS(app),
sessionApiClient(app)
]

然后,更新 .env.test 文件(如果不存在则创建一个),并将 SESSON_DRIVER 设置为 memory

.env.test
SESSION_DRIVER=memory

携带会话数据发起请求

你可以在 Japa API client 上使用 withSession 方法,携带一些预定义的会话数据发起 HTTP 请求。

withSession 方法将创建一个新的会话 ID,并用数据填充内存存储,你的 AdonisJS 应用代码可以像往常一样读取会话数据。

请求完成后,该会话 ID 及其数据将被销毁。

test('checkout with cart items', async ({ client }) => {
await client
.post('/checkout')
.withSession({
cartItems: [
{
id: 1,
name: 'South Indian Filter Press Coffee'
},
{
id: 2,
name: 'Cold Brew Bags',
}
]
})
})

withSession 方法类似,你可以使用 withFlashMessages 方法在发起 HTTP 请求时设置闪存消息。

const response = await client
.get('posts/1')
.withFlashMessages({
success: 'Post created successfully'
})
response.assertTextIncludes(`Post created successfully`)

从响应中读取会话数据

你可以使用 response.session() 方法访问 AdonisJS 服务器设置的会话数据。该方法以键值对对象的形式返回会话数据。

const response = await client
.post('/posts')
.json({
title: 'some title',
body: 'some description',
})
console.log(response.session()) // all session data
console.log(response.session('post_id')) // value of post_id

你可以使用 response.flashMessageresponse.flashMessages 方法从响应中读取闪存消息。

const response = await client.post('/posts')
response.flashMessages()
response.flashMessage('errors')
response.flashMessage('success')

最后,你可以使用以下方法之一为会话数据编写断言。

const response = await client.post('/posts')
/**
* Assert a specific key (with optional value) exists
* in the session store
*/
response.assertSession('cart_items')
response.assertSession('cart_items', [{
id: 1,
}, {
id: 2,
}])
/**
* Assert a specific key is not in the session store
*/
response.assertSessionMissing('cart_items')
/**
* Assert a flash message exists (with optional value)
* in the flash messages store
*/
response.assertFlashMessage('success')
response.assertFlashMessage('success', 'Post created successfully')
/**
* Assert a specific key is not in the flash messages store
*/
response.assertFlashMissing('errors')
/**
* Assert for validation errors in the flash messages
* store
*/
response.assertHasValidationError('title')
response.assertValidationError('title', 'Enter post title')
response.assertValidationErrors('title', [
'Enter post title',
'Post title must be 10 characters long.'
])
response.assertDoesNotHaveValidationError('title')

认证用户

如果你使用 @adonisjs/auth 包在应用中认证用户,那么在向应用发起 HTTP 请求时,你可以使用 authApiClient Japa 插件来认证用户。

第一步是在 tests/bootstrap.ts 文件中注册该插件。

tests/bootstrap.ts
import { authApiClient } from '@adonisjs/auth/plugins/api_client'
export const plugins: Config['plugins'] = [
assert(),
pluginAdonisJS(app),
authApiClient(app)
]

如果你使用基于会话的身份认证,请务必同时设置好 session 插件。参见填充会话存储 - 安装配置

这样就可以了。现在,你可以使用 loginAs 方法登录用户。该方法接受用户对象作为唯一参数,并将该用户标记为在当前 HTTP 请求中已登录。

import User from '#models/user'
test('get payments list', async ({ client }) => {
const user = await User.create(payload)
await client
.get('/me/payments')
.loginAs(user)
})

loginAs 方法使用 config/auth.ts 文件中配置的默认守卫进行身份认证。不过,你可以使用 withGuard 方法指定自定义守卫。例如:

await client
.get('/me/payments')
.withGuard('api_tokens')
.loginAs(user)

携带 CSRF token 发起请求

如果你应用中的表单使用了 CSRF 保护,你可以使用 withCsrfToken 方法生成一个 CSRF token 并在请求期间将其作为请求头传递。

在使用 withCsrfToken 方法之前,请在 tests/bootstrap.ts 文件中注册以下 Japa 插件,同时确保SESSION_DRIVER 环境变量切换memory

tests/bootstrap.ts
import { shieldApiClient } from '@adonisjs/shield/plugins/api_client'
import { sessionApiClient } from '@adonisjs/session/plugins/api_client'
export const plugins: Config['plugins'] = [
assert(),
pluginAdonisJS(app),
sessionApiClient(app),
shieldApiClient()
]
test('create a post', async ({ client }) => {
await client
.post('/posts')
.form(dataGoesHere)
.withCsrfToken()
})

route 辅助函数

你可以使用 TestContext 中的 route 辅助函数为某个路由创建 URL。使用 route 辅助函数可确保每当你更新路由时,无需回过头去修复测试中所有的 URL。

route 辅助函数接受与全局模板方法 route 相同的一组参数。

test('get a list of users', async ({ client, route }) => {
const response = await client.get(
route('users.list')
)
response.assertStatus(200)
response.assertBody({
data: [
{
id: 1,
email: '[email protected]',
}
]
})
})