Skip to content

Commit

Permalink
refactor!: Remove instance
Browse files Browse the repository at this point in the history
BREAKING CHANGE: You no longer have to remember to call `instance`
before passing the mock to the code under test, because we removed it!
The object returned by `mock()` can now be passed directly to your code.
  • Loading branch information
NiGhTTraX committed Jul 15, 2022
1 parent b0e46f4 commit dc2338d
Show file tree
Hide file tree
Showing 18 changed files with 139 additions and 225 deletions.
56 changes: 25 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</div>

```typescript
import { mock, when, instance } from 'strong-mock';
import { mock, when } from 'strong-mock';

interface Foo {
bar: (x: number) => string;
Expand All @@ -16,7 +16,7 @@ const foo = mock<Foo>();

when(() => foo.bar(23)).thenReturn('I am strong!');

console.log(instance(foo).bar(23)); // 'I am strong!'
console.log(foo.bar(23)); // 'I am strong!'
```

----
Expand Down Expand Up @@ -52,7 +52,7 @@ console.log(instance(foo).bar(23)); // 'I am strong!'
- [Why do I have to set a return value even if it's `undefined`?](#why-do-i-have-to-set-a-return-value-even-if-its-undefined)
- [How do I provide a function for the mock to call?](#how-do-i-provide-a-function-for-the-mock-to-call)
- [Why does accessing an unused method throw?](#why-does-accessing-an-unused-method-throw)
- [Can I spread/enumerate a mock instance?](#can-i-spreadenumerate-a-mock-instance)
- [Can I spread/enumerate a mock?](#can-i-spreadenumerate-a-mock)
- [How can I ignore `undefined` keys when setting expectations on objects?](#how-can-i-ignore-undefined-keys-when-setting-expectations-on-objects)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Expand Down Expand Up @@ -101,12 +101,6 @@ Expectations are set by calling the mock inside a `when` callback and finishing
when(() => foo.bar(23)).thenReturn('awesome');
```

After expectations have been set you need to get an instance of the mock by calling `instance()`.

```typescript
instance(foo)
```

### Setting multiple expectations

You can set as many expectations as you want by calling `when` multiple times. If you have multiple expectations with the same arguments they will be consumed in the order they were created.
Expand All @@ -115,8 +109,8 @@ You can set as many expectations as you want by calling `when` multiple times. I
when(() => foo.bar(23)).thenReturn('awesome');
when(() => foo.bar(23)).thenReturn('even more awesome');

console.log(instance(foo).bar(23)); // awesome
console.log(instance(foo).bar(23)); // even more awesome
console.log(foo.bar(23)); // awesome
console.log(foo.bar(23)); // even more awesome
```

By default, each call is expected to be called only once. You can expect a call to be made multiple times using the [invocation count](#setting-invocation-count-expectations) helpers.
Expand All @@ -130,10 +124,10 @@ const fn = mock<(x: number) => number>();

when(() => fn(1)).thenReturn(1).between(2, 3);

console.log(instance(fn)(1)); // 1
console.log(instance(fn)(1)); // 1
console.log(instance(fn)(1)); // 1
console.log(instance(fn)(1)); // throws because the expectation is finished
console.log(fn(1)); // 1
console.log(fn(1)); // 1
console.log(fn(1)); // 1
console.log(fn(1)); // throws because the expectation is finished
```

### Mocking interfaces
Expand All @@ -151,8 +145,8 @@ const foo = mock<Foo>();
when(() => foo.bar(23)).thenReturn('awesome');
when(() => foo.baz).thenReturn(100);

console.log(instance(foo).bar(23)); // 'awesome'
console.log(instance(foo).baz); // 100
console.log(foo.bar(23)); // 'awesome'
console.log(foo.baz); // 100
```

Since the mock is type safe the compiler will guarantee that you're only mocking things that actually exist on the interface.
Expand All @@ -168,7 +162,7 @@ const fn = mock<Fn>();

when(() => fn(1)).thenReturn(2);

console.log(instance(fn)(1)); // 2
console.log(fn(1)); // 2
```

### Mocking promises
Expand All @@ -182,7 +176,7 @@ const fn = mock<Fn>();

when(() => fn(1)).thenResolve(2);

console.log(await instance(fn)()); // 2
console.log(await fn()); // 2
```

### Throwing errors
Expand Down Expand Up @@ -218,7 +212,7 @@ It will also throw if any unexpected calls happened that were maybe caught in th
const fn = mock<() => void>();

try {
instance(fn)(); // throws because the call is unexpected
fn(); // throws because the call is unexpected
} catch(e) {
// your code might transition to an error state here
}
Expand All @@ -241,7 +235,7 @@ when(() => fn(1)).thenReturn(1);

reset(fn);

instance(fn)(1); // throws
fn(1); // throws
```

If you create common mocks that are shared by multiple tests you should reset them before using them e.g. in a `beforeEach` hook. You can use `resetAll()` to reset all existing mocks.
Expand All @@ -260,7 +254,7 @@ when(() => fn(
It.isObject({ values: [1, 2, 3] })
)).thenReturn('matched!');

console.log(instance(fn)(
console.log(fn(
123,
{ values: [1, 2, 3], labels: ['a', 'b', 'c'] })
); // 'matched!'
Expand Down Expand Up @@ -317,7 +311,7 @@ const fn = mock<(cb: Cb) => number>();
const matcher = It.willCapture<Cb>();
when(() => fn(matcher)).thenReturn(42);

console.log(instance(fn)(23, (x) => x + 1)); // 42
console.log(fn(23, (x) => x + 1)); // 42
console.log(matcher.value?.(3)); // 4
```

Expand All @@ -326,7 +320,7 @@ console.log(matcher.value?.(3)); // 4
You can override the default matcher that will be used when setting expectations with non-matcher values e.g. `42` or `{ foo: "bar" }`.

```ts
import { mock, when, instance, It, setDefaults } from 'strong-mock';
import { mock, when, It, setDefaults } from 'strong-mock';

// Use strict equality instead of deep equality.
setDefaults({
Expand All @@ -336,7 +330,7 @@ setDefaults({
const fn = mock<(x: number[]) => boolean>();
when(() => fn([1, 2, 3])).thenReturn(true);

instance(fn)([1, 2, 3]); // throws because different arrays
fn([1, 2, 3]); // throws because different arrays
```

## FAQ
Expand Down Expand Up @@ -374,7 +368,7 @@ const foo = mock<Foo>();

when(() => foo.bar).thenReturn(x => `called ${x}`);

console.log(instance(foo).bar(23)); // 'called 23'
console.log(foo.bar(23)); // 'called 23'
```

The function in `thenReturn()` will be type checked against the actual interface, so you can make sure you're passing in an implementation that makes sense. Moreover, refactoring the interface will also refactor the expectation (in a capable IDE).
Expand Down Expand Up @@ -406,7 +400,7 @@ const foo = mock<Foo>();
when(() => foo.bar()).thenReturn(42);

// Throws with unexpected access on `baz`.
doFoo(instance(foo), { callBaz: false });
doFoo(foo, { callBaz: false });
```

To work around this, either change your code to avoid destructuring
Expand All @@ -427,17 +421,17 @@ or set a dummy expectation on the methods you're not interested in during the te
when(() => foo.baz()).thenThrow('should not be called').anyTimes();
```

### Can I spread/enumerate a mock instance?
### Can I spread/enumerate a mock?

Yes, and you will only get the properties that have expectations on them.

```typescript
const foo = mock<{ bar: number; baz: number }>();
when(() => foo.bar).thenReturn(42);

console.log(Object.keys(instance(foo))); // ['bar']
console.log(Object.keys(foo)); // ['bar']

const foo2 = { ...instance(foo) };
const foo2 = { ...foo };

console.log(foo2.bar); // 42
console.log(foo2.baz); // undefined
Expand All @@ -453,7 +447,7 @@ const fn = mock<(x: { foo: string }) => boolean>();

when(() => fn(It.deepEquals({ foo: "bar" }, { strict: false }))).thenReturn(true);

instance(fn)({ foo: "bar", baz: undefined }) === true
fn({ foo: "bar", baz: undefined }) === true
```

You can also set this behavior to be the default by using [`setDefaults`](#overriding-default-matcher):
Expand Down
10 changes: 2 additions & 8 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ export class UnfinishedExpectation extends Error {
${pendingExpectation.toJSON()}
Please finish it by setting a return value even if the value
is undefined.
This may have been caused by using the mock without getting
an instance from it first. Please use instance(mock) and use
that value in the code you're testing.`);
is undefined.`);
}
}

Expand Down Expand Up @@ -118,9 +114,7 @@ const parentMock = mock<T1>();
const childMock = mock<T2>();
when(() => childMock${printProperty(childProp)}).thenReturn(...);
when(() => parentMock${printProperty(
parentProp
)}).thenReturn(instance(childMock))
when(() => parentMock${printProperty(parentProp)}).thenReturn(childMock)
`;

super(
Expand Down
26 changes: 13 additions & 13 deletions src/expectation/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import { isMatcher, Matcher, MATCHER_SYMBOL, TypeMatcher } from './matcher';
* const fn = mock<(x: number) => number>();
* when(() => fn(It.matches(x => x >= 0))).returns(42);
*
* instance(fn)(2) === 42
* instance(fn)(-1) // throws
* fn(2) === 42
* fn(-1) // throws
*/
const matches = <T>(
cb: (actual: T) => boolean,
Expand Down Expand Up @@ -94,7 +94,7 @@ const is = <T = unknown>(expected: T): TypeMatcher<T> =>
* const fn = mock<(x: number, y: string) => number>();
* when(() => fn(It.isAny(), It.isAny())).thenReturn(1);
*
* instance(fn)(23, 'foobar') === 1
* fn(23, 'foobar') === 1
*/
const isAny = (): TypeMatcher<any> =>
matches(() => true, { toJSON: () => 'anything' });
Expand All @@ -114,8 +114,8 @@ type DeepPartial<T> = T extends object
* const fn = mock<(foo: { x: number, y: number }) => number>();
* when(() => fn(It.isObject({ x: 23 }))).returns(42);
*
* instance(fn)({ x: 100, y: 200 }) // throws
* instance(fn)({ x: 23, y: 200 }) // returns 42
* fn({ x: 100, y: 200 }) // throws
* fn({ x: 23, y: 200 }) // returns 42
*
* @example
* It.isObject({ foo: It.isString() })
Expand Down Expand Up @@ -143,8 +143,8 @@ const isObject = <T extends object, K extends DeepPartial<T>>(
* const fn = mock<(x: number) => number>();
* when(() => fn(It.isNumber())).returns(42);
*
* instance(fn)(20.5) === 42
* instance(fn)(NaN) // throws
* fn(20.5) === 42
* fn(NaN) // throws
*/
const isNumber = (): TypeMatcher<number> =>
matches((actual) => typeof actual === 'number' && !Number.isNaN(actual), {
Expand All @@ -161,8 +161,8 @@ const isNumber = (): TypeMatcher<number> =>
* const fn = mock<(x: string, y: string) => number>();
* when(() => fn(It.isString(), It.isString({ containing: 'bar' }))).returns(42);
*
* instance(fn)('foo', 'baz') // throws
* instance(fn)('foo', 'bar') === 42
* fn('foo', 'baz') // throws
* fn('foo', 'bar') === 42
*/
const isString = ({
matching,
Expand Down Expand Up @@ -206,9 +206,9 @@ const isString = ({
* when(() => fn(It.isArray())).thenReturn(1);
* when(() => fn(It.isArray([2, 3]))).thenReturn(2);
*
* instance(fn)({ length: 1, 0: 42 }) // throws
* instance(fn)([]) === 1
* instance(fn)([3, 2, 1]) === 2
* fn({ length: 1, 0: 42 }) // throws
* fn([]) === 1
* fn([3, 2, 1]) === 2
*
* @example
* It.isArray([It.isString({ containing: 'foobar' })])
Expand Down Expand Up @@ -255,7 +255,7 @@ const isArray = <T extends any[]>(containing?: T): TypeMatcher<T> =>
* const matcher = It.willCapture();
* when(() => fn(matcher)).thenReturn();
*
* instance(fn)(x => x + 1);
* fn(x => x + 1);
* matcher.value?.(3) === 4
*/
const willCapture = <T = unknown>(
Expand Down
6 changes: 3 additions & 3 deletions src/expectation/repository/base-repository.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { returnOrThrow } from '../../instance/instance';
import { returnOrThrow } from '../../mock/stub';
import { Property } from '../../proxy';
import { ApplyProp, Expectation, ReturnValue } from '../expectation';
import { MATCHER_SYMBOL } from '../matcher';
import { CallMap, ExpectationRepository } from './expectation-repository';
import { Property } from '../../proxy';

export type CountableExpectation = {
expectation: Expectation;
Expand Down Expand Up @@ -64,7 +64,7 @@ export abstract class BaseRepository implements ExpectationRepository {
this.recordExpected(property, args);
this.countAndConsume(callExpectation);

// TODO: this is duplicated in instance
// TODO: this is duplicated in stub
return returnOrThrow(callExpectation.expectation.returnValue);
}

Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* istanbul ignore file */
export { mock } from './mock/mock';
export { when } from './when/when';
export { instance } from './instance/instance';
export { reset, resetAll } from './verify/reset';
export { verify, verifyAll } from './verify/verify';
export { It } from './expectation/it';
Expand Down
26 changes: 0 additions & 26 deletions src/instance/instance.spec.ts

This file was deleted.

0 comments on commit dc2338d

Please sign in to comment.