Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Testing with Japa

A test is code that checks by itself that the application works: it opens a page, submits a form and compares the response with what is expected. After each change you run the tests instead of clicking through the site by hand. AdonisJS has the Japa test framework built in, and the project comes with it configured.

Where tests live

tests/
├── bootstrap.ts       Japa plugin setup
├── functional/        tests of pages and the API over HTTP
└── unit/              tests of single classes and functions

The test suites are described in adonisrc.ts. Functional tests start a real HTTP server and send requests to it, like a browser.

The first test

node ace make:test posts --suite=functional

The file tests/functional/posts.spec.ts appears:

// tests/functional/posts.spec.ts
import { test } from '@japa/runner'
 
test.group('Posts', () => {
  test('shows the list of posts', async ({ client }) => {
    const response = await client.get('/posts')
 
    response.assertStatus(200)
    response.assertTextIncludes('All posts')
  })
})

test.group combines tests on one topic. The client object sends requests to the application, and the assert... methods check the response.

Running

node ace test

Japa prints the list of tests with pass marks and the run time. To run only one suite, use node ace test functional.

Data in tests

A test should create the data it needs and remove it afterwards. For this every test is wrapped in a transaction that is rolled back:

// tests/functional/posts.spec.ts
import { test } from '@japa/runner'
import testUtils from '@adonisjs/core/services/test_utils'
import { PostFactory } from '#database/factories/post_factory'
 
test.group('Posts', (group) => {
  group.each.setup(() => testUtils.db().withGlobalTransaction())
 
  test('shows a post', async ({ client }) => {
    const post = await PostFactory.merge({ title: 'Test post' }).create()
 
    const response = await client.get(`/posts/${post.id}`)
    response.assertTextIncludes('Test post')
  })
})

merge gives the factory specific values. After the test the post disappears with the rolled-back transaction.

Testing as a user

// tests/functional/posts.spec.ts
import { UserFactory } from '#database/factories/user_factory'
 
test('a guest cannot open the form', async ({ client }) => {
  const response = await client.get('/posts/create').redirects(0)
  response.assertStatus(302)
})
 
test('an author sees the form', async ({ client }) => {
  const user = await UserFactory.create()
  const response = await client.get('/posts/create').loginAs(user)
  response.assertStatus(200)
})

loginAs sends the request on behalf of the user, and redirects(0) stops the client from following the redirect so you can check the 302 response itself. UserFactory is created the same way as the post factory. loginAs works when the sessionApiClient and authApiClient plugins are registered in tests/bootstrap.ts.

Testing the API

// tests/functional/api_posts.spec.ts
test('the API returns posts', async ({ client, assert }) => {
  const response = await client.get('/api/posts').header('Accept', 'application/json')
  response.assertStatus(200)
  assert.isArray(response.body())
})

response.body() returns the parsed JSON, and the assert object from the Japa plugin checks that it is an array.

Common mistake

Tests run against the same database as development, and they wipe or clutter your data. Set a separate database in .env.test — for example, another SQLite file or a blog_test database. node ace test loads this file on top of .env.

Thanks for reading!

Donate to the site — we work hard for you.