Skip to content

Commit

Permalink
feat(helpers): adds a mocked test helper for mock typings
Browse files Browse the repository at this point in the history
The `jest.mock()` does not change the type of the exported values from a
module. So we have to:
```ts
expect((foo as any).mock.calls[0][0]).toBe('foo')
```
Now with the `mocked` helper it is possible to do:
```ts
expect(mocked(foo).mock.calls[0][0]).toBe('foo')
```
It is also possible to deeply mock a module right after importing it.
Docs will be updated with related details.

Closes #576
  • Loading branch information
huafu committed Sep 28, 2018
1 parent 13cb48d commit f976135
Show file tree
Hide file tree
Showing 15 changed files with 136 additions and 13 deletions.
10 changes: 10 additions & 0 deletions e2e/__cases__/test-helpers/fail.spec.ts
@@ -0,0 +1,10 @@
import { mocked } from 'ts-jest'
import { foo, bar } from './to-mock'
jest.mock('./to-mock')

it('should fail to compile', () => {
// the method does not accept any arg
expect(mocked(foo)('hello')).toBeUndefined()
// the method accepts a string so typing should fail here
expect(mocked(bar, true).dummy.deep.deeper(42)).toBeUndefined()
})
17 changes: 17 additions & 0 deletions e2e/__cases__/test-helpers/pass.spec.ts
@@ -0,0 +1,17 @@
import { mocked } from 'ts-jest'
import { foo, bar } from './to-mock'
jest.mock('./to-mock')

test('foo', () => {
// real returns 'foo', mocked returns 'bar'
expect(foo()).toBeUndefined()
expect(mocked(foo).mock.calls.length).toBe(1)
})

test('bar', () => {
const mockedBar = mocked(bar, true)
// real returns 'foo', mocked returns 'bar'
expect(mockedBar()).toBeUndefined()
expect(mockedBar.dummy.deep.deeper()).toBeUndefined()
expect(mockedBar.dummy.deep.deeper.mock.calls.length).toBe(1)
})
15 changes: 15 additions & 0 deletions e2e/__cases__/test-helpers/to-mock.ts
@@ -0,0 +1,15 @@
export const foo = () => 'foo'

export function bar() {
return 'bar'
}
export namespace bar {
export function dummy() {
return 'dummy'
}
export namespace dummy {
export const deep = {
deeper: (one: string = '1') => `deeper ${one}`
}
}
}
29 changes: 29 additions & 0 deletions e2e/__tests__/__snapshots__/test-helpers.test.ts.snap
@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`test-helpers 1`] = `
× jest --no-cache
↳ exit code: 1
===[ STDOUT ]===================================================================
===[ STDERR ]===================================================================
PASS ./pass.spec.ts
FAIL ./fail.spec.ts
● Test suite failed to run
TypeScript diagnostics (customize using \`[jest-config].globals.ts-jest.diagnostics\` option):
fail.spec.ts:7:10 - error TS2554: Expected 0 arguments, but got 1.
7 expect(mocked(foo)('hello')).toBeUndefined()
~~~~~~~~~~~~~~~~~~~~
fail.spec.ts:9:46 - error TS2345: Argument of type '42' is not assignable to parameter of type 'string'.
9 expect(mocked(bar, true).dummy.deep.deeper(42)).toBeUndefined()
~~
Test Suites: 1 failed, 1 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: XXs
Ran all test suites.
================================================================================
`;
6 changes: 6 additions & 0 deletions e2e/__tests__/test-helpers.test.ts
@@ -0,0 +1,6 @@
import { configureTestCase } from '../__helpers__/test-case'

test('test-helpers', () => {
const test = configureTestCase('test-helpers', { noCache: true })
expect(test.run(1)).toMatchSnapshot()
})
8 changes: 0 additions & 8 deletions src/__helpers__/mocks.ts
Expand Up @@ -2,14 +2,6 @@ import { testing } from 'bs-logger'

import { rootLogger } from '../util/logger'

// typings helper
export function mocked<T>(val: T): T extends (...args: any[]) => any ? jest.MockInstance<T> : jest.Mocked<T> {
return val as any
}
export function spied<T>(val: T): T extends (...args: any[]) => any ? jest.SpyInstance<T> : jest.Mocked<T> {
return val as any
}

export const logTargetMock = () => (rootLogger as testing.LoggerMock).target

export const mockObject = <T, M>(obj: T, newProps: M): T & M & { mockRestore: () => T } => {
Expand Down
3 changes: 2 additions & 1 deletion src/cli/cli.spec.ts
@@ -1,7 +1,8 @@
import * as _fs from 'fs'
import { normalize, resolve } from 'path'

import { logTargetMock, mockObject, mockWriteStream, mocked } from '../__helpers__/mocks'
import { mocked } from '../..'
import { logTargetMock, mockObject, mockWriteStream } from '../__helpers__/mocks'

import { processArgv } from '.'

Expand Down
3 changes: 2 additions & 1 deletion src/config/config-set.spec.ts
Expand Up @@ -3,8 +3,9 @@ import { resolve } from 'path'
import ts, { Diagnostic, DiagnosticCategory, ModuleKind, ScriptTarget } from 'typescript'

import * as _myModule from '..'
import { mocked } from '../..'
import * as fakers from '../__helpers__/fakers'
import { logTargetMock, mocked } from '../__helpers__/mocks'
import { logTargetMock } from '../__helpers__/mocks'
import { TsJestGlobalOptions } from '../types'
import * as _backports from '../util/backports'
import { normalizeSlashes } from '../util/normalize-slashes'
Expand Down
3 changes: 3 additions & 0 deletions src/index.spec.ts
Expand Up @@ -27,6 +27,9 @@ describe('ts-jest', () => {
it('should export a `createJestPreset` function', () => {
expect(typeof tsJest.createJestPreset).toBe('function')
})
it('should export a `mocked` function', () => {
expect(typeof tsJest.mocked).toBe('function')
})
it('should export a `pathsToModuleNameMapper` function', () => {
expect(typeof tsJest.pathsToModuleNameMapper).toBe('function')
})
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Expand Up @@ -7,6 +7,8 @@ import { TsJestTransformer } from './ts-jest-transformer'
import { TsJestGlobalOptions } from './types'
import { VersionCheckers } from './util/version-checkers'

export * from './util/testing'

// tslint:disable-next-line:no-var-requires
export const version: string = require('../package.json').version
export const digest: string = readFileSync(resolve(__dirname, '..', '.ts-jest-digest'), 'utf8')
Expand Down
30 changes: 30 additions & 0 deletions src/types.ts
Expand Up @@ -180,3 +180,33 @@ export interface AstTransformerDesc {
version: number
factory(cs: ConfigSet): TransformerFactory<SourceFile>
}

// test helpers

interface MockWithArgs<T> extends Function, jest.MockInstance<T> {
new (...args: ArgumentsOf<T>): T
(...args: ArgumentsOf<T>): any
}

// tslint:disable-next-line:ban-types
type MethodKeysOf<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]
// tslint:disable-next-line:ban-types
type PropertyKeysOf<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]
type ArgumentsOf<T> = T extends (...args: infer A) => any ? A : never
interface MockWithArgs<T> extends Function, jest.MockInstance<T> {
new (...args: ArgumentsOf<T>): T
(...args: ArgumentsOf<T>): any
}

type MockedFunction<T> = MockWithArgs<T> & { [K in keyof T]: T[K] }
type MockedFunctionDeep<T> = MockWithArgs<T> & MockedObjectDeep<T>
type MockedObject<T> = { [K in MethodKeysOf<T>]: MockedFunction<T[K]> } & { [K in PropertyKeysOf<T>]: T[K] }
type MockedObjectDeep<T> = { [K in MethodKeysOf<T>]: MockedFunctionDeep<T[K]> } &
{ [K in PropertyKeysOf<T>]: MaybeMockedDeep<T[K]> }

export type MockedDeep<T> = MockWithArgs<T> & { [K in MethodKeysOf<T>]: MockedDeep<T[K]> }
export type Mocked<T> = MockWithArgs<T> & { [K in MethodKeysOf<T>]: Mocked<T[K]> }
// tslint:disable-next-line:ban-types
export type MaybeMockedDeep<T> = T extends Function ? MockedFunctionDeep<T> : T extends object ? MockedObjectDeep<T> : T
// tslint:disable-next-line:ban-types
export type MaybeMocked<T> = T extends Function ? MockedFunction<T> : T extends object ? MockedObject<T> : T
4 changes: 2 additions & 2 deletions src/util/jsonable-value.spec.ts
@@ -1,4 +1,4 @@
import { mocked } from '../__helpers__/mocks'
import { mocked } from '../..'

import * as _json from './json'
import { JsonableValue } from './jsonable-value'
Expand All @@ -13,7 +13,7 @@ beforeEach(() => {
jest.clearAllMocks()
})

it('should cache the seralized value', () => {
it('should cache the serialized value', () => {
const jv = new JsonableValue({ foo: 'bar' })
expect(jv.serialized).toBe('{"foo":"bar"}')
expect(stringify).toHaveBeenCalledTimes(1)
Expand Down
8 changes: 8 additions & 0 deletions src/util/testing.spec.ts
@@ -0,0 +1,8 @@
import { mocked } from './testing'

describe('mocked', () => {
it('should return unmodified input', () => {
const subject = {}
expect(mocked(subject)).toBe(subject)
})
})
8 changes: 8 additions & 0 deletions src/util/testing.ts
@@ -0,0 +1,8 @@
import { MaybeMocked, MaybeMockedDeep } from '../types'

// the typings test helper
export function mocked<T>(item: T, deep?: false): MaybeMocked<T>
export function mocked<T>(item: T, deep: true): MaybeMockedDeep<T>
export function mocked<T>(item: T, _deep = false): MaybeMocked<T> | MaybeMockedDeep<T> {
return item as any
}
3 changes: 2 additions & 1 deletion src/util/version-checkers.spec.ts
@@ -1,5 +1,6 @@
// tslint:disable:max-line-length
import { logTargetMock, mocked } from '../__helpers__/mocks'
import { mocked } from '../..'
import { logTargetMock } from '../__helpers__/mocks'

import * as _pv from './get-package-version'
import { VersionChecker, VersionCheckers } from './version-checkers'
Expand Down

0 comments on commit f976135

Please sign in to comment.