strong-mock 8.0.0-beta.0 → 8.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,14 +44,16 @@ console.log(foo.bar(23)); // 'I am strong!'
44
44
  - [Verifying expectations](#verifying-expectations)
45
45
  - [Resetting expectations](#resetting-expectations)
46
46
  - [Argument matchers](#argument-matchers)
47
- - [Overriding default matcher](#overriding-default-matcher)
47
+ - [Mock options](#mock-options)
48
+ - [Strictness](#strictness)
49
+ - [Concrete matcher](#concrete-matcher)
50
+ - [Defaults](#defaults)
48
51
  - [FAQ](#faq)
49
52
  - [Why do I have to set all expectations first?](#why-do-i-have-to-set-all-expectations-first)
50
- - [Can I mock an existing object/function?](#can-i-mock-an-existing-objectfunction)
53
+ - [Can I partially mock an existing object/function?](#can-i-partially-mock-an-existing-objectfunction)
51
54
  - [How do I set expectations on setters?](#how-do-i-set-expectations-on-setters)
52
55
  - [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)
53
56
  - [How do I provide a function for the mock to call?](#how-do-i-provide-a-function-for-the-mock-to-call)
54
- - [Why does accessing an unused method throw?](#why-does-accessing-an-unused-method-throw)
55
57
  - [Can I spread/enumerate a mock?](#can-i-spreadenumerate-a-mock)
56
58
  - [How can I ignore `undefined` keys when setting expectations on objects?](#how-can-i-ignore-undefined-keys-when-setting-expectations-on-objects)
57
59
 
@@ -63,27 +65,37 @@ console.log(foo.bar(23)); // 'I am strong!'
63
65
 
64
66
  The created mock matches the mocked type so all expectations are type safe. Moreover, refactorings in an IDE will also cover your expectations.
65
67
 
66
- ![rename-interface](media/rename-interface.gif)
68
+ ![Renaming production code and test code](media/rename-refactor.gif)
67
69
 
68
70
  ### Useful error messages
69
71
 
70
72
  Error messages include the property that has been accessed, any arguments passed to it and any remaining unmet expectations.
71
73
 
72
- ![error messages](media/error-messages.png)
74
+ ```typescript
75
+ import { mock, when } from 'strong-mock';
76
+
77
+ const fn = mock<(a: number, b: number, c: number) => number>();
78
+
79
+ when(() => fn(1, 2, 3)).thenReturn(42);
80
+
81
+ fn(4, 5, 6);
82
+ ```
83
+
84
+ ![Test output showing details about mock expectations](media/error-messages.png)
73
85
 
74
86
  ### Type safe argument matchers
75
87
 
76
88
  Optional argument matchers allow you to create complex expectations, while still maintaining type safety.
77
89
 
78
- ![type safe matchers](./media/type-safe-matchers.png)
90
+ ![Type safe matcher showing a type error](media/type-safe-matchers.png)
79
91
 
80
92
  ## Installation
81
93
 
82
- ```
94
+ ```shell
83
95
  npm i -D strong-mock
84
96
  ```
85
97
 
86
- ```
98
+ ```shell
87
99
  yarn add -D strong-mock
88
100
  ```
89
101
 
@@ -260,6 +272,12 @@ console.log(fn(
260
272
  ); // 'matched!'
261
273
  ```
262
274
 
275
+ You can mix argument matchers with concrete arguments:
276
+
277
+ ```typescript
278
+ when(() => fn(42, It.isObject())).thenReturn('matched');
279
+ ```
280
+
263
281
  Available matchers:
264
282
  - `deepEquals` - the default, uses deep equality,
265
283
  - `is` - uses `Object.is` for comparison,
@@ -315,24 +333,67 @@ console.log(fn(23, (x) => x + 1)); // 42
315
333
  console.log(matcher.value?.(3)); // 4
316
334
  ```
317
335
 
318
- ### Overriding default matcher
336
+ ### Mock options
319
337
 
320
- You can override the default matcher that will be used when setting expectations with non-matcher values e.g. `42` or `{ foo: "bar" }`.
338
+ #### Strictness
321
339
 
322
- ```ts
323
- import { mock, when, It, setDefaults } from 'strong-mock';
340
+ strong-mock has a few levels of "strictness" that control what values are returned when an unexpected property is accessed or an unexpected call is made. The strictness can be configured for each mock, or for all mocks with `setDefaults`.
324
341
 
325
- // Use strict equality instead of deep equality.
326
- setDefaults({
327
- matcher: It.is
328
- })
342
+ ```typescript
343
+ import { mock, when } from 'strong-mock';
344
+ import { Strictness } from './options';
345
+
346
+ type Foo = {
347
+ bar: (value: number) => number;
348
+ }
349
+
350
+ // This is the default.
351
+ const strictFoo = mock<Foo>({ strictness: Strictness.STRICT });
352
+
353
+ // Accessing properties with no expectations is fine.
354
+ strictFoo.bar;
355
+ // Throws "Didn't expect bar(42) to be called".
356
+ strictFoo.bar(42);
357
+
358
+ const superStrictFoo = mock<Foo>({ strictness: Strictness.SUPER_STRICT });
359
+
360
+ // Throws "Didn't expect property bar to be accessed".
361
+ superStrictFoo.bar;
362
+ // Throws "Didn't expect property bar to be accessed".
363
+ superStrictFoo.bar(42);
364
+ ```
329
365
 
330
- const fn = mock<(x: number[]) => boolean>();
366
+ #### Concrete matcher
367
+
368
+ You can set the matcher that will be used in expectations with concrete values e.g. `42` or `{ foo: "bar" }`. Passing in a [matcher argument](#argument-matchers) will always take priority.
369
+
370
+ ```typescript
371
+ import { mock, when, It } from 'strong-mock';
372
+
373
+ // Use strict equality instead of deep equality.
374
+ const fn = mock<(x: number[]) => boolean>({ concreteMatcher: It.is });
331
375
  when(() => fn([1, 2, 3])).thenReturn(true);
332
376
 
333
377
  fn([1, 2, 3]); // throws because different arrays
334
378
  ```
335
379
 
380
+ #### Defaults
381
+
382
+ Mock options can be set for all mocks with `setDefaults`.
383
+
384
+ ```typescript
385
+ import { mock, when, setDefaults, Strictness } from 'strong-mock';
386
+
387
+ setDefaults({
388
+ strictness: Strictness.SUPER_STRICT
389
+ });
390
+
391
+ // Uses the new default.
392
+ const superStrictMock = mock<() => void>();
393
+ // Overrides the default.
394
+ const strictMock = mock<() => void>({ strictness: Strictness.STRICT });
395
+ ```
396
+
336
397
  ## FAQ
337
398
 
338
399
  ### Why do I have to set all expectations first?
@@ -343,9 +404,9 @@ This design decision has a few reasons behind it. First, it forces you to be awa
343
404
 
344
405
  Secondly, it will highlight potential design problems such as violations of the SOLID principles. If you find yourself duplicating expectations between tests and passing dummy values to them because your test is not concerned with them then you might want to look into splitting the code to only depend on things it really needs.
345
406
 
346
- ### Can I mock an existing object/function?
407
+ ### Can I partially mock an existing object/function?
347
408
 
348
- No, although you can pass its type to `mock()` and set expectations on it as you would with a type.
409
+ No, passing a concrete implementation to `mock()` will be the same as passing a type: all properties will be mocked, and you have to set expectations on the ones that will be accessed.
349
410
 
350
411
  ### How do I set expectations on setters?
351
412
 
@@ -373,54 +434,6 @@ console.log(foo.bar(23)); // 'called 23'
373
434
 
374
435
  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).
375
436
 
376
- ![call-rename](media/rename-args.gif)
377
-
378
- ### Why does accessing an unused method throw?
379
-
380
- Any unexpected property access will throw an error, even if the property is a method, and you never call it. This can sometimes be inconvenient if your code e.g. destructures your mock and only calls parts of it inside your test.
381
-
382
- ```typescript
383
- interface Foo {
384
- bar: () => number;
385
- baz: () => number;
386
- }
387
-
388
- function doFoo(foo: Foo, { callBaz }: { callBaz: boolean }) {
389
- // Will throw here with unexpected access on `baz`.
390
- const { bar, baz } = foo;
391
-
392
- bar();
393
-
394
- if (callBaz) {
395
- baz();
396
- }
397
- }
398
-
399
- const foo = mock<Foo>();
400
- when(() => foo.bar()).thenReturn(42);
401
-
402
- // Throws with unexpected access on `baz`.
403
- doFoo(foo, { callBaz: false });
404
- ```
405
-
406
- To work around this, either change your code to avoid destructuring
407
-
408
- ```typescript
409
- function doFoo(foo: Foo, callBaz: boolean) {
410
- foo.bar();
411
-
412
- if (callBaz) {
413
- foo.baz();
414
- }
415
- }
416
- ```
417
-
418
- or set a dummy expectation on the methods you're not interested in during the test.
419
-
420
- ```typescript
421
- when(() => foo.baz()).thenThrow('should not be called').anyTimes();
422
- ```
423
-
424
437
  ### Can I spread/enumerate a mock?
425
438
 
426
439
  Yes, and you will only get the properties that have expectations on them.
@@ -437,7 +450,6 @@ console.log(foo2.bar); // 42
437
450
  console.log(foo2.baz); // undefined
438
451
  ```
439
452
 
440
-
441
453
  ### How can I ignore `undefined` keys when setting expectations on objects?
442
454
 
443
455
  Use the `It.deepEquals` matcher explicitly inside `when` and pass `{ strict: false }`:
@@ -445,15 +457,17 @@ Use the `It.deepEquals` matcher explicitly inside `when` and pass `{ strict: fal
445
457
  ```ts
446
458
  const fn = mock<(x: { foo: string }) => boolean>();
447
459
 
448
- when(() => fn(It.deepEquals({ foo: "bar" }, { strict: false }))).thenReturn(true);
460
+ when(() => fn(
461
+ It.deepEquals({ foo: "bar" }, { strict: false }))
462
+ ).thenReturn(true);
449
463
 
450
464
  fn({ foo: "bar", baz: undefined }) === true
451
465
  ```
452
466
 
453
- You can also set this behavior to be the default by using [`setDefaults`](#overriding-default-matcher):
467
+ You can set this behavior to be the default by configuring the [concrete matcher](#concrete-matcher), and set it on all mocks using [setDefaults](#defaults):
454
468
 
455
469
  ```ts
456
470
  setDefaults({
457
- matcher: (expected) => It.deepEquals(expected, { strict: false })
471
+ concreteMatcher: (expected) => It.deepEquals(expected, { strict: false })
458
472
  });
459
473
  ```
@@ -1,5 +1,5 @@
1
- import { Expectation, ReturnValue } from '../expectation';
2
1
  import { Property } from '../../proxy';
2
+ import { Expectation, ReturnValue } from '../expectation';
3
3
  export declare type Call = {
4
4
  arguments: any[] | undefined;
5
5
  };
@@ -31,7 +31,7 @@ export interface ExpectationRepository {
31
31
  /**
32
32
  * Get a return value for the given property.
33
33
  *
34
- * The value might be a non-callable e.g. a number or a string or it might
34
+ * The value might be a non-callable e.g. a number or a string, or it might
35
35
  * be a function that, upon receiving arguments, will start a new search and
36
36
  * return a value again.
37
37
  *
@@ -52,6 +52,21 @@ export interface ExpectationRepository {
52
52
  * get('getData').value(1, 2, '3', false, NaN) === 42
53
53
  */
54
54
  get(property: Property): ReturnValue;
55
+ /**
56
+ * Get a return value for a function call.
57
+ *
58
+ * Note: this will only be invoked if the mocked type is a function. For
59
+ * method property calls {@link get} will be called instead.
60
+ *
61
+ * The list of expectations should be consulted from first to last when
62
+ * getting a return value. If none of them match it is up to the
63
+ * implementation to decide what to do.
64
+ *
65
+ * @example
66
+ * add(new Expectation(ApplyProp, [1, 2], 23);
67
+ * apply(1, 2) === 23
68
+ */
69
+ apply(args: unknown[]): unknown;
55
70
  /**
56
71
  * Get all the properties that have expectations.
57
72
  *
@@ -0,0 +1,37 @@
1
+ import { Strictness } from '../../mock/options';
2
+ import { Property } from '../../proxy';
3
+ import { Expectation } from '../expectation';
4
+ import { CallMap, ExpectationRepository } from './expectation-repository';
5
+ declare type CountableExpectation = {
6
+ expectation: Expectation;
7
+ matchCount: number;
8
+ };
9
+ /**
10
+ * An expectation repository for configurable levels of strictness.
11
+ */
12
+ export declare class FlexibleRepository implements ExpectationRepository {
13
+ private strictness;
14
+ constructor(strictness?: Strictness);
15
+ protected readonly expectations: Map<Property, CountableExpectation[]>;
16
+ private readonly expectedCallStats;
17
+ private readonly unexpectedCallStats;
18
+ add(expectation: Expectation): void;
19
+ clear(): void;
20
+ apply: (args: unknown[]) => unknown;
21
+ get(property: Property): any;
22
+ private handlePropertyWithMatchingExpectations;
23
+ private handlePropertyWithNoExpectations;
24
+ getAllProperties(): Property[];
25
+ getCallStats(): {
26
+ expected: CallMap;
27
+ unexpected: CallMap;
28
+ };
29
+ getUnmet(): Expectation[];
30
+ private recordExpected;
31
+ private recordUnexpected;
32
+ private countAndConsume;
33
+ private consumeExpectation;
34
+ private getValueForUnexpectedCall;
35
+ private getValueForUnexpectedAccess;
36
+ }
37
+ export {};
package/dist/index.d.ts CHANGED
@@ -5,4 +5,5 @@ export { verify, verifyAll } from './verify/verify';
5
5
  export { It } from './expectation/it';
6
6
  export { setDefaults } from './mock/defaults';
7
7
  export type { Matcher } from './expectation/matcher';
8
- export type { StrongMockDefaults } from './mock/defaults';
8
+ export type { MockOptions } from './mock/options';
9
+ export { Strictness } from './mock/options';