Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds mocked test helper for jest.mock typings #774

Merged
merged 4 commits into from
Sep 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/_data/nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
url: /user/install
- title: Configuration
url: /user/config
- title: Test helpers
url: /user/test-helpers
- title: React Native
url: /user/react-native
- title: Babel7 or TypeScript
Expand Down
50 changes: 50 additions & 0 deletions docs/user/test-helpers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
title: Test helpers
---
`ts-jest` provides some test utilities to be used in your test, related to TypeScript.

## `mocked()`

The `mocked` test helper provides typings on your mocked modules and even their deep methods, based on the typing of its source. It make use of the latest TypeScript features so you even have argument types completion in the IDE (as opposed to `jest.MockInstance`).

**Note:** while it needs to be a function so that input type is changed, the helper itself does nothing else than returning the given input value.

### Example

```ts
// foo.ts
export const foo = {
a : {
b: {
c: {
hello: (name: string) => `Hello, ${name}`,
},
},
},
name: () => 'foo',
}
```

```ts
// foo.spec.ts
import { mocked } from 'ts-jest'
import { foo } from './foo'
jest.mock('./foo')

// here the whole foo var is mocked deeply
const mockedFoo = mocked(foo, true)

test('deep', () => {
// there will be no TS error here, and you'll have completion in modern IDEs
mockedFoo.a.b.c.hello('me')
// same here
expect(mockedFoo.a.b.c.hello.mock.calls).toHaveLength(1)
})

test('direct', () => {
foo.name()
// here only foo.name is mocked (or its methods if it's an object)
expect(mocked(foo.name).mock.calls).toHaveLength(1)
})

```
10 changes: 10 additions & 0 deletions e2e/__cases__/test-helpers/fail.spec.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
28 changes: 28 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,31 @@ 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]> }

// 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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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