Skip to content

Commit

Permalink
Figured out API and wrote readme
Browse files Browse the repository at this point in the history
  • Loading branch information
mattpocock committed Apr 11, 2023
1 parent cc94b24 commit 1152222
Show file tree
Hide file tree
Showing 11 changed files with 139 additions and 218 deletions.
5 changes: 5 additions & 0 deletions .changeset/seven-bikes-explode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"shoehorn": minor
---

Figured out name for library and reduced API surface area.
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
{
"name": "@total-typescript/mock-utils",
"name": "shoehorn",
"version": "0.0.2",
"description": "Work seamlessly with TypeScript in test suites.",
"description": "Work seamlessly with partial mocks in TypeScript.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"module": "dist/index.mjs",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./package.json": "./package.json"
},
"scripts": {
"test": "vitest run",
"dev": "vitest",
Expand Down
123 changes: 100 additions & 23 deletions rEaDMe.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
# `mock-utils`
# `shoehorn`

`mock-utils` solves the 'as' problem in TypeScript tests.
`shoehorn` lets you pass partial data in tests, while keeping TypeScript happy.

```ts
type User = {
id: string;
name: string;
type Request = {
body: {
id: string;
};
// Imagine oodles of other properties...
};

it("Should return an id", () => {
// Even though we only care about User.id for
// this test, we need to pass in the whole User
it("Should get the user", () => {
// Even though we only care about body.id for
// this test, we need to pass in the whole Request
// object
getUserId({
id: "123",
} as User);
getUser({
body: {
id: "123",
},
} as Request);
});
```

Expand All @@ -25,34 +28,108 @@ it("Should return an id", () => {
- You need to _manually_ specify the type you want to assert to
- For testing with incorrect data, you need to 'double-as' (`as unknown as User`)

`mock-utils` gives you some first-class primitives for _safely_ providing incomplete data to tests.
`shoehorn` gives you some first-class primitives for _safely_ providing incomplete data to tests.

```ts
import { fromPartial } from "@total-typescript/mock-utils";
import { fromPartial } from "@total-typescript/shoehorn";

it("Should return an id", () => {
it("Should get the user", () => {
getUserId(
fromPartial({
id: "123",
})
body: {
id: "123",
},
}),
);
});
```

### But isn't passing partial data to tests bad?

Yes, in general. Having to pass huge objects to tests is a sign that your types are too loose. Ideally, every function should only specify the data it needs.

Unfortunately, we live in the real world. There are many cases where `shoehorn` is the best choice:

- **Legacy codebases**: If you're working on a large codebase, you might not have the time to refactor everything to be perfect.
- **Third-party libraries**: If you're using a third-party library, you might not be able to alter the types without needless wrapper functions.

## API

### `fromExact`
For each example below, imagine that the following types are defined:

```ts
type Request = {
body: {
id: string;
};
// Imagine oodles of other properties...
};

// The function we're testing
const requiresRequest = (request: Request) => {};
```

### `fromPartial`

Lets you pass a deep partial to a slot expecting a type.

```ts
import { fromPartial } from "@total-typescript/shoehorn";

TODO
requiresRequest(
fromPartial({
body: {
id: "123",
},
}),
);
```

It'll fail if you pass a type that doesn't match the one expected:

```ts
// Type "1234123" has no properties in common
// with type 'PartialObjectDeep<Request>'
requiresRequest(fromPartial("1234123"));
```

### `fromAny`

TODO
Lets you pass anything to a slot, while still giving you autocomplete on the original type:

### `fromPartial`
```ts
import { fromAny } from "@total-typescript/shoehorn";

requiresRequest(
fromAny({
body: {
id: 124123,
},
}),
);
```

It WILL NOT FAIL if you pass something that doesn't match.

```ts
// All good!
requiresRequest(fromPartial("1234123"));
```

TODO
### `fromExact`

## `createFixture`
A convenience method for forcing you to pass all the properties of a type. Useful for when you want to swap in and out of `fromPartial`/`fromAny`:

TODO
```ts
import { fromExact } from "@total-typescript/shoehorn";

requiresRequest(
// Will fail! We're not passing all the oodles of
// properties of Request
fromExact({
body: {
id: 124123,
},
}),
);
```
32 changes: 0 additions & 32 deletions src/createFixture.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from "./types";
export * from "./utils";
export * from "./createFixture";
25 changes: 9 additions & 16 deletions src/playground.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import { it } from "vitest";
import { createFixture } from "./createFixture";
import { fromAny, fromPartial, fromExact } from "./utils";
import { fromAny, fromPartial } from "./utils";

type User = {
id: string;
name: string;
type Request = {
body: {
id: string;
};
// Imagine oodles of other properties...
};

const func = (user: User) => {};
// The function we're testing
const requiresRequest = (request: Request) => {};

const baseUser = createFixture<User>().set({
id: "123123",
name: "awdawd",
});

it("Should work", () => {
func(baseUser.fromExact({}));
});

const getUserId = (user: User) => {};
requiresRequest(fromAny("1234123"));
16 changes: 3 additions & 13 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,10 @@ export type PartialDeep<T> = T extends (...args: any[]) => unknown
? readonly ItemType[] extends T // Differentiate readonly and mutable arrays
? ReadonlyArray<PartialDeep<ItemType | undefined>>
: Array<PartialDeep<ItemType | undefined>>
: PartialObjectDeep<T> // Tuples behave properly
: PartialObjectDeep<T>
: PartialDeepObject<T> // Tuples behave properly
: PartialDeepObject<T>
: T;

export type PartialObjectDeep<ObjectType extends object> = {
export type PartialDeepObject<ObjectType extends object> = {
[KeyType in keyof ObjectType]?: PartialDeep<ObjectType[KeyType]>;
};

export interface Base<T, Default = {}> {
get: () => T;
set: <NewDefault extends PartialDeep<T>>(
base: NewDefault
) => Base<T, NewDefault>;
fromExact: (mock: Omit<T, keyof Default> & Partial<Default>) => T;
fromPartial: (mock: PartialDeep<T>) => T;
fromAny: <U>(mock: U | NoInfer<T>) => T;
}
14 changes: 3 additions & 11 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { Base, NoInfer, PartialDeep } from "./types";
import { NoInfer, PartialDeep } from "./types";

/**
* Lets you pass a deep partial of a type to a function
* Lets you pass a deep partial to a slot expecting a type.
*
* @returns whatever you pass in
*/
export const fromPartial = <T>(mock: PartialDeep<NoInfer<T>>): T => {
// const proxy = new Proxy(mock, {
// get(target, p, receiver) {
// if (typeof p !== "symbol" && !(p in target)) {
// throw new Error(`${String(p)} not found in mocked object`);
// }
// return Reflect.get(target, p, receiver);
// },
// });
return mock as T;
};

Expand All @@ -28,7 +20,7 @@ export const fromAny = <T, U>(mock: U | NoInfer<T>): T => {
};

/**
* Forces you to pass the entire object
* Forces you to pass the exact type of the thing the slot requires
*
* @returns whatever you pass in
*/
Expand Down
Loading

0 comments on commit 1152222

Please sign in to comment.