Testing fakes and Helpers

Quite often you want to fake out the original implementation of certain parts of your application when writing tests. Since AdonisJs leverages an IoC container to manage dependencies, it becomes so easy to create fake implementations when writing tests

Self-implementing fakes

Let’s start with a basic example of faking out a service which sends an email to a given user.

Creating too many fakes in testing may lead to false tests, where all your are testing is the syntax and not the implementation.
Always make sure to keep fakes as the last option when writing tests.
app/Services/UserRegistration
class UserRegistration {

  async sendVerificationEmail (user) {
    await Mail.send('emails.verify', user, (message) => {
      message.to(user.email)
      message.subject('Verify account')
    })
  }
}

Now let’s say this service is used by the UserController, when testing the user registration, a bunch of false emails is sent to some email address.

To avoid this behavior, it makes sense to fake out the UserRegistration service.

const { ioc } = use('@adonisjs/fold')
const { test } = use('Test/Suite')('User registration')

test('register user', async () => {
  ioc.fake('App/Services/UserRegistration', () => {
    return {
      sendVerificationEmail () {}
    }
  })

  // code to test user registration
  // ....

  ioc.restore('App/Services/UserRegistration')
})

The ioc.fake method let you bind a value to the IoC container, and when any part of the application tries to resolve the namespace, the fake value is return over the actual value.

In the same way, we need to call ioc.restore to remove the fake.

This approach works great for a majority of use cases until you can create a fake which is similar to the actual implementation. For greater control, you can make use of external libraries like sinonjs.

Mail fake

AdonisJs mail provider comes with a fake, which can be used when writing tests.

const Mail = use('Mail')
const { test } = use('Test/Suite')('User registration')

test('register user', async ({ assert }) => {
  Mail.fake()

  // write your test

  const recentEmail = Mail.pullRecent()
  assert.equal(recentEmail.to.address, '[email protected]')
  assert.equal(recentEmail.to.name, 'Joe')

  Mail.restore()
})

Calling Mail.fake method binds a fake to the IoC container. From this point, all emails are stored in memory as an array of objects, which can later be used for running assertions.

Below is the list of available methods on a fake mailer.

recent()

Returns the recent email object

Mail.recent()

pullRecent()

Returns the recent email object and removes it from the in-memory array.

Mail.pullRecent()

all()

Returns all emails

assert.lengthof(Mail.all(), 1)

clear()

Clear the in-memory emails array

Mail.clear()

restore()

Restore the original emailer class

Mail.restore()

Events fake

Just like Mail provider, Email provider also comes with an in-built faker object to store events inside an in-memory array and use them later for assertions.

const Event = use('Event')
const { test } = use('Test/Suite')('User registration')

test('register user', async ({ assert }) => {
  Event.fake()

  // write your test
  ....

  const recentEvent = Event.pullRecent()
  assert.equal(recentEvent.event, 'register:user')

  Event.restore()
})

Alternatively, you can trap an event inline and run assertions inside the callback.

test('register user', async ({ assert }) => {
  assert.plan(2)
  Event.fake()

  Event.trap('register:user', function (data) {
    assert.equal(data.username, 'joe')
    assert.equal(data.email, '[email protected]')
  })

  // write your test
  ....

  Event.restore()
})

Here is the list of all available methods

recent()

Returns the recent event object

Event.recent()

pullRecent()

Returns the recent event object and removes it from the in-memory array.

Event.pullRecent()

all()

Returns all events

Event.all()

clear()

Clear in-memory array of events

Event.clear()

restore()

Restore the original event class

Event.restore()

Database transactions

A struggle to keep the database clean for each test is quite hard. You may end up using lifecycle hooks to truncate the tables after each test.

To make this process easy, AdonisJs ships with a database transaction trait, which wraps all of your databases queries inside a transaction and roll it back after each test.

const { test, trait } = use('Test/Suite')('User registration')

trait('DatabaseTransactions')

That is all 😊