Skip to content

Commit 14d9647

Browse files
committed
chore: initial commit
implement jest and sinon versions of mocks
0 parents  commit 14d9647

File tree

13 files changed

+328
-0
lines changed

13 files changed

+328
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
yarn.lock
3+
*.js
4+
*.d.ts
5+
!jest.config.js
6+
!.eslintrc.js

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# proxy-mocks
2+
3+
Generate mocks for any class or object.
4+
5+
## Example
6+
7+
```typescript
8+
// import { IMock, Mock } from 'proxy-mocks/jest';
9+
import { IMock, Mock } from "proxy-mocks/sinon";
10+
import Dependency from "./dependency";
11+
import Implementation from "./implementation";
12+
13+
describe("Implementation", () => {
14+
let dependency: IMock<Dependency>;
15+
16+
let implementation: Implementation;
17+
18+
beforeEach(() => {
19+
dependency = Mock.of(Dependency);
20+
21+
implementation = new Implementation(dependency);
22+
});
23+
24+
test("your test", () => {
25+
dependency.someMethod.returns("your result");
26+
27+
const result = implementation.anotherMethod();
28+
29+
expect(result).toEqual("your result");
30+
});
31+
});
32+
```

dprint.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"$schema": "https://dprint.dev/schemas/v0.json",
3+
"projectType": "openSource",
4+
"incremental": true,
5+
"typescript": {
6+
"indentWidth": 2
7+
},
8+
"json": {
9+
},
10+
"markdown": {
11+
},
12+
"includes": ["**/*.{ts,tsx,js,jsx,json,md}"],
13+
"excludes": [
14+
"**/node_modules",
15+
"**/*-lock.json",
16+
"src/**/*.js",
17+
"**/*.d.ts"
18+
],
19+
"plugins": [
20+
"https://plugins.dprint.dev/typescript-0.44.0.wasm",
21+
"https://plugins.dprint.dev/json-0.10.1.wasm",
22+
"https://plugins.dprint.dev/markdown-0.6.2.wasm"
23+
]
24+
}

package.json

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{
2+
"name": "proxy-mocks",
3+
"version": "0.1.0",
4+
"description": "Provide mocks utilizing the Proxy API",
5+
"main": "index.js",
6+
"repository": "https://github.com/maxjoehnk/proxy-mocks",
7+
"author": "Max Jöhnk <[email protected]>",
8+
"license": "MIT",
9+
"scripts": {
10+
"clean": "rimraf '*.{d.ts,js}'",
11+
"prebuild": "npm run clean",
12+
"build": "tsc --outDir .",
13+
"test": "jest",
14+
"lint": "eslint src --ext .ts",
15+
"prepare": "npm run check",
16+
"prepack": "npm run check",
17+
"format": "dprint fmt",
18+
"format.check": "dprint check",
19+
"check": "npm run build && npm run lint && npm run test"
20+
},
21+
"files": [
22+
"README.md",
23+
"jest.js",
24+
"sinon.js",
25+
"mock.js",
26+
"index.js"
27+
],
28+
"peerDependencies": {
29+
"jest": "^26.6.3",
30+
"sinon": "^10.0.1"
31+
},
32+
"devDependencies": {
33+
"@types/jest": "^26.0.22",
34+
"@types/sinon": "^9.0.11",
35+
"@typescript-eslint/eslint-plugin": "^4.21.0",
36+
"@typescript-eslint/parser": "^4.21.0",
37+
"eslint": "^7.23.0",
38+
"jest": "^26.6.3",
39+
"rimraf": "^3.0.2",
40+
"sinon": "^10.0.1",
41+
"ts-jest": "^26.5.4",
42+
"typescript": "^4.2.4"
43+
},
44+
"jest": {
45+
"moduleFileExtensions": [
46+
"ts",
47+
"js",
48+
"json"
49+
],
50+
"testMatch": [
51+
"<rootDir>/src/**/*.test.ts"
52+
],
53+
"preset": "ts-jest",
54+
"globals": {
55+
"ts-jest": {
56+
"tsconfig": "tsconfig.test.json"
57+
}
58+
}
59+
},
60+
"eslintConfig": {
61+
"root": true,
62+
"parser": "@typescript-eslint/parser",
63+
"plugins": [
64+
"@typescript-eslint"
65+
],
66+
"extends": [
67+
"eslint:recommended",
68+
"plugin:@typescript-eslint/recommended"
69+
],
70+
"rules": {
71+
"@typescript-eslint/no-explicit-any": 0,
72+
"@typescript-eslint/ban-types": 0
73+
}
74+
}
75+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * as jest from "./jest";
2+
export * as sinon from "./sinon";

src/jest.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Mock } from "./jest";
2+
import { TestServiceToMock } from "./test-utils";
3+
4+
describe("Jest Mocks", () => {
5+
test("accessing a method should create a stub for it", () => {
6+
const mock = Mock.of(TestServiceToMock);
7+
8+
const isStub = jest.isMockFunction(mock.someMethod);
9+
10+
expect(isStub).toBe(true);
11+
});
12+
});

src/jest.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { createMockImplementationWithStubFunction, MockableObject, MockPrototype } from "./mock";
2+
3+
import * as base from "./mock";
4+
5+
/**
6+
* Mocked object with stubs generated with jest
7+
*/
8+
export type IMock<TObject extends MockableObject> = base.IMock<TObject, jest.Mock<TObject>>;
9+
10+
export const Mock: MockPrototype<jest.Mock> = createMockImplementationWithStubFunction(jest.fn);

src/mock.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
export type RecursivePartial<T> = Partial<
2+
{
3+
[key in keyof T]: T[key] extends (...a: Array<infer U>) => unknown
4+
? (...a: Array<U>) => RecursivePartial<ReturnType<T[key]>> | ReturnType<T[key]> // tslint:disable-line
5+
: T[key] extends Array<unknown> ? Array<RecursivePartial<T[key][number]>>
6+
: RecursivePartial<T[key]> | T[key];
7+
}
8+
>;
9+
10+
/**
11+
* Mock object with generated stubs.
12+
*
13+
* There is no guarantee whether a property is actually a stub or not as it can be overridden at creation time.
14+
*/
15+
export type IMock<TObject extends MockableObject, TStub> = TObject & { [P in keyof TObject]: TStub };
16+
17+
/**
18+
* Generate new mock class using given {@param stubFunction} to generate stubs
19+
*/
20+
export function createMockImplementationWithStubFunction<TStub>(stubFunction: () => TStub): MockPrototype<TStub> {
21+
return class Mock {
22+
/**
23+
* Generate a new mock for the given class.
24+
*
25+
* @param clazz the class to generate a mock for.
26+
* @param overrides can be used to set properties. They will not be replaced with stubs.
27+
*/
28+
public static of<TObject extends MockableObject>(
29+
clazz: Clazz<TObject> = null,
30+
overrides: RecursivePartial<TObject> = {},
31+
): IMock<TObject, TStub> {
32+
return new Proxy<IMock<TObject, TStub>>(overrides as any, {
33+
get(target: IMock<TObject, TStub>, key: PropertyKey) {
34+
// Angular tries to create a Set of a provided value via the following code
35+
//
36+
// ```javascript
37+
// var QUOTED_KEYS = '$quoted$';
38+
// var quotedSet = new Set(map && map[QUOTED_KEYS]);
39+
// ```
40+
//
41+
// This fails when $quoted$ is a stub, so we explicitly return undefined here
42+
if (key === "$quoted$") {
43+
return undefined;
44+
}
45+
// TODO: allow configuration of Mock as a Promise
46+
if (key === "then") {
47+
return undefined;
48+
}
49+
const name: keyof TObject = key as any;
50+
if (target[name] === undefined) {
51+
target[name] = stubFunction() as any;
52+
}
53+
return target[name];
54+
},
55+
getPrototypeOf(): MockableObject | null {
56+
return clazz?.prototype ?? null;
57+
},
58+
});
59+
}
60+
61+
/**
62+
* Generate new mock without setting the prototype (for e.g. a Typescript interface)
63+
*
64+
* {@param overrides} can be used to set properties. They will not be replaced with stubs.
65+
*/
66+
public static with<TObject extends MockableObject>(
67+
overrides: RecursivePartial<TObject> = {},
68+
): IMock<TObject, TStub> {
69+
return Mock.of<TObject>(null, overrides);
70+
}
71+
};
72+
}
73+
74+
interface Clazz<T> extends Function {
75+
new(...args: any[]): T;
76+
}
77+
78+
export interface MockPrototype<TStub> {
79+
/**
80+
* Generate a new mock for the given class.
81+
*
82+
* @param clazz the class to generate a mock for.
83+
* @param overrides can be used to set properties. They will not be replaced with stubs.
84+
*/
85+
of<TObject extends MockableObject>(
86+
clazz: Clazz<TObject>,
87+
overrides?: RecursivePartial<TObject>,
88+
): IMock<TObject, TStub>;
89+
90+
/**
91+
* Generate new mock without setting the prototype (for e.g. a Typescript interface)
92+
*
93+
* {@param overrides} can be used to set properties. They will not be replaced with stubs.
94+
*/
95+
with<TObject extends MockableObject>(overrides: RecursivePartial<TObject>): IMock<TObject, TStub>;
96+
}
97+
98+
export type MockableObject = object;

src/sinon.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { SinonStub } from "sinon";
2+
import { Mock } from "./sinon";
3+
import { TestServiceToMock } from "./test-utils";
4+
5+
describe("Sinon Mocks", () => {
6+
test("accessing a method should create a stub for it", () => {
7+
const mock = Mock.of(TestServiceToMock);
8+
9+
const isStub = isSinonStub(mock.someMethod);
10+
11+
expect(isStub).toBe(true);
12+
});
13+
});
14+
15+
function isSinonStub(stub: SinonStub): boolean {
16+
// implementation detail of sinon
17+
// sinon sets the isSinonProxy to true when it creates a stub
18+
return (stub as any).isSinonProxy;
19+
}

src/sinon.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { SinonStub, stub } from "sinon";
2+
import { createMockImplementationWithStubFunction, MockPrototype } from "./mock";
3+
4+
import * as base from "./mock";
5+
6+
/**
7+
* Mock object with generated stubs.
8+
*
9+
* There is no guarantee whether a property is actually a stub or not as it can be overridden at creation time.
10+
*/
11+
export type IMock<TObject extends base.MockableObject> = base.IMock<TObject, SinonStub>;
12+
13+
export const Mock: MockPrototype<SinonStub> = createMockImplementationWithStubFunction(stub);

0 commit comments

Comments
 (0)