Skip to main content

Mocking

Understand how to mock dependencies and functions in Athenna.

Introduction

When testing Athenna applications, you may wish to "mock" certain aspects of your application, so they are not executed during a given test. For example, when testing a controller that calls a service, you may wish to mock the service, so they are not executed during the test. This allows you to only test the controller's HTTP response without worrying about the execution of the service since the service can be tested in their own test case.

This API uses sinon library under the hood to integrate with the Athenna ecosystem. If you need to create more specific mocks, we recommend you to check their documentation.

Mocking objects

The most convenient way to mock an object and change it behavior is by using the Mock::when() method:

import { Test, Mock, type Context } from '@athenna/test'

export default class MockTest {
public object = {
foo: () => undefined
}

@Test()
public async mockAnObjectMethod({ assert }: Context) {
Mock.when(this.object, 'foo').return('bar') 👈

assert.equal(this.object.foo(), 'bar')
}
}
tip

The Mock::when() method is a shorthand for Mock::stub(). If you want to learn more about stubs and their API, check the stub documentation from sinon.

You can also use this method to throw an error when the given method is called:

@Test()
public async mockAnObjectMethod({ assert }: Context) {
Mock.when(this.object, 'foo').throw(new Error()) 👈

assert.throw(() => this.object.foo(), Error)
}

For promises, you can use the resolve() or reject() methods:

@Test()
public async mockAnObjectMethod({ assert }: Context) {
// Resolving a promise
Mock.when(this.object, 'foo').resolve('bar')
assert.equal(await this.object.bar(), 'bar')

// Rejecting a promise
Mock.when(this.object, 'foo').reject(new Error())
await assert.rejects(() => this.object.foo(), Error)
}

If you don't have access to the method you are mocking, or you want to create a specific kind of mock, you can get the stub created with the get() method:

@Test()
public async mockAnObjectMethod({ assert }: Context) {
const fooMock = Mock.when(this.object, 'foo').resolve('bar').get()

await this.someMethodThatCallsFooMethod()

assert.isTrue(fooMock.called)
}

// Or

@Test()
public async mockAnObjectMethod({ assert }: Context) {
const fooMock = Mock.when(this.object, 'foo').get()

fooMock.returns('bar')

await this.someMethodThatCallsFooMethod()

assert.isTrue(fooMock.called)
}

To deal with more complex objects while using Mock::when(), you can use the Mock::fake() method that creates a fake method that you can replace by the original one:

import { Test, Mock, type Context } from '@athenna/test'

export default class MockTest {
public object = {
foo: () => ({ bar: () => undefined })
}

@Test()
public async fakeAnObjectMethod({ assert }: Context) {
const barFake = Mock.fake()
Mock.when(this.object, 'foo').return({ bar: barFake }) 👈

this.object.foo().bar()

assert.calledOnce(barFake)
}
}
tip

For more assertions like assert.calledOnce() method, check the assertion in mocks documentation section.

Mocking by arguments

If your method receives an argument, you might need to test different scenarios based on the argument it has received. Athenna makes it delicious to mock this kind of situation using the withArgs() method:

import { Test, Mock, type Context } from '@athenna/test'

export default class MockTest {
public object = {
foo: (arg?: any) => 'Im the original!'
}

@Test()
public async mockAnObjectMethodByArgument({ assert }: Context) {
Mock.when(this.object, 'foo')
.withArgs('1') 👈
.return('mocked!')
.withArgs('2') 👈
.return('mocked!!')

assert.equal(this.object.foo('1'), 'mocked!')
assert.equal(this.object.foo('2'), 'mocked!!')

assert.equal(this.object.foo(), 'Im the original!')
assert.equal(this.object.foo('3'), 'Im the original!')
}
}

As you can see in the example above, when the arguments set for the foo() method don't match the defined mocks, the real implementation of foo() will be the chosen one, which can be a beneficial behavior for certain scenarios.

If you wish to remove this behavior, you could add the "default" return before adding the arguments mocks:

@Test()
public async mockAnObjectMethodByArgument({ assert }: Context) {
Mock.when(this.object, 'foo')
.return('default mock') 👈
.withArgs('1')
.return('mocked!')
.withArgs('2')
.return('mocked!!')

assert.equal(this.object.foo('1'), 'mocked!')
assert.equal(this.object.foo('2'), 'mocked!!')

assert.equal(this.object.foo(), 'default mock')
assert.equal(this.object.foo('3'), 'default mock')
}

Matching by argument type

Sometimes you don't mind what is the value of the argument that you will receive, but only the type of it, for this kind of situation you can use methods like withStringArg():

@Test()
public async mockAnObjectMethodByArgumentType({ assert }: Context) {
Mock.when(this.object, 'foo')
.withStringArg() 👈
.return('mocked!')
.withNumberArg() 👈
.return('mocked!!')

assert.equal(this.object.foo(1), 'mocked!!')
assert.equal(this.object.foo('1'), 'mocked!')
}

The code above is basically an alias for the following:

@Test()
public async mockAnObjectMethodByArgumentType({ assert }: Context) {
Mock.when(this.object, 'foo')
.withArgs(Mock.match.string) 👈
.return('mocked!')
.withArgs(Mock.match.number) 👈
.return('mocked!!')

assert.equal(this.object.foo(1), 'mocked!!')
assert.equal(this.object.foo('1'), 'mocked!')
}

This means that you can use the Mock.match helper to match more complex kind of data in your arguments:

@Test()
public async mockAnObjectMethodByArgumentType({ assert }: Context) {
Mock.when(this.object, 'foo')
.withArgs(Mock.match({ id: 1 })) 👈
.return('mocked!')
.withArgs(Mock.match({ id: 2 })) 👈
.return('mocked!!')

const user1 = { id: 1, createdAt: new Date() }
const user2 = { id: 2, createdAt: new Date() }

assert.equal(this.object.foo(user1), 'mocked!')
assert.equal(this.object.foo(user2), 'mocked!!')

// Not a mocked scenario, returning the real implementation.
assert.equal(this.object.foo({ id: 3 }), 'Im the original!')
}

Here you can check other methods that could help you to mock your methods by the argument value:

withArrayArg()

Set up a mock when mocked method is called with an array:

Mock.when(this.object, 'foo').withArrayArg().return('bar')

this.object.foo([])

withObjectArg()

Set up a mock when mocked method is called with an object:

Mock.when(this.object, 'foo').withObjectArg().return('bar')

this.object.foo({})

withBooleanArg()

Set up a mock when mocked method is called with a boolean:

Mock.when(this.object, 'foo').withBooleanArg().return('bar')

this.object.foo(true)
this.object.foo(false)

withFunctionArg()

Set up a mock when mocked method is called with a function:

Mock.when(this.object, 'foo').withFunctionArg().return('bar')

this.object.foo(() => {})

withDateArg()

Set up a mock when mocked method is called with a date:

Mock.when(this.object, 'foo').withDateArg().return('bar')

this.object.foo(new Date())

withRegexpArg()

Set up a mock when mocked method is called with a regexp:

Mock.when(this.object, 'foo').withRegexpArg().return('bar')

this.object.foo(new RegExp())

withFalsyArg()

Set up a mock when mocked method is called with a falsy value:

Mock.when(this.object, 'foo').withFalsyArg().return('bar')

this.object.foo('')
this.object.foo(false)

withTruthyArg()

Set up a mock when mocked method is called with a truthy value:

Mock.when(this.object, 'foo').withTruthyArg().return('bar')

this.object.foo('hey')
this.object.foo(true)

Mocking by call order

It's possible to mock a method only for a specific call count, for example, I might need to mock my method on the first and second call, but on the third one, I want the original method result. To do so you can use methods like onCall():

@Test()
public async mockAnObjectMethodByCall({ assert }: Context) {
Mock.when(this.object, 'foo')
.onCall(1) 👈
.return('mocked!')
.onCall(2) 👈
.return('mocked!!')

assert.equal(this.object.foo(), 'mocked!')
assert.equal(this.object.foo(), 'mocked!!')

// Not a mocked scenario, returning the real implementation.
assert.equal(this.object.foo(), 'Im the original!')
}

For convenience, you can use the onFirstCall(), onSecondCall() or the onThirdCall() methods also:

@Test()
public async mockAnObjectMethodByCall({ assert }: Context) {
Mock.when(this.object, 'foo')
.onFirstCall() 👈
.return('mocked!')
.onSecondCall() 👈
.return('mocked!!')
.onThirdCall() 👈
.return('mocked!!!')

assert.equal(this.object.foo(), 'mocked!')
assert.equal(this.object.foo(), 'mocked!!')
assert.equal(this.object.foo(), 'mocked!!!')

// Not a mocked scenario, returning the real implementation.
assert.equal(this.object.foo(), 'Im the original!')
}
tip

You can also merge onCall() with withArgs() to craft mocks that will be called nth times with determined argument:

@Test()
public async mockAnObjectMethodByCall({ assert }: Context) {
Mock.when(this.object, 'foo')
.withArgs(1)
.onFirstCall()
.return('mocked!')
.onSecondCall()
.return('mocked!!')

assert.equal(this.object.foo(1), 'mocked!')
assert.equal(this.object.foo(1), 'mocked!!')

// Not a mocked scenario, returning the real implementation.
assert.equal(this.object.foo(1), 'Im the original!')
}

Restoring mocks

When you mock a method, you are changing its behavior, so it is important to restore the default behavior of the method and also reset the mock history after your test finishes.

To do so you can use the Mock::restore() method within @AfterEach() hook:

import { Test, Mock, AfterEach, type Context } from '@athenna/test'

export default class MockTest {
public object = {
foo: () => undefined
}

@AfterEach()
public afterEach() {
Mock.restore(this.object.foo) 👈
}

@Test()
public async mockAnObjectMethod({ assert }: Context) {
Mock.when(this.object, 'foo').return('bar')

assert.equal(this.object.foo(), 'bar')
}
}

Restoring each mock individually can be a tedious task, so to be more convenient for you, use the Mock::restoreAll() method to restore all everything that has been mocked using Mock class to default:

@AfterEach()
public afterEach() {
Mock.restoreAll()
}

Spying objects

Sometimes you might need to only spy a given method without changing its behavior. For this scenario, you can use the Mock::spy() method:

import { Test, Mock, type Context } from '@athenna/test'

export default class MockTest {
public object = {
foo: () => 'bar'
}

@Test()
public async spyAnObjectMethod({ assert }: Context) {
const spy = Mock.spy(this.object, 'foo') 👈

assert.equal(this.object.foo(), 'bar')
assert.isTrue(spy.called) 👈
}
}

You can also spy an entire object using Mock::spy():

@Test()
public async spyAnObjectMethod({ assert }: Context) {
Mock.spy(this.object) 👈

assert.equal(this.object.foo(), 'bar')
assert.isTrue(this.object.foo.called) 👈
}

Just like Mock::when(), if you don't have access to the method you want to spy, you can save the return of Mock::spy() method and use it to make your assertions:

@Test()
public async spyAnObjectMethod({ assert }: Context) {
Mock.spy(this.object) 👈

await this.someMethodThatCallsObjectFoo()

assert.calledOnce(this.object.foo) 👈
}

Mocking classes

Mocking classes is the same of mocking objects. But you need to be aware when you are mocking a class instance or all instances of it (prototype).

Let's see how to mock a single class instance:

import { ApiHelper } from '#app/helpers/ApiHelper'
import { Test, Mock, type Context } from '@athenna/test'

export default class MockTest {
public apiHelper = new ApiHelper()

@Test()
public async mockAClassMethod({ assert }: Context) {
Mock.when(this.apiHelper, 'findOne').return({ fake: true })

assert.deepEqual(this.apiHelper.findOne(), { fake: true })
}
}

As mentioned before, with this approach, you can only mock the method for the current instance. If another instance of ApiHelper is created, that one will not be mocked.

To mock a class method for all instances of a given class, you need to mock the class prototype:

import { ApiHelper } from '#app/helpers/ApiHelper'
import { Test, Mock, type Context } from '@athenna/test'

export default class MockTest {
@Test()
public async mockAClassMethod({ assert }: Context) {
Mock.when(ApiHelper.prototype, 'findOne').return({ fake: true })

assert.deepEqual(new ApiHelper().findOne(), { fake: true })
}
}

Mocking services

Since the default registration type of services is transient, mocking a service from the service container is the same process of mocking a simple class using the prototype property:

import { AppService } from '#app/services/AppService'
import { Test, Mock, type Context } from '@athenna/test'

export default class MockTest {
@Test()
public async mockServiceMethod({ assert }: Context) {
Mock.when(AppService.prototype, 'findOne').return({ fake: true }) 👈

assert.deepEqual(new AppService().findOne(), { fake: true })
}
}
tip

If your service is registered as a singleton check the inversion of control documentation section.

Inversion of control

Sometimes you may want to have more control over the service instance; for this scenario, we recommend you to create a new instance of the service within your test class, registering it in the service container and then mocking the method you want in each test case:

import { AppService } from '#app/services/AppService'
import { BaseHttpTest } from '@athenna/core/testing/BaseHttpTest'
import { Test, type Context, BeforeAll, Mock } from '@athenna/test'

export default class AppControllerTest extends BaseHttpTest {
public appService = new AppService()

@BeforeAll()
public async beforeAll() {
ioc.instance('App/Services/AppService', this.appService) 👈
}

@Test()
public async mockServiceMethod({ assert, request }: Context) {
Mock.when(this.appService, 'findOne').return({ fake: true }) 👈

const response = await request.get('/api/v1')

response.assertStatusCode(200)
response.assertBodyContains({ fake: true })
assert.calledOnce(this.appService.findOne) 👈
}
}
warning

Keep in mind that if you change your service alias in the @Service() annotation, you will also need to use the same value in the ioc.instance() method.

For more information, check the @Service() annotation documentation section.

With ioc.instance() method, you can also create an entire different implementation of your service that will be used only for testing:

Path.fixtures('services/FakeAppService.ts')
import { AppServiceInterface } from '#app/interfaces/AppServiceInterface'

export class FakeAppService implements AppServiceInterface {
public findOne() {
return { fake: true }
}
}

And now let's use it in our test class:

import { Test, BeforeAll, type Context } from '@athenna/test'
import { BaseHttpTest } from '@athenna/core/testing/BaseHttpTest'
import { FakeAppService } from '#tests/fixtures/services/FakeAppService'

export default class AppControllerTest extends BaseHttpTest {
@BeforeAll()
public async beforeAll() {
ioc.instance('App/Services/AppService', new FakeAppService()) 👈
}

@Test()
public async mockServiceMethod({ request }: Context) {
const response = await request.get('/api/v1')

response.assertStatusCode(200)
response.assertBodyContains({ fake: true }) 👈
}
}
tip

Taste the power of dependency injection! 🚀🧙‍♂️

Mocking facades

When testing, you may often want to mock a call to an Athenna facade that occurs in your code logic. For example, consider the following controller action:

import { Mail } from '@athenna/mail'
import { Controller, type Context } from '@athenna/http'

@Controller()
export class UserController {
public async store({ response }: Context) {
const user = {
name: 'Antoine Du Hamel',
email: 'duhamelantoine1995@gmail.com'
}

await Mail.from('lenon@athenna.io')
.to(user.email)
.subject(`Welcome ${user.name} to Athenna!`)
.html('<h1>Welcome to Athenna!</h1>')
.send()

return response.status(200).send(user)
}
}

We can mock the call to the Mail facade by using the when() method, check the example:

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

export default class AppControllerTest extends BaseHttpTest {
@Test()
public async mockMailSendMethod({ assert, request }: Context) {
Mail.when('send').resolve(undefined) 👈

const response = await request.post('/api/v1/users')

assert.calledOnce(Mail.send) 👈
response.assertStatusCode(200)
response.assertBodyContains([
// ...
])
}
}
tip

Just like in Mock::when() method, you are able to mock by arguments and by calls using the Facade's when() method:

Mail.when('to')
.withArgs('lenon@athenna.io')
.throw(new Error())

Restoring facades

To restore a mocked facade to it default behavior, you can use the restore() method:

@AfterEach()
public afterEach() {
Mail.restore()
}
warning

When mocking facades, calling Mock::restoreAll() method will restore the facade to its default behavior, but it will leave the facade using the same instance for all calls. IOW, the facade is converted to a singleton.

This could lead to unexpected behavior in your tests, so it is recommended to restore each facade individually:

@AfterEach()
public afterEach() {
Mail.restore()
Mock.restoreAll()
}

Spying facades

If you would like to spy on a facade, you may call the spy() method on the corresponding facade:

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

export default class AppControllerTest extends BaseHttpTest {
@Test()
public async spyMailSendMethod({ assert, request }: Context) {
Mail.spy('send') 👈

const response = await request.post('/api/v1/users')

assert.calledOnce(Mail.send) 👈
response.assertStatusCode(200)
response.assertBodyContains([
// ...
])
}
}
note

Remember that the code above will effectively try to send the email since spies does not change the behavior of the method.

Assertions in mocks

Athenna's assert helper provides a variety of extended assertion methods that you may utilize when testing your mocks:

assert.called()

Assert the mock was called at least once:

assert.called(this.object.foo)
assert.notCalled(this.object.foo)

assert.calledOnce()

Assert the mock was called only once:

assert.calledOnce(this.object.foo)
assert.notCalledOnce(this.object.foo)

assert.calledTimes()

Assert the mock was called the given number of times:

assert.calledTimes(this.object.foo, 1)
assert.notCalledTimes(this.object.foo, 1)

assert.calledWith()

Assert the mock was called with the given arguments:

assert.calledWith(this.object.foo, 'bar')
assert.notCalledWith(this.object.foo, 'bar')

assert.calledOnceWith()

Assert the mock was called only once with the given arguments:

assert.calledOnceWith(this.object.foo, 'bar')
assert.notCalledOnceWith(this.object.foo, 'bar')

assert.calledTimesWith()

Assert the mock was called the given times with the given arguments:

assert.calledTimesWith(this.object.foo, 1, 'bar')
assert.notCalledTimesWith(this.object.foo, 1, 'bar')

assert.calledWithMatch()

Assert the mock was called with the given arguments matching some of the given arguments.

assert.calledWithMatch(this.object.foo, 'bar')
assert.notCalledWithMatch(this.object.foo, 'bar')
tip

This is an alias for the following:

assert.calledWith(this.object.foo, Mock.match('bar'))

assert.calledBefore()

Assert the mock was called before another mock:

assert.calledBefore(this.object.foo, this.object.bar)
assert.notCalledBefore(this.object.foo, this.object.bar)

assert.calledAfter()

Assert the mock was called after another mock:

assert.calledAfter(this.object.foo, this.object.bar)
assert.notCalledAfter(this.object.foo, this.object.bar)

Mocking commands

note

Before checking how to mock commands, we recommend you to check the changing Artisan file path per command documentation section to understand how you can create an isolated test case for the command you wish to test.

When testing commands, you may often want to mock some logic that happens inside the child process that Athenna creates to run your command.

Let's suppose we have created a command called greet which prompts the user his name to say hy:

Path.commands('Greet.ts')
import { BaseCommand } from '@athenna/artisan'

export class Greet extends BaseCommand {
public static signature(): string {
return 'greet'
}

public async handle(): Promise<void> {
const name = await this.prompt.input('What is your name?')

this.logger.info(`Hello ${name}!`)
}
}

If you try to run greet command in your tests it will exceed the timeout since the command will be waiting for the user input name.

To solve this kind of situation and others, you can create your own Artisan console that will first mock the this.prompt.input() method and then boot Artisan:

Path.fixtures('consoles/mock-greet-input.ts')
import { Mock } from '@athenna/test'
import { Ignite } from '@athenna/core'
import { Prompt } from '@athenna/artisan'

const ignite = await new Ignite().load(import.meta.url, { bootLogs: false })

Mock.when(Prompt.prototype, 'input').resolve('Valmir Barbosa')

await ignite.console(process.argv, { displayName: 'Artisan' })

Now, we can use this new Artisan console file to run the command in our test cases:

import { Path } from '@athenna/common'
import { Test, type Context } from '@athenna/test'

export default class GreetTest {
@Test()
public async testGreet({ command }: Context) {
const output = await command.run('greet', {
path: Path.fixtures('consoles/mock-greet-input.ts') 👈
})

output.assertLogged('Hello Valmir Barbosa!')
}
}

Mocking database

When working with the database library, you might need to mock the database calls in your tests, specially in unitary tests. For this situation, you can use the fake connection as your default connection in your .env.test file:

.env.test
DB_CONNECTION=fake

Next step is to register the Database facade using the DatabaseProvider to be able to access the facade. We could do it inside a @BeforeAll() hook. Let's craft a UserServiceTest class to use as an example:

UserServiceTest.ts
import { DatabaseProvider } from '@athenna/database'
import { Mock, BeforeAll, AfterEach } from '@athenna/test'

export default class UserServiceTest {
@BeforeAll()
public beforeAll() {
new DatabaseProvider().register()
}

@AfterEach()
public afterEach() {
Mock.restoreAll()
}
}

Nice, now we have everything set up to start mocking database calls 🥳

warning

Never forget the Mock.restoreAll() in the @AfterEach() hook to clean your mocks in each test 👍

Mocking the find() method

Imagine that our UserService has a method called findById() that is responsible to find a user in database by the id using an User model. To mock this scenario, we could do the following:

import { UserService } from '#app/services/UserService'
import { Database, DatabaseProvider } from '@athenna/database'
import { Test, Mock, BeforeAll, AfterEach, type Context } from '@athenna/test'

export default class UserServiceTest {
@BeforeAll()
public beforeEach() {
new DatabaseProvider().register()
}

@AfterEach()
public afterEach() {
Mock.restoreAll()
}

@Test()
public async testFindById({ assert }: Context) {
Mock.when(Database.driver, 'find').resolve({
id: 1,
name: 'Antoine Du Hamel'
})

const userService = new UserService()
const user = await userService.findById(1) 👈

assert.calledOnce(Database.driver.find)
assert.deepEqual(user, { id: 1, name: 'Antoine Du Hamel' })
}
}

Mocking the create() method

Now imagine that our UserService has a method called create() that is responsible to create a user in database using an User model. To mock this scenario, we could do the following:

import { UserService } from '#app/services/UserService'
import { Database, DatabaseProvider } from '@athenna/database'
import { Test, Mock, BeforeAll, AfterEach, type Context } from '@athenna/test'

export default class UserServiceTest {
@BeforeAll()
public beforeAll() {
new DatabaseProvider().register()
}

@AfterEach()
public afterEach() {
Mock.restoreAll()
}

@Test()
public async testCreate({ assert }: Context) {
Mock.when(Database.driver, 'create')
.withArgs({ name: 'João Lenon' })
.resolve({ id: 1, name: 'João Lenon', createdAt: new Date() })
.withArgs({ name: 'Antoine Du Hamel' })
.resolve({ id: 2, name: 'Antoine Du Hamel', createdAt: new Date() })

const userService = new UserService()
const data = { name: 'Antoine Du Hamel' }
const user = await userService.create(data) 👈

assert.containsSubset(user, { id: 2, name: 'Antoine Du Hamel' })
assert.calledOnceWith(Database.driver.create, data)
}
}
tip

Mocking the methods of the Database.driver property gaves you power to change the behavior of models and also Database facade calls. You are able to mock any method of the database query builder.