HTTP tests

HTTP tests

HTTP tests refer to testing your application endpoints by making an actual HTTP request against them and asserting the response body, headers, cookies, session, etc.

HTTP tests are performed using the API client plugin of Japa. The API client plugin is a stateless request library similar to Axios or fetch but more suited for testing.

If you want to test your web apps inside a real browser and interact with them programmatically, we recommend using the Browser client that uses Playwright for testing.

Setup

The first step is to install the following packages from the npm packages registry.

npm i -D @japa/api-client

Registering the plugin

Before moving forward, register the plugin inside the tests/bootstrap.ts file.

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

The apiClient method optionally accepts the baseURL for the server. If not provided, it will use the HOST and the PORT environment variables.

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

Basic example

Once the apiClient plugin is registered, you may access the client object from TestContext to make an HTTP request.

The HTTP tests must be written inside the folder configured for the functional tests suite. You may use the following command to create a new test file.

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

To view all the available request and assertion methods, make sure to go through the Japa documentation.

Open API testing

The assertion and API client plugins allow you to use Open API spec files for writing assertions. Instead of manually testing the response against a fixed payload, you may use a spec file to test the shape of the HTTP response.

It is a great way to keep your Open API spec and server responses in sync. Because if you remove a certain endpoint from the spec file or change the response data shape, your tests will fail.

Registering schema

AdonisJS does not offer tooling for generating Open API schema files from code. You may write it by hand or use graphical tools to create it.

Once you have a spec file, save it inside the resources directory (create the directory if missing) and register it with the assert plugin within the tests/bootstrap.ts file.

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

Writing assertions

Once the schema is registered, you can use the response.assertAgainstApiSpec method to assert against the API spec.

test('paginate posts', async ({ client }) => {
const response = await client.get('/posts')
response.assertAgainstApiSpec()
})
  • The response.assertAgainstApiSpec method will use the request method, the endpoint, and the response status code to find the expected response schema.
  • An exception will be raised when the response schema cannot be found. Otherwise, the response body will be validated against the schema.

Only the response's shape is tested, not the actual values. Therefore, you may have to write additional assertions. For example:

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

Reading/writing cookies

You may send cookies during the HTTP request using the withCookie method. The method accepts the cookie name as the first argument and the value as the second.

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

The withCookie method defines a singed cookie. In addition, you may use the withEncryptedCookie or withPlainCookie methods to send other types of cookies to the server.

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

Reading cookies from the response

You may access the cookies set by your AdonisJS server using the response.cookies method. The method returns an object of cookies as a key-value pair.

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

You may use the response.cookie method to access a single cookie value by its name. Or use the assertCookie method to assert the cookie value.

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

Populating session store

If you are using the @adonisjs/session package to read/write session data in your application, you may also want to use the sessionApiClient plugin to populate the session store when writing tests.

Setup

The first step is registering the plugin inside the tests/bootstrap.ts file.

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

And then, update the .env.test file (create one if it is missing) and set the SESSON_DRIVER to memory.

.env.test
SESSION_DRIVER=memory

Making requests with session data

You may use the withSession method on the Japa API client to make an HTTP request with some pre-defined session data.

The withSession method will create a new session ID and populate the memory store with the data, and your AdonisJS application code can read the session data as usual.

After the request finishes, the session ID and its data will be destroyed.

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',
}
]
})
})

Like the withSession method, you may use the withFlashMessages method to set flash messages when making an HTTP request.

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

Reading session data from the response

You may access the session data set by your AdonisJS server using the response.session() method. The method returns the session data as an object of a key-value pair.

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

You may read flash messages from the response using the response.flashMessage or response.flashMessages method.

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

Finally, you may write assertions for the session data using one of the following methods.

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')

Authenticating users

If you use the @adonisjs/auth package to authenticate users in your application, you may use the authApiClient Japa plugin to authenticate users when making HTTP requests to your application.

The first step is registering the plugin inside the tests/bootstrap.ts file.

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

If you are using session-based authentication, then make sure to switch the session driver to an in-memory store.

.env.test
SESSION_DRIVER=memory

That's all. Now, you may login users using the loginAs method. The method accepts the user object as the only argument and marks the user as logged in for the current HTTP request.

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

The loginAs method uses the default guard configured inside the config/auth.ts file for authentication. However, you may specify a custom guard using the withGuard method. For example:

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

Making a request with a CSRF token

If forms in your application use CSRF protection, you may use the withCsrfToken method to generate a CSRF token and pass it as a header during the request.

Before using the withCsrfToken method, register the following Japa plugins inside the tests/bootstrap.ts file and also make sure to switch the SESSION_DRIVER env variable to 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()
})

The route helper

You may use the route helper from the TestContext to create a URL for a route. Using the route helper ensures that whenever you update your routes, you do not have to come back and fix all the URLs inside your tests.

The route helper accepts the same set of arguments accepted by the global template method route.

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