Skip to content

Commit

Permalink
chore: initial commit
Browse files Browse the repository at this point in the history
implement jest and sinon versions of mocks
  • Loading branch information
maxjoehnk committed Apr 9, 2021
0 parents commit 14d9647
Show file tree
Hide file tree
Showing 13 changed files with 328 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
yarn.lock
*.js
*.d.ts
!jest.config.js
!.eslintrc.js
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# proxy-mocks

Generate mocks for any class or object.

## Example

```typescript
// import { IMock, Mock } from 'proxy-mocks/jest';
import { IMock, Mock } from "proxy-mocks/sinon";
import Dependency from "./dependency";
import Implementation from "./implementation";

describe("Implementation", () => {
let dependency: IMock<Dependency>;

let implementation: Implementation;

beforeEach(() => {
dependency = Mock.of(Dependency);

implementation = new Implementation(dependency);
});

test("your test", () => {
dependency.someMethod.returns("your result");

const result = implementation.anotherMethod();

expect(result).toEqual("your result");
});
});
```
24 changes: 24 additions & 0 deletions dprint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "https://dprint.dev/schemas/v0.json",
"projectType": "openSource",
"incremental": true,
"typescript": {
"indentWidth": 2
},
"json": {
},
"markdown": {
},
"includes": ["**/*.{ts,tsx,js,jsx,json,md}"],
"excludes": [
"**/node_modules",
"**/*-lock.json",
"src/**/*.js",
"**/*.d.ts"
],
"plugins": [
"https://plugins.dprint.dev/typescript-0.44.0.wasm",
"https://plugins.dprint.dev/json-0.10.1.wasm",
"https://plugins.dprint.dev/markdown-0.6.2.wasm"
]
}
75 changes: 75 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"name": "proxy-mocks",
"version": "0.1.0",
"description": "Provide mocks utilizing the Proxy API",
"main": "index.js",
"repository": "https://github.com/maxjoehnk/proxy-mocks",
"author": "Max Jöhnk <[email protected]>",
"license": "MIT",
"scripts": {
"clean": "rimraf '*.{d.ts,js}'",
"prebuild": "npm run clean",
"build": "tsc --outDir .",
"test": "jest",
"lint": "eslint src --ext .ts",
"prepare": "npm run check",
"prepack": "npm run check",
"format": "dprint fmt",
"format.check": "dprint check",
"check": "npm run build && npm run lint && npm run test"
},
"files": [
"README.md",
"jest.js",
"sinon.js",
"mock.js",
"index.js"
],
"peerDependencies": {
"jest": "^26.6.3",
"sinon": "^10.0.1"
},
"devDependencies": {
"@types/jest": "^26.0.22",
"@types/sinon": "^9.0.11",
"@typescript-eslint/eslint-plugin": "^4.21.0",
"@typescript-eslint/parser": "^4.21.0",
"eslint": "^7.23.0",
"jest": "^26.6.3",
"rimraf": "^3.0.2",
"sinon": "^10.0.1",
"ts-jest": "^26.5.4",
"typescript": "^4.2.4"
},
"jest": {
"moduleFileExtensions": [
"ts",
"js",
"json"
],
"testMatch": [
"<rootDir>/src/**/*.test.ts"
],
"preset": "ts-jest",
"globals": {
"ts-jest": {
"tsconfig": "tsconfig.test.json"
}
}
},
"eslintConfig": {
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/ban-types": 0
}
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * as jest from "./jest";
export * as sinon from "./sinon";
12 changes: 12 additions & 0 deletions src/jest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Mock } from "./jest";
import { TestServiceToMock } from "./test-utils";

describe("Jest Mocks", () => {
test("accessing a method should create a stub for it", () => {
const mock = Mock.of(TestServiceToMock);

const isStub = jest.isMockFunction(mock.someMethod);

expect(isStub).toBe(true);
});
});
10 changes: 10 additions & 0 deletions src/jest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createMockImplementationWithStubFunction, MockableObject, MockPrototype } from "./mock";

import * as base from "./mock";

/**
* Mocked object with stubs generated with jest
*/
export type IMock<TObject extends MockableObject> = base.IMock<TObject, jest.Mock<TObject>>;

export const Mock: MockPrototype<jest.Mock> = createMockImplementationWithStubFunction(jest.fn);
98 changes: 98 additions & 0 deletions src/mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
export type RecursivePartial<T> = Partial<
{
[key in keyof T]: T[key] extends (...a: Array<infer U>) => unknown
? (...a: Array<U>) => RecursivePartial<ReturnType<T[key]>> | ReturnType<T[key]> // tslint:disable-line
: T[key] extends Array<unknown> ? Array<RecursivePartial<T[key][number]>>
: RecursivePartial<T[key]> | T[key];
}
>;

/**
* Mock object with generated stubs.
*
* There is no guarantee whether a property is actually a stub or not as it can be overridden at creation time.
*/
export type IMock<TObject extends MockableObject, TStub> = TObject & { [P in keyof TObject]: TStub };

/**
* Generate new mock class using given {@param stubFunction} to generate stubs
*/
export function createMockImplementationWithStubFunction<TStub>(stubFunction: () => TStub): MockPrototype<TStub> {
return class Mock {
/**
* Generate a new mock for the given class.
*
* @param clazz the class to generate a mock for.
* @param overrides can be used to set properties. They will not be replaced with stubs.
*/
public static of<TObject extends MockableObject>(
clazz: Clazz<TObject> = null,
overrides: RecursivePartial<TObject> = {},
): IMock<TObject, TStub> {
return new Proxy<IMock<TObject, TStub>>(overrides as any, {
get(target: IMock<TObject, TStub>, key: PropertyKey) {
// Angular tries to create a Set of a provided value via the following code
//
// ```javascript
// var QUOTED_KEYS = '$quoted$';
// var quotedSet = new Set(map && map[QUOTED_KEYS]);
// ```
//
// This fails when $quoted$ is a stub, so we explicitly return undefined here
if (key === "$quoted$") {
return undefined;
}
// TODO: allow configuration of Mock as a Promise
if (key === "then") {
return undefined;
}
const name: keyof TObject = key as any;
if (target[name] === undefined) {
target[name] = stubFunction() as any;
}
return target[name];
},
getPrototypeOf(): MockableObject | null {
return clazz?.prototype ?? null;
},
});
}

/**
* Generate new mock without setting the prototype (for e.g. a Typescript interface)
*
* {@param overrides} can be used to set properties. They will not be replaced with stubs.
*/
public static with<TObject extends MockableObject>(
overrides: RecursivePartial<TObject> = {},
): IMock<TObject, TStub> {
return Mock.of<TObject>(null, overrides);
}
};
}

interface Clazz<T> extends Function {
new(...args: any[]): T;
}

export interface MockPrototype<TStub> {
/**
* Generate a new mock for the given class.
*
* @param clazz the class to generate a mock for.
* @param overrides can be used to set properties. They will not be replaced with stubs.
*/
of<TObject extends MockableObject>(
clazz: Clazz<TObject>,
overrides?: RecursivePartial<TObject>,
): IMock<TObject, TStub>;

/**
* Generate new mock without setting the prototype (for e.g. a Typescript interface)
*
* {@param overrides} can be used to set properties. They will not be replaced with stubs.
*/
with<TObject extends MockableObject>(overrides: RecursivePartial<TObject>): IMock<TObject, TStub>;
}

export type MockableObject = object;
19 changes: 19 additions & 0 deletions src/sinon.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SinonStub } from "sinon";
import { Mock } from "./sinon";
import { TestServiceToMock } from "./test-utils";

describe("Sinon Mocks", () => {
test("accessing a method should create a stub for it", () => {
const mock = Mock.of(TestServiceToMock);

const isStub = isSinonStub(mock.someMethod);

expect(isStub).toBe(true);
});
});

function isSinonStub(stub: SinonStub): boolean {
// implementation detail of sinon
// sinon sets the isSinonProxy to true when it creates a stub
return (stub as any).isSinonProxy;
}
13 changes: 13 additions & 0 deletions src/sinon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { SinonStub, stub } from "sinon";
import { createMockImplementationWithStubFunction, MockPrototype } from "./mock";

import * as base from "./mock";

/**
* Mock object with generated stubs.
*
* There is no guarantee whether a property is actually a stub or not as it can be overridden at creation time.
*/
export type IMock<TObject extends base.MockableObject> = base.IMock<TObject, SinonStub>;

export const Mock: MockPrototype<SinonStub> = createMockImplementationWithStubFunction(stub);
5 changes: 5 additions & 0 deletions src/test-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class TestServiceToMock {
someMethod(arg: string): string {
return `transformed ${arg}`;
}
}
23 changes: 23 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"lib": [
"es5",
"es2015.proxy"
],
"strict": true,
"strictNullChecks": false,
"alwaysStrict": true,
"declaration": true,
"target": "es2015",
"allowJs": false,
"esModuleInterop": true,
"module": "es2020",
"moduleResolution": "node"
},
"include": [
"src/**/*.ts"
],
"exclude": [
"src/**/*.test.ts"
]
}
9 changes: 9 additions & 0 deletions tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": [
"src/**/*.ts"
]
}

0 comments on commit 14d9647

Please sign in to comment.