Skip to content

Commit

Permalink
refactor!: Require callback when setting expectations
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Expectations now have to be wrapped in a callback
inside `when`. This change is necessary to remove the `instance`
function. Before: `when(foo.bar())`. After: `when(() => foo.bar())`.
  • Loading branch information
NiGhTTraX committed Jul 15, 2022
1 parent 2cda37c commit b0e46f4
Show file tree
Hide file tree
Showing 17 changed files with 130 additions and 114 deletions.
52 changes: 26 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface Foo {

const foo = mock<Foo>();

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

console.log(instance(foo).bar(23)); // 'I am strong!'
```
Expand Down Expand Up @@ -95,10 +95,10 @@ strong-mock requires an environment that supports the [ES6 Proxy object](https:/

### Setting expectations

Expectations are set by calling the mock inside a `when()` call and finishing it by setting a return value.
Expectations are set by calling the mock inside a `when` callback and finishing it by setting a return value.

```typescript
when(foo.bar(23)).thenReturn('awesome');
when(() => foo.bar(23)).thenReturn('awesome');
```

After expectations have been set you need to get an instance of the mock by calling `instance()`.
Expand All @@ -109,11 +109,11 @@ 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.
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.

```typescript
when(foo.bar(23)).thenReturn('awesome');
when(foo.bar(23)).thenReturn('even more awesome');
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
Expand All @@ -128,7 +128,7 @@ You can expect a call to be made multiple times by using the invocation count he
```typescript
const fn = mock<(x: number) => number>();

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

console.log(instance(fn)(1)); // 1
console.log(instance(fn)(1)); // 1
Expand All @@ -148,8 +148,8 @@ interface Foo {

const foo = mock<Foo>();

when(foo.bar(23)).thenReturn('awesome');
when(foo.baz).thenReturn(100);
when(() => foo.bar(23)).thenReturn('awesome');
when(() => foo.baz).thenReturn(100);

console.log(instance(foo).bar(23)); // 'awesome'
console.log(instance(foo).baz); // 100
Expand All @@ -166,7 +166,7 @@ type Fn = (x: number) => number;

const fn = mock<Fn>();

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

console.log(instance(fn)(1)); // 2
```
Expand All @@ -180,7 +180,7 @@ type Fn = (x: number) => Promise<number>;

const fn = mock<Fn>();

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

console.log(await instance(fn)()); // 2
```
Expand All @@ -194,8 +194,8 @@ type FnWithPromise = (x: number) => Promise<void>;
const fn = mock<Fn>();
const fnWithPromise = mock<FnWithPromise>();

when(fn(1)).thenThrow();
when(fnWithPromise(1)).thenReject();
when(() => fn(1)).thenThrow();
when(() => fnWithPromise(1)).thenReject();
```

You'll notice there is no `never()` helper - if you expect a call to not be made simply don't set an expectation on it and the mock will throw if the call happens.
Expand All @@ -207,7 +207,7 @@ Calling `verify(mock)` will make sure that all expectations set on `mock` have b
```typescript
const fn = mock<(x: number) => number>();

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

verify(fn); // throws
```
Expand Down Expand Up @@ -237,7 +237,7 @@ You can remove all expectations from a mock by using the `reset()` method:
```typescript
const fn = mock<(x: number) => number>();

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

reset(fn);

Expand All @@ -255,7 +255,7 @@ const fn = mock<
(x: number, data: { values: number[]; labels: string[] }) => string
>();

when(fn(
when(() => fn(
It.isAny(),
It.isObject({ values: [1, 2, 3] })
)).thenReturn('matched!');
Expand Down Expand Up @@ -301,7 +301,7 @@ You can create arbitrarily complex and type safe matchers with `It.matches(cb)`:
```typescript
const fn = mock<(x: number, y: number[]) => string>();

when(fn(
when(() => fn(
It.matches(x => x > 0),
It.matches(y => y.includes(42))
)).thenReturn('matched');
Expand All @@ -315,7 +315,7 @@ type Cb = (value: number) => number;
const fn = mock<(cb: Cb) => number>();

const matcher = It.willCapture<Cb>();
when(fn(matcher)).thenReturn(42);
when(() => fn(matcher)).thenReturn(42);

console.log(instance(fn)(23, (x) => x + 1)); // 42
console.log(matcher.value?.(3)); // 4
Expand All @@ -334,7 +334,7 @@ setDefaults({
})

const fn = mock<(x: number[]) => boolean>();
when(fn([1, 2, 3])).thenReturn(true);
when(() => fn([1, 2, 3])).thenReturn(true);

instance(fn)([1, 2, 3]); // throws because different arrays
```
Expand All @@ -359,11 +359,11 @@ You currently can't do that. Please use a normal method instead e.g. `setFoo()`

### Why do I have to set a return value even if it's `undefined`?

To make side effects explicit and to prevent future refactoring headaches. If you had just `when(fn())` and you later changed `fn()` to return a `number` then your expectation would become incorrect and the compiler couldn't check that for you.
To make side effects explicit and to prevent future refactoring headaches. If you had just `when(() => fn())` and you later changed `fn()` to return a `number` then your expectation would become incorrect and the compiler couldn't check that for you.

### How do I provide a function for the mock to call?

There is no `thenCall()` method because it can't be safely typed - the type for `thenReturn()` is inferred from the return type in `when()`, meaning that the required type would be the return value for the function, not the function itself. However, we can leverage this by setting an expectation on the function property instead:
There is no `thenCall()` method because it can't be safely typed - the type for `thenReturn()` is inferred from the return type in `when`, meaning that the required type would be the return value for the function, not the function itself. However, we can leverage this by setting an expectation on the function property instead:

```typescript
interface Foo {
Expand All @@ -372,7 +372,7 @@ interface Foo {

const foo = mock<Foo>();

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

console.log(instance(foo).bar(23)); // 'called 23'
```
Expand Down Expand Up @@ -403,7 +403,7 @@ function doFoo(foo: Foo, { callBaz }: { callBaz: boolean }) {
}

const foo = mock<Foo>();
when(foo.bar()).thenReturn(42);
when(() => foo.bar()).thenReturn(42);

// Throws with unexpected access on `baz`.
doFoo(instance(foo), { callBaz: false });
Expand All @@ -424,7 +424,7 @@ function doFoo(foo: Foo, callBaz: boolean) {
or set a dummy expectation on the methods you're not interested in during the test.

```typescript
when(foo.baz()).thenThrow('should not be called').anyTimes();
when(() => foo.baz()).thenThrow('should not be called').anyTimes();
```

### Can I spread/enumerate a mock instance?
Expand All @@ -433,7 +433,7 @@ 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);
when(() => foo.bar).thenReturn(42);

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

Expand All @@ -451,7 +451,7 @@ Use the `It.deepEquals` matcher explicitly inside `when` and pass `{ strict: fal
```ts
const fn = mock<(x: { foo: string }) => boolean>();

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

instance(fn)({ foo: "bar", baz: undefined }) === true
```
Expand Down
19 changes: 11 additions & 8 deletions src/errors.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable class-methods-use-this */
import { expectAnsilessContain, expectAnsilessEqual } from '../tests/ansiless';
import { SM } from '../tests/old';
import {
NestedWhen,
UnexpectedAccess,
Expand All @@ -8,17 +10,15 @@ import {
UnmetExpectations,
} from './errors';
import { Expectation } from './expectation/expectation';
import {
spyExpectationFactory,
SpyPendingExpectation,
} from './expectation/expectation.mocks';
import {
CallMap,
ExpectationRepository,
} from './expectation/repository/expectation-repository';
import { RepoSideEffectPendingExpectation } from './when/pending-expectation';
import { expectAnsilessContain, expectAnsilessEqual } from '../tests/ansiless';
import {
spyExpectationFactory,
SpyPendingExpectation,
} from './expectation/expectation.mocks';
import { SM } from '../tests/old';

describe('errors', () => {
describe('PendingExpectation', () => {
Expand Down Expand Up @@ -188,8 +188,11 @@ foobar`
it('should print the nested property', () => {
const error = new NestedWhen('foo', Symbol('bar'));

expectAnsilessContain(error.message, `when(parentMock.foo)`);
expectAnsilessContain(error.message, `when(childMock[Symbol(bar)])`);
expectAnsilessContain(error.message, `when(() => parentMock.foo)`);
expectAnsilessContain(
error.message,
`when(() => childMock[Symbol(bar)])`
);
});
});
});
8 changes: 5 additions & 3 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { EXPECTED_COLOR } from 'jest-matcher-utils';
import { Expectation } from './expectation/expectation';
import { CallMap } from './expectation/repository/expectation-repository';
import { PendingExpectation } from './when/pending-expectation';
import { printCall, printProperty, printRemainingExpectations } from './print';
import { Property } from './proxy';
import { PendingExpectation } from './when/pending-expectation';

export class UnfinishedExpectation extends Error {
constructor(pendingExpectation: PendingExpectation) {
Expand Down Expand Up @@ -117,8 +117,10 @@ export class NestedWhen extends Error {
const parentMock = mock<T1>();
const childMock = mock<T2>();
when(childMock${printProperty(childProp)}).thenReturn(...);
when(parentMock${printProperty(parentProp)}).thenReturn(instance(childMock))
when(() => childMock${printProperty(childProp)}).thenReturn(...);
when(() => parentMock${printProperty(
parentProp
)}).thenReturn(instance(childMock))
`;

super(
Expand Down
16 changes: 8 additions & 8 deletions src/expectation/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { isMatcher, Matcher, MATCHER_SYMBOL, TypeMatcher } from './matcher';
*
* @example
* const fn = mock<(x: number) => number>();
* when(fn(It.matches(x => x >= 0))).returns(42);
* when(() => fn(It.matches(x => x >= 0))).returns(42);
*
* instance(fn)(2) === 42
* instance(fn)(-1) // throws
Expand Down Expand Up @@ -92,7 +92,7 @@ const is = <T = unknown>(expected: T): TypeMatcher<T> =>
*
* @example
* const fn = mock<(x: number, y: string) => number>();
* when(fn(It.isAny(), It.isAny())).thenReturn(1);
* when(() => fn(It.isAny(), It.isAny())).thenReturn(1);
*
* instance(fn)(23, 'foobar') === 1
*/
Expand All @@ -112,7 +112,7 @@ type DeepPartial<T> = T extends object
*
* @example
* const fn = mock<(foo: { x: number, y: number }) => number>();
* when(fn(It.isObject({ x: 23 }))).returns(42);
* when(() => fn(It.isObject({ x: 23 }))).returns(42);
*
* instance(fn)({ x: 100, y: 200 }) // throws
* instance(fn)({ x: 23, y: 200 }) // returns 42
Expand Down Expand Up @@ -141,7 +141,7 @@ const isObject = <T extends object, K extends DeepPartial<T>>(
*
* @example
* const fn = mock<(x: number) => number>();
* when(fn(It.isNumber())).returns(42);
* when(() => fn(It.isNumber())).returns(42);
*
* instance(fn)(20.5) === 42
* instance(fn)(NaN) // throws
Expand All @@ -159,7 +159,7 @@ const isNumber = (): TypeMatcher<number> =>
*
* @example
* const fn = mock<(x: string, y: string) => number>();
* when(fn(It.isString(), It.isString({ containing: 'bar' }))).returns(42);
* when(() => fn(It.isString(), It.isString({ containing: 'bar' }))).returns(42);
*
* instance(fn)('foo', 'baz') // throws
* instance(fn)('foo', 'bar') === 42
Expand Down Expand Up @@ -203,8 +203,8 @@ const isString = ({
*
* @example
* const fn = mock<(arr: number[]) => number>();
* when(fn(It.isArray())).thenReturn(1);
* when(fn(It.isArray([2, 3]))).thenReturn(2);
* when(() => fn(It.isArray())).thenReturn(1);
* when(() => fn(It.isArray([2, 3]))).thenReturn(2);
*
* instance(fn)({ length: 1, 0: 42 }) // throws
* instance(fn)([]) === 1
Expand Down Expand Up @@ -253,7 +253,7 @@ const isArray = <T extends any[]>(containing?: T): TypeMatcher<T> =>
* @example
* const fn = mock<(cb: (value: number) => number) => void>();
* const matcher = It.willCapture();
* when(fn(matcher)).thenReturn();
* when(() => fn(matcher)).thenReturn();
*
* instance(fn)(x => x + 1);
* matcher.value?.(3) === 4
Expand Down
6 changes: 3 additions & 3 deletions src/mock/defaults.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('defaults', () => {

const fn = mock<(x: number) => boolean>();

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

expect(instance(fn)(-1)).toBeTruthy();
});
Expand All @@ -26,7 +26,7 @@ describe('defaults', () => {

const fn = mock<(x: number) => boolean>();

when(fn(It.matches((x) => x === 1))).thenReturn(true);
when(() => fn(It.matches((x) => x === 1))).thenReturn(true);

expect(() => instance(fn)(-1)).toThrow();
});
Expand All @@ -40,7 +40,7 @@ describe('defaults', () => {

const fn = mock<(x: number) => boolean>();

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

expect(() => instance(fn)(-1)).toThrow();
});
Expand Down
2 changes: 1 addition & 1 deletion src/mock/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type StrongMockDefaults = {
* matcher: () => It.matches(() => true)
* });
*
* when(fn('value')).thenReturn(true);
* when(() => fn('value')).thenReturn(true);
*
* instance(fn('not-value')) === true;
*/
Expand Down
8 changes: 4 additions & 4 deletions src/mock/map.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { NotAMock } from '../errors';
import { ExpectationRepository } from '../expectation/repository/expectation-repository';
import { Mock } from './mock';
import { PendingExpectation } from '../when/pending-expectation';
import { Mock } from './mock';

/**
* Since `when()` doesn't receive the mock subject (because we can't make it
* Since `when` doesn't receive the mock subject (because we can't make it
* consistently return it from `mock()`, `mock.foo` and `mock.bar()`) we need
* to store a global state for the currently active mock.
*
* We also want to throw in the following case:
*
* ```
* when(mock()) // forgot returns here
* when(mock()) // should throw
* when(() => mock()) // forgot returns here
* when(() => mock()) // should throw
* ```
*
* For that reason we can't just store the currently active mock, but also
Expand Down
Loading

0 comments on commit b0e46f4

Please sign in to comment.