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')
}
}
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)
}
}
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()
withObjectArg()
withBooleanArg()
withFunctionArg()
withDateArg()
withRegexpArg()
withFalsyArg()
withTruthyArg()
withAnyArg()
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!')
}
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 '#src/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 '#src/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 '#src/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 })
}
}
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 '#src/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) 👈
}
}
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:
import { AppServiceInterface } from '#src/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 }) 👈
}
}
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([
// ...
])
}
}
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()
}
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([
// ...
])
}
}
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.calledOnce()
assert.calledTimes()
assert.calledWith()
assert.calledOnceWith()
assert.calledTimesWith()
assert.calledWithMatch()
assert.calledBefore()
assert.calledAfter()
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')
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
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:
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:
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:
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:
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 🥳
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 '#src/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 '#src/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) ✅
}
}
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.