Skip to content

Commit

Permalink
Merge pull request #99 from AthennaIO/develop
Browse files Browse the repository at this point in the history
feat(mock): add mock API
  • Loading branch information
jlenon7 committed Sep 13, 2023
2 parents f93c30f + 7ae40db commit 5e72a0f
Show file tree
Hide file tree
Showing 16 changed files with 834 additions and 3 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@athenna/test",
"version": "4.5.0",
"version": "4.6.0",
"description": "The Athenna test runner. Built on top of Japa.",
"license": "MIT",
"author": "João Lenon <lenon@athenna.io>",
Expand Down
138 changes: 138 additions & 0 deletions src/globals/Assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,85 @@
* file that was distributed with this source code.
*/

import { Assert } from '@japa/assert'
import type { SinonSpy } from 'sinon'

Assert.macro('called', function (mock: SinonSpy) {
return this.isTrue(mock.called)
})

Assert.macro('calledOnce', function (mock: SinonSpy) {
return this.deepEqual(mock.callCount, 1)
})

Assert.macro('calledTimes', function (mock: SinonSpy, times: number) {
return this.deepEqual(mock.callCount, times)
})

Assert.macro('calledWith', function (mock: SinonSpy, ...args: any[]) {
const hasCalledWith = mock.calledWith(...args)

if (!hasCalledWith) {
return this.deepEqual(mock.args[0], args)
}

return this.isTrue(hasCalledWith)
})

Assert.macro('calledOnceWith', function (mock: SinonSpy, ...args: any[]) {
return this.isTrue(mock.calledOnceWith(...args))
})

Assert.macro(
'calledTimesWith',
function (mock: SinonSpy, times: number, ...args: any[]) {
this.calledTimes(mock, times)
this.calledWith(mock, ...args)
}
)

Assert.macro('calledBefore', function (mock: SinonSpy, beforeMock: SinonSpy) {
return this.isTrue(mock.calledBefore(beforeMock))
})

Assert.macro('calledAfter', function (mock: SinonSpy, afterMock: SinonSpy) {
return this.isTrue(mock.calledAfter(afterMock))
})

Assert.macro('returned', function (mock: SinonSpy, value: any) {
const hasReturned = mock.returned(value)

if (!hasReturned) {
return this.deepEqual(mock.returnValues[0], value)
}

return this.isTrue(hasReturned)
})

Assert.macro('threw', function (mock: SinonSpy, value?: any) {
const hasThrew = mock.threw(value)

if (!hasThrew) {
return this.deepEqual(mock.returnValues[0], value)
}

return this.isTrue(hasThrew)
})

Assert.macro('resolved', async function (mock: SinonSpy, value?: any) {
const hasResolved = mock.returned(value)

if (!hasResolved) {
return this.deepEqual(await mock.returnValues[0], value)
}

return this.isTrue(hasResolved)
})

Assert.macro('rejected', async function (mock: SinonSpy, value?: any) {
return this.rejects(() => mock.returnValues[0], value)
})

export {}

declare module '@japa/assert' {
Expand All @@ -23,5 +102,64 @@ declare module '@japa/assert' {
errType: any,
message?: string
): Promise<any>
hey(): void
/**
* Assert that the given mock was called.
*/
called(mock: SinonSpy): void
/**
* Assert that the given mock was called only once.
*/
calledOnce(mock: SinonSpy): void
/**
* Assert that the given mock was called the
* determined number of times.
*/
calledTimes(mock: SinonSpy, times: number): void
/**
* Assert that the given mock was called with the
* determined arguments.
*/
calledWith(mock: SinonSpy, ...args: any[]): void
/**
* Assert that the given mock was called only once
* with the determined arguments.
*/
calledOnceWith(mock: SinonSpy, ...args: any[]): void
/**
* Assert that the given mock was called the
* determined number of times with always the determined
* arguments.
*/
calledTimesWith(mock: SinonSpy, times: number, ...args: any[]): void
/**
* Assert that the given mock was called before
* an other determined mock.
*/
calledBefore(mock: SinonSpy, beforeMock: SinonSpy): void
/**
* Assert that the given mock was called after
* an other determined mock.
*/
calledAfter(mock: SinonSpy, afterMock: SinonSpy): void
/**
* Assert that the given mock returned a determined value.
*/
returned(mock: SinonSpy, value: any): void
/**
* Assert that the given threw. Could also
* assert the value that has been thrown.
*/
threw(mock: SinonSpy, value?: any): void
/**
* Assert that the given mock resolved. Could also
* assert the value that has been returned.
*/
resolved(mock: SinonSpy, value?: any): Promise<void>
/**
* Assert that the given mock rejected. Could also
* assert the value that has been returned.
*/
rejected(mock: SinonSpy, value?: any): Promise<void>
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export * from '#src/globals/Context'
export * from '#src/helpers/Runner'
export * from '#src/helpers/Importer'
export * from '#src/helpers/ExitFaker'
export * from '#src/mocks/Mock'
export * from '#src/mocks/MockBuilder'
export * from '#src/annotations/AfterAll'
export * from '#src/annotations/AfterEach'
export * from '#src/annotations/BeforeAll'
Expand Down
70 changes: 70 additions & 0 deletions src/mocks/Mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* @athenna/test
*
* (c) João Lenon <lenon@athenna.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { createSandbox } from 'sinon'
import { MockBuilder } from '#src/mocks/MockBuilder'
import type { StubMethod } from '#src/types/StubMethod'
import type { SpyMethod, SpyInstance } from '#src/types'
import type { StubInstance } from '#src/types/StubInstance'

export class Mock {
/**
* Sinon sandbox instance.
*/
public static sandbox = createSandbox()

/**
* Create a mock builder instance for the given object
* and a method of the object.
*/
public static when<T = any>(object: T, method: keyof T): MockBuilder {
return new MockBuilder(object, method, Mock.sandbox)
}

/**
* Create a spy function for a given object.
*/
public static spy<T = any>(object: T): SpyInstance<T>

/**
* Create a spy function for a given object method.
*/
public static spy<T = any>(object: T, method: keyof T): SpyMethod<T[keyof T]>

public static spy<T = any>(object: T, method?: keyof T) {
return Mock.sandbox.spy(object, method)
}

/**
* Create a stub function for a given object.
*/
public static stub<T = any>(object: T): StubInstance<T>

/**
* Create a stub function for a given object method.
*/
public static stub<T = any>(
object: T,
method: keyof T
): StubMethod<T[keyof T]>

public static stub<T = any>(object: T, method?: keyof T) {
return Mock.sandbox.stub(object, method)
}

/**
* Restore all mocks to default.
*/
public static restoreAll(): void {
Mock.sandbox.restore()
Mock.sandbox.resetHistory()
Mock.sandbox.resetBehavior()
Mock.sandbox.reset()
}
}
100 changes: 100 additions & 0 deletions src/mocks/MockBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* @athenna/test
*
* (c) João Lenon <lenon@athenna.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import type { Stub } from '#src/types'
import type { SinonSandbox } from 'sinon'
import type { Exception } from '@athenna/common'

export class MockBuilder {
public constructor(
private object: any,
private method: any,
private sandbox: SinonSandbox
) {}

/**
* Mock the method to return the given value.
*
* @example
* ```ts
* import { Mock } from '@athenna/test'
*
* const mock = Mock.when(console, 'log').return('Hello World')
*
* mock.called // false
* ```
*/
public return<T = any>(value: T): Stub {
const stub = this.sandbox.stub(this.object, this.method)

stub.returns(value)

return stub
}

/**
* Mock the method to throw the given value.
*
* @example
* ```ts
* import { Mock } from '@athenna/test'
*
* const mock = Mock.when(console, 'log').throw('Hello World')
*
* mock.called // false
* ```
*/
public throw(value: string | Error | Exception): Stub {
const stub = this.sandbox.stub(this.object, this.method)

stub.throws(value)

return stub
}

/**
* Mock the method to resolve returning the given value.
*
* @example
* ```ts
* import { Mock } from '@athenna/test'
*
* const mock = Mock.when(console, 'log').resolve('Hello World')
*
* mock.called // false
* ```
*/
public resolve<T = any>(value: T): Stub {
const stub = this.sandbox.stub(this.object, this.method)

stub.resolves(value)

return stub
}

/**
* Mock the method to reject returning the given value.
*
* @example
* ```ts
* import { Mock } from '@athenna/test'
*
* const mock = Mock.when(console, 'log').reject('Hello World')
*
* mock.called // false
* ```
*/
public reject<T = any>(value: T): Stub {
const stub = this.sandbox.stub(this.object, this.method)

stub.rejects(value)

return stub
}
}
15 changes: 15 additions & 0 deletions src/types/Spy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @athenna/test
*
* (c) João Lenon <lenon@athenna.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import type { SinonSpy } from 'sinon'

export type Spy<
TArgs extends readonly any[] = any[],
TReturnValue = any
> = SinonSpy<TArgs, TReturnValue>
14 changes: 14 additions & 0 deletions src/types/SpyInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @athenna/test
*
* (c) João Lenon <lenon@athenna.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import type { SpyMethod } from '#src/types/SpyMethod'

export type SpyInstance<T> = {
[P in keyof T]: SpyMethod<T[P]>
}
12 changes: 12 additions & 0 deletions src/types/SpyMethod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* @athenna/test
*
* (c) João Lenon <lenon@athenna.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import type { SinonSpiedMember } from 'sinon'

export type SpyMethod<T> = SinonSpiedMember<T>
Loading

0 comments on commit 5e72a0f

Please sign in to comment.