From f976135095b39d3793b16b0b7116dec748b190f0 Mon Sep 17 00:00:00 2001 From: Huafu Gandon Date: Fri, 28 Sep 2018 11:38:32 +0200 Subject: [PATCH] feat(helpers): adds a mocked test helper for mock typings 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 --- e2e/__cases__/test-helpers/fail.spec.ts | 10 +++++++ e2e/__cases__/test-helpers/pass.spec.ts | 17 +++++++++++ e2e/__cases__/test-helpers/to-mock.ts | 15 ++++++++++ .../__snapshots__/test-helpers.test.ts.snap | 29 ++++++++++++++++++ e2e/__tests__/test-helpers.test.ts | 6 ++++ src/__helpers__/mocks.ts | 8 ----- src/cli/cli.spec.ts | 3 +- src/config/config-set.spec.ts | 3 +- src/index.spec.ts | 3 ++ src/index.ts | 2 ++ src/types.ts | 30 +++++++++++++++++++ src/util/jsonable-value.spec.ts | 4 +-- src/util/testing.spec.ts | 8 +++++ src/util/testing.ts | 8 +++++ src/util/version-checkers.spec.ts | 3 +- 15 files changed, 136 insertions(+), 13 deletions(-) create mode 100644 e2e/__cases__/test-helpers/fail.spec.ts create mode 100644 e2e/__cases__/test-helpers/pass.spec.ts create mode 100644 e2e/__cases__/test-helpers/to-mock.ts create mode 100644 e2e/__tests__/__snapshots__/test-helpers.test.ts.snap create mode 100644 e2e/__tests__/test-helpers.test.ts create mode 100644 src/util/testing.spec.ts create mode 100644 src/util/testing.ts diff --git a/e2e/__cases__/test-helpers/fail.spec.ts b/e2e/__cases__/test-helpers/fail.spec.ts new file mode 100644 index 0000000000..f2f4d6372d --- /dev/null +++ b/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() +}) diff --git a/e2e/__cases__/test-helpers/pass.spec.ts b/e2e/__cases__/test-helpers/pass.spec.ts new file mode 100644 index 0000000000..a275fdb7ff --- /dev/null +++ b/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) +}) diff --git a/e2e/__cases__/test-helpers/to-mock.ts b/e2e/__cases__/test-helpers/to-mock.ts new file mode 100644 index 0000000000..7e0593d394 --- /dev/null +++ b/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}` + } + } +} diff --git a/e2e/__tests__/__snapshots__/test-helpers.test.ts.snap b/e2e/__tests__/__snapshots__/test-helpers.test.ts.snap new file mode 100644 index 0000000000..a2a24c83b3 --- /dev/null +++ b/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. + ================================================================================ +`; diff --git a/e2e/__tests__/test-helpers.test.ts b/e2e/__tests__/test-helpers.test.ts new file mode 100644 index 0000000000..df0bdaf8f8 --- /dev/null +++ b/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() +}) diff --git a/src/__helpers__/mocks.ts b/src/__helpers__/mocks.ts index 4da5680117..75cdd8bc9c 100644 --- a/src/__helpers__/mocks.ts +++ b/src/__helpers__/mocks.ts @@ -2,14 +2,6 @@ import { testing } from 'bs-logger' import { rootLogger } from '../util/logger' -// typings helper -export function mocked(val: T): T extends (...args: any[]) => any ? jest.MockInstance : jest.Mocked { - return val as any -} -export function spied(val: T): T extends (...args: any[]) => any ? jest.SpyInstance : jest.Mocked { - return val as any -} - export const logTargetMock = () => (rootLogger as testing.LoggerMock).target export const mockObject = (obj: T, newProps: M): T & M & { mockRestore: () => T } => { diff --git a/src/cli/cli.spec.ts b/src/cli/cli.spec.ts index d555281317..ff6952fe51 100644 --- a/src/cli/cli.spec.ts +++ b/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 '.' diff --git a/src/config/config-set.spec.ts b/src/config/config-set.spec.ts index eef9b851ae..277f45a4b0 100644 --- a/src/config/config-set.spec.ts +++ b/src/config/config-set.spec.ts @@ -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' diff --git a/src/index.spec.ts b/src/index.spec.ts index 19afff83fe..a489a7103a 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -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') }) diff --git a/src/index.ts b/src/index.ts index 15806be569..c658bd887f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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') diff --git a/src/types.ts b/src/types.ts index 48c3c3f783..66d421b2ad 100644 --- a/src/types.ts +++ b/src/types.ts @@ -180,3 +180,33 @@ export interface AstTransformerDesc { version: number factory(cs: ConfigSet): TransformerFactory } + +// test helpers + +interface MockWithArgs extends Function, jest.MockInstance { + new (...args: ArgumentsOf): T + (...args: ArgumentsOf): any +} + +// tslint:disable-next-line:ban-types +type MethodKeysOf = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T] +// tslint:disable-next-line:ban-types +type PropertyKeysOf = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T] +type ArgumentsOf = T extends (...args: infer A) => any ? A : never +interface MockWithArgs extends Function, jest.MockInstance { + new (...args: ArgumentsOf): T + (...args: ArgumentsOf): any +} + +type MockedFunction = MockWithArgs & { [K in keyof T]: T[K] } +type MockedFunctionDeep = MockWithArgs & MockedObjectDeep +type MockedObject = { [K in MethodKeysOf]: MockedFunction } & { [K in PropertyKeysOf]: T[K] } +type MockedObjectDeep = { [K in MethodKeysOf]: MockedFunctionDeep } & + { [K in PropertyKeysOf]: MaybeMockedDeep } + +export type MockedDeep = MockWithArgs & { [K in MethodKeysOf]: MockedDeep } +export type Mocked = MockWithArgs & { [K in MethodKeysOf]: Mocked } +// tslint:disable-next-line:ban-types +export type MaybeMockedDeep = T extends Function ? MockedFunctionDeep : T extends object ? MockedObjectDeep : T +// tslint:disable-next-line:ban-types +export type MaybeMocked = T extends Function ? MockedFunction : T extends object ? MockedObject : T diff --git a/src/util/jsonable-value.spec.ts b/src/util/jsonable-value.spec.ts index a4ccc2e55e..5ef95a6d26 100644 --- a/src/util/jsonable-value.spec.ts +++ b/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' @@ -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) diff --git a/src/util/testing.spec.ts b/src/util/testing.spec.ts new file mode 100644 index 0000000000..9ccdb126bc --- /dev/null +++ b/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) + }) +}) diff --git a/src/util/testing.ts b/src/util/testing.ts new file mode 100644 index 0000000000..a45b27cb72 --- /dev/null +++ b/src/util/testing.ts @@ -0,0 +1,8 @@ +import { MaybeMocked, MaybeMockedDeep } from '../types' + +// the typings test helper +export function mocked(item: T, deep?: false): MaybeMocked +export function mocked(item: T, deep: true): MaybeMockedDeep +export function mocked(item: T, _deep = false): MaybeMocked | MaybeMockedDeep { + return item as any +} diff --git a/src/util/version-checkers.spec.ts b/src/util/version-checkers.spec.ts index e90401205e..3670b6071a 100644 --- a/src/util/version-checkers.spec.ts +++ b/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'