permalink | title | category |
---|---|---|
testing |
Getting Started |
testing |
Manually testing your application by visiting each webpage or API endpoint can be tedious, and sometimes even impossible.
Automated testing is the preferred strategy to confirm your application continues to behave as expected as you make changes to your codebase.
In this guide, we learn about the benefits of testing and different ways to test your application’s code.
If you are new to testing, you may find it hard to understand the benefits.
Once you get into the habit of writing tests, your code quality and confidence about your code’s behavior should improve drastically.
Testing is divided into multiple categories, encouraging you to write different types of test cases with clear boundaries.
These test categories include:
Unit tests are written to test small pieces of code in isolation.
For example, you might test a class directly without worrying how that class is used in the real world:
const { test } = use('Test/Suite')('Example unit test')
const UserValidator = use('App/Services/UserValidator')
test('validate user details', async ({ assert }) => {
const validation = await UserValidator.validate({
email: 'wrong email'
})
assert.isTrue(validation.fails())
assert.deepEqual(validation.messages(), [
{
field: 'email',
message: 'Invalid user email address'
}
])
})
Functional tests are written to test your application like an end-user.
For example, you might programmatically open a browser and interact with various webpages to ensure they work as intended:
const { test, trait } = use('Test/Suite')('Example functional test')
trait('Test/Browser')
test('validate user details', async ({ browser }) => {
const page = await browser.visit('/')
await page
.type('email', 'wrong email')
.submitForm('form')
.waitForNavigation()
page.session.assertError('email', 'Invalid user email address')
})
Both the above test examples validate the email address for a given user, but the approach is different based on the type of test you are writing.
As the Vow Provider is not installed by default, we need to pull it from npm
:
> adonis install @adonisjs/vow
Next, register the provider in the start/app.js
file aceProviders
array:
const aceProviders = [
'@adonisjs/vow/providers/VowProvider'
]
Note
|
The provider is registered inside the aceProviders array since we do not want to boot the testing engine when running your app in production.
|
Installing @adonisjs/vow
creates the following files and directory:
vowfiles.js
is loaded before your tests are executed, and is used to define tasks that should occur before/after running all tests.
env.testing
contains the environment variables used when running tests. This file gets merged with .env
, so you only need to define values you want to override from the .env
file.
All application tests are stored inside subfolders of the test
directory. An example unit test is added to this directory when @adonisjs/vow
is installed:
'use strict'
const { test } = use('Test/Suite')('Example')
test('make sure 2 + 2 is 4', async ({ assert }) => {
assert.equal(2 + 2, 4)
})
Installing the Vow Provider creates an example unit test for you, which can be executed by running the following command:
> adonis test
Example
✓ make sure 2 + 2 is 4 (2ms)
PASSED
total : 1
passed : 1
time : 6ms
Before we dive into writing tests, let’s understand some fundamentals which are important to understanding the flow of tests.
Each file is a test suite, defining a group of tests with similar behavior.
For example, we can have a suite of tests for user registration:
const Suite = use('Test/Suite')('User registeration')
// or destructuring
const { test } = use('Test/Suite')('User registeration')
The test
function obtained from the Suite
instance is used to define tests:
test('return error when credentials are wrong', async (ctx) => {
// implementation
})
To avoid bloating the test runner with unnecessary functionality, AdonisJs ships different pieces of code as traits (the building blocks for your test suite).
For example, we call the Test/Browser
trait so we can test via web browser:
const { test, trait } = use('Test/Suite')('User registeration')
trait('Test/Browser')
test('return error when credentials are wrong', async ({ browser }) => {
const page = await browser.visit('/user')
})
Note
|
In the example above, if we were to remove the Test/Browser trait, the browser object would be undefined inside our tests.
|
You can define custom traits with a closure or IoC container binding:
const { test, trait } = use('Test/Suite')('User registeration')
trait(function (suite) {
suite.Context.getter('foo', () => {
return 'bar'
})
})
test('foo must be bar', async ({ foo, assert }) => {
assert.equal(foo, 'bar')
})
Note
|
Traits are helpful when you want to bundle a package to be used by others, though for most situations, you could simply use Lifecycle Hooks instead. |
Each test has an isolated context.
By default, the context has only one property called assert
which is an instance of chaijs/assert to run assertions.
You can pass custom values to each test context by defining getters or macros to be accessed inside the test
callback closure (see the Traits closure example).
Each suite has lifecycle hooks which can be used to perform repetitive tasks (for example, cleaning the database after each test):
const Suite = use('Test/Suite')('User registeration')
const { before, beforeEach, after, afterEach } = Suite
before(async () => {
// executed before all the tests for a given suite
})
beforeEach(async () => {
// executed before each test inside a given suite
})
after(async () => {
// executed after all the tests for a given suite
})
afterEach(async () => {
// executed after each test inside a given suite
})
The assert
object is an instance of chaijs/assert, passed to each test as a property of the test
callback context.
To make your tests more reliable, you can also plan assertions to be executed for a given test. Let’s consider this example:
test('must throw exception', async ({ assert }) => {
try {
await badOperation()
} catch ({ message }) {
assert.equal(message, 'Some error message')
}
})
The above test passes even if an exception was never thrown and no assertions were run. This is a bad test, passing only because we structured it poorly.
To overcome this scenario, plan
for your expected number of assertions:
test('must throw exception', async ({ assert }) => {
assert.plan(1)
try {
await badOperation()
} catch ({ message }) {
assert.equal(message, 'Some error message')
}
})
In the above example, if badOperation
doesn’t throw an exception, the test still fails since we planned for 1
assertion and 0
were made.