Skip to main content

REST API Testing

See how to create tests for REST APIs applications in Athenna.

Introduction

Athenna provides a very fluent API for making HTTP requests to your application and examining the responses. For example, take a look at the e2e test defined below:

import { Test, type Context } from '@athenna/test'
import { BaseHttpTest } from '@athenna/core/testing/BaseHttpTest'

export default class ExampleTest extends BaseHttpTest {
@Test()
public async 'test successful response'({ request }: Context) {
const response = await request.get('/')

response.assertStatusCode(200)
}
}

The request.get() method makes a GET request into the application, while the assertStatusCode() method asserts that the returned response should have the given HTTP status code. In addition to this simple assertion, Athenna also contains a variety of assertions for inspecting the response headers, content, JSON structure, and more.

You might have noticed that the ExampleTest is extending the BaseHttpTest class. We gonna see later on this documentation what is the purpose of this class, and how to configure it for your needs.

Registering request plugin

The request property in your test context will only be available if you register the request plugin within the Runner class. By default, your Athenna application already comes with the request plugin registered. But we are going to cover how to register it manually if needed.

Just call the Runner.addPlugin() static method to setup the request plugin imported from @athenna/http/testing/plugins:

Path.bin('test.ts')
import { Runner } from '@athenna/test'
import { request } from '@athenna/http/testing/plugins'
import { command } from '@athenna/artisan/testing/plugins'

await Runner.setTsEnv()
.setAppEnv('test')
.addAssertPlugin()
.addPlugin(request()) 👈
.addPlugin(command())
.addPath('tests/e2e/**/*.ts')
.addPath('tests/unit/**/*.ts')
.setCliArgs(process.argv.slice(2))
.setGlobalTimeout(5000)
.run()

Making Requests

To make a request to your application, you may invoke the request.get(), request.post(), request.put(), request.patch(), or request.delete() methods within your test. These methods do not actually issue a "real" HTTP request to your application. Instead, the entire network request is simulated internally.

Instead of returning a response instance, test request methods return an instance of TestResponse, which provides a variety of helpful assertions that allow you to inspect your application's responses:

import { Test, type Context } from '@athenna/test'
import { BaseHttpTest } from '@athenna/core/testing/BaseHttpTest'

export default class ExampleTest extends BaseHttpTest {
@Test()
public async testBasicRequest({ request }: Context) {
const response = await request.get('/')

response.assertStatusCode(200)
}
}

Customizing request

All the request methods accept a second argument where you can setup the request options:

import { type InjectOptions } from '@athenna/http'
import { Test, type Context } from '@athenna/test'
import { BaseHttpTest } from '@athenna/core/testing/BaseHttpTest'

export default class ExampleTest extends BaseHttpTest {
@Test()
public async testBasicRequest({ request }: Context) {
const options: InjectOptions = {
headers: {
'X-Header': 'value'
},
body: {
foo: 'bar'
}
}

const response = await request.get('/', options)

response.assertStatusCode(200)
}
}

Debugging responses

After making a test request to your application, the response returned will contain the response property inside with all the response data:

import { Test, type Context } from '@athenna/test'
import { BaseHttpTest } from '@athenna/core/testing/BaseHttpTest'

export default class ExampleTest extends BaseHttpTest {
@Test()
public async testBasicRequest({ request }: Context) {
const response = await request.get('/')

console.log(response.response.status)

response.assertStatusCode(200)
}
}

Testing file uploads

Coming soon with @athenna/storage package!

Response assertions

Athenna's TestResponse class provides a variety of custom assertion methods that you may utilize when testing your application. These assertions may be accessed on the response that is returned by the request.get(), request.post(), request.put(), request.patch(), or request.delete() test methods:

assertStatusCode()

Assert that the response has a given HTTP status code:

const statusCode = 200

response.assertStatusCode(statusCode)
response.assertIsNotStatusCode(statusCode)

assertBodyContains()

Assert that the response body contains some of the given values:

const partialBody = { hello: 'world' } 

response.assertBodyContains(partialBody)
response.assertBodyNotContains(partialBody)

assertBodyContainsKey()

Assert that the response body contains a key with the given name:

const key = 'hello'

response.assertBodyContainsKey(key)
response.assertBodyNotContainsKey(key)

assertBodyContainsAllKeys()

Assert that the response body contains all the keys with given names:

const keys = ['hello']

response.assertBodyContainsAllKeys(keys)
response.assertBodyNotContainsAllKeys(keys)

assertBodyDeepEqual()

Assert that the response body is deep equal and exactly like the given value:

const fullBody = [{ hello: 'world' }]

response.assertBodyDeepEqual(fullBody)
response.assertBodyNotDeepEqual(fullBody)

assertBodyIsArray()

Assert that the response body is an array:

response.assertBodyIsArray()
response.assertBodyIsNotArray()

assertBodyIsObject()

Assert that the response body is an object:

response.assertBodyIsObject()
response.assertBodyIsNotObject()

assertHeaderContains()

Assert that the response header contains some of the given values:

const partialHeader = { hello: 'world' } 

response.assertHeaderContains(partialHeader)
response.assertHeaderNotContains(partialHeader)

assertHeaderContainsKey()

Assert that the response header contains a key with the given name:

const key = 'hello'

response.assertHeaderContainsKey(key)
response.assertHeaderNotContainsKey(key)

assertHeaderContainsAllKeys()

Assert that the response header contains all the keys with given names:

const keys = ['hello']

response.assertHeaderContainsAllKeys(keys)
response.assertHeaderNotContainsAllKeys(keys)

assertHeaderDeepEqual()

Assert that the response header is deep equal and exactly like the given value:

const fullHeader = [{ hello: 'world' }]

response.assertHeaderDeepEqual(fullHeader)
response.assertHeaderNotDeepEqual(fullHeader)

The BaseHttpTest class

The BaseHttpTest class is responsible to bootstrap your Athenna application before running all tests and also to kill the application after running all tests, meaning that is not possible to use the request property without extending this class or at least setting up your own Athenna application using Ignite class.

If for some reason you need to change the options set when calling the Ignite.load() or Ignite.httpServer() methods, you can set the igniteOptions and httpOptions properties in your test class:

import { Path } from '@athenna/common'
import { Test, type Context } from '@athenna/test'
import { BaseHttpTest } from '@athenna/core/testing/BaseHttpTest'
import { type HttpOptions, type IgniteOptions } from '@athenna/core'

export default class ExampleTest extends BaseHttpTest {
public httpOptions: HttpOptions = {
host: '0.0.0.0',
port: 9999,
trace: true,
routePath: Path.fixtures('routes/routes.ts'),
kernelPath: Path.fixtures('kernels/Kernel.ts'),
exceptionHandlerPath: Path.fixtures('handlers/Handler.ts')
}

public igniteOptions: IgniteOptions = {
bootLogs: true,
shutdownLogs: true,
envPath: Path.fixtures('envs/.env'),
athennaRcPath: Path.fixtures('rcs/.athennarc.json'),
environments: ['http', 'test']
}

@Test()
public async 'test successful response'({ request }: Context) {
const response = await request.get('/')

response.assertStatusCode(200)
}
}

Accessing Ignite and Http instance

You are able to access the Ignite and Http instances by using the ignite and httpServer properties:

import { Test, type Context } from '@athenna/test'
import { BaseHttpTest } from '@athenna/core/testing/BaseHttpTest'

export default class ExampleTest extends BaseHttpTest {
@Test()
public async 'test successful response'({ request }: Context) {
this.ignite 👈
this.httpServer 👈
}
}