├── .eslintignore ├── .eslintrc ├── .gitignore ├── .tool-versions ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── when.js └── when.test.js ├── stryker.conf.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.json -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "env": { 4 | "es6": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "rules": { 9 | "object-curly-spacing": ["error", "always"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | coverage 4 | package-lock.json 5 | reports 6 | .stryker-tmp/ 7 | /.idea 8 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 14.18.3 2 | yarn 1.22.17 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - node 5 | 6 | cache: 7 | directories: 8 | - node_modules 9 | 10 | before_script: 11 | - yarn global add codecov 12 | 13 | script: 14 | - yarn run lint 15 | - yarn test 16 | - codecov 17 | 18 | deploy: 19 | provider: npm 20 | email: "$NPM_EMAIL" 21 | api_key: "$NPM_TOKEN" 22 | skip_cleanup: true 23 | on: 24 | tags: true 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jonas Holtkamp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jest-when 2 | 3 | [![build status](https://travis-ci.org/timkindberg/jest-when.svg?branch=master)](https://travis-ci.org/timkindberg/jest-when) 4 | [![codecov](https://codecov.io/gh/timkindberg/jest-when/branch/master/graph/badge.svg)](https://codecov.io/gh/timkindberg/jest-when) 5 | [![GitHub license](https://img.shields.io/github/license/timkindberg/jest-when.svg)](https://github.com/timkindberg/jest-when/blob/master/LICENSE) 6 | [![npm](https://img.shields.io/npm/v/jest-when.svg)](https://www.npmjs.com/package/jest-when) 7 | [![ThoughtWorks Tech Radar 2020 | Adopt](https://img.shields.io/badge/Tech%20Radar-Adopt-b3005d)](https://www.thoughtworks.com/radar/languages-and-frameworks?blipid=201911030) 8 | 9 | Specify dynamic return values for specifically matched mocked function arguments. Flexible matchers. Feels like canonical jest syntax. 10 | 11 | ThoughtWorks says: 12 | > jest-when is a lightweight JavaScript library that complements Jest by matching mock function call arguments. Jest is a great tool for testing the stack; jest-when allows you to expect specific arguments for mock functions which enables you to write more robust unit tests of modules with many dependencies. It's easy to use and provides great support for multiple matchers, which is why our teams have made jest-when their default choice for mocking in this space. 13 | 14 | ### Introduction 15 | `jest-when` allows you to use a set of the original 16 | [Jest mock functions](https://facebook.github.io/jest/docs/en/mock-function-api) in order to train 17 | your mocks only based on parameters your mocked function is called with. 18 | 19 | #### An Example 20 | 21 | So in jest if you want to mock a return value you would do: 22 | 23 | ```javascript 24 | const fn = jest.fn() 25 | fn.mockReturnValue('yay!') 26 | ``` 27 | 28 | But that will return "yay!" regardless of what arguments are send to the `fn`. If you want to change the return value 29 | based on the arguments, you have to use `mockImplementation` and it can be a bit cumbersome. 30 | 31 | `jest-when` makes this easy and fun! 32 | 33 | ```javascript 34 | when(fn).calledWith(1).mockReturnValue('yay!') 35 | ``` 36 | 37 | Now, the mock function `fn` will behave as follows—assuming no other trainings took place: 38 | * return `yay!` if called with `1` _as the only parameter_ 39 | * return `undefined` if called with _any parameters other_ than `1` 40 | 41 | So the steps are: 42 | ```javascript 43 | const fn = jest.fn() // 1) Start with any normal jest mock function 44 | when(fn) // 2) Wrap it with when() 45 | .calledWith(/* any matchers here */) // 3) Add your matchers with calledWith() 46 | .mockReturnValue(/* some value */) // 4) Then use any of the normal set of jest mock functions 47 | ``` 48 | 49 | The supported set of mock functions is: 50 | * `mockReturnValue` 51 | * `mockReturnValueOnce` 52 | * `mockResolvedValue` 53 | * `mockResolvedValueOnce` 54 | * `mockRejectedValue` 55 | * `mockRejectedValueOnce` 56 | * `mockImplementation` 57 | * `mockImplementationOnce` 58 | 59 | For extended usage see the examples below. 60 | 61 | ### Features 62 | 63 | - Match literals: `1`, `true`, `"string"`, `/regex/`, `null`, etc 64 | - Match objects or arrays: `{ foo: true }`, `[1, 2, 3]` 65 | - Match [asymmetric matchers](https://jestjs.io/docs/en/expect#expectanything): expect.any(), expect.objectContaining(), expect.stringMatching(), etc 66 | - Setup multiple matched calls with differing returns 67 | - Chaining of mock trainings 68 | - Replacement of mock trainings 69 | - One-time trainings, removed after they are matched 70 | - Promises, resolved or rejected 71 | - Can also wrap jest.spyOn functions with when() 72 | - Supports function matchers 73 | - Setup a default behavior 74 | - Supports resetting mocks between tests 75 | - Supports verifying all whenMocks were called 76 | 77 | ### Usage Examples 78 | 79 | #### Installation 80 | ```bash 81 | npm i --save-dev jest-when 82 | ``` 83 | 84 | #### Basic usage: 85 | ```javascript 86 | import { when } from 'jest-when' 87 | 88 | const fn = jest.fn() 89 | when(fn).calledWith(1).mockReturnValue('yay!') 90 | 91 | expect(fn(1)).toEqual('yay!') 92 | ``` 93 | 94 | #### Supports chaining of mock trainings: 95 | ```javascript 96 | when(fn) 97 | .calledWith(1).mockReturnValue('yay!') 98 | .calledWith(2).mockReturnValue('nay!') 99 | 100 | expect(fn(1)).toEqual('yay!') 101 | expect(fn(2)).toEqual('nay!') 102 | ``` 103 | Thanks to [@fkloes](https://github.com/fkloes). 104 | 105 | ```javascript 106 | when(fn) 107 | .calledWith(1) 108 | .mockReturnValueOnce('yay!') 109 | .mockReturnValue('nay!') 110 | 111 | expect(fn(1)).toEqual('yay!') 112 | expect(fn(1)).toEqual('nay!') 113 | ``` 114 | Thanks to [@danielhusar](https://github.com/danielhusar). 115 | 116 | #### Supports replacement of mock trainings: 117 | ```javascript 118 | when(fn).calledWith(1).mockReturnValue('yay!') 119 | expect(fn(1)).toEqual('yay!') 120 | 121 | when(fn).calledWith(1).mockReturnValue('nay!') 122 | expect(fn(1)).toEqual('nay!') 123 | ``` 124 | This replacement of the training only happens for mock functions _not_ ending in `*Once`. 125 | Trainings like `mockReturnValueOnce` are removed after a matching function call anyway. 126 | 127 | Thanks to [@fkloes](https://github.com/fkloes). 128 | 129 | #### Supports training for single calls 130 | ```javascript 131 | when(fn).calledWith(1, true, 'foo').mockReturnValueOnce('yay!') 132 | when(fn).calledWith(1, true, 'foo').mockReturnValueOnce('nay!') 133 | 134 | expect(fn(1, true, 'foo')).toEqual('yay!') 135 | expect(fn(1, true, 'foo')).toEqual('nay!') 136 | expect(fn(1, true, 'foo')).toBeUndefined() 137 | ``` 138 | 139 | #### Supports Promises, both resolved and rejected 140 | ```javascript 141 | when(fn).calledWith(1).mockResolvedValue('yay!') 142 | when(fn).calledWith(2).mockResolvedValueOnce('nay!') 143 | 144 | await expect(fn(1)).resolves.toEqual('yay!') 145 | await expect(fn(1)).resolves.toEqual('yay!') 146 | 147 | await expect(fn(2)).resolves.toEqual('nay!') 148 | expect(await fn(2)).toBeUndefined() 149 | 150 | 151 | when(fn).calledWith(3).mockRejectedValue(new Error('oh no!')) 152 | when(fn).calledWith(4).mockRejectedValueOnce(new Error('oh no, an error again!')) 153 | 154 | await expect(fn(3)).rejects.toThrow('oh no!') 155 | await expect(fn(3)).rejects.toThrow('oh no!') 156 | 157 | await expect(fn(4)).rejects.toThrow('oh no, an error again!') 158 | expect(await fn(4)).toBeUndefined() 159 | ``` 160 | 161 | #### Supports jest.spyOn: 162 | ```javascript 163 | const theSpiedMethod = jest.spyOn(theInstance, 'theMethod'); 164 | when(theSpiedMethod) 165 | .calledWith(1) 166 | .mockReturnValue('mock'); 167 | const returnValue = theInstance.theMethod(1); 168 | expect(returnValue).toBe('mock'); 169 | ``` 170 | 171 | #### Supports jest [asymmetric matchers](https://jestjs.io/docs/en/expect#expectanything): 172 | 173 | Use all the same asymmetric matchers available to the `toEqual()` assertion 174 | 175 | ```javascript 176 | when(fn).calledWith( 177 | expect.anything(), 178 | expect.any(Number), 179 | expect.arrayContaining(false) 180 | ).mockReturnValue('yay!') 181 | 182 | const result = fn('whatever', 100, [true, false]) 183 | expect(result).toEqual('yay!') 184 | ``` 185 | 186 | #### Supports function matchers: 187 | 188 | Just wrap any regular function (cannot be a jest mock or spy!) with `when`. 189 | 190 | The function will receive the arg and will be considered a match if the function returns true. 191 | 192 | It works with both calledWith and expectCalledWith. 193 | 194 | ```javascript 195 | const allValuesTrue = when((arg) => Object.values(arg).every(Boolean)) 196 | const numberDivisibleBy3 = when((arg) => arg % 3 === 0) 197 | 198 | when(fn) 199 | .calledWith(allValuesTrue, numberDivisibleBy3) 200 | .mockReturnValue('yay!') 201 | 202 | expect(fn({ foo: true, bar: true }, 9)).toEqual('yay!') 203 | expect(fn({ foo: true, bar: false }, 9)).toEqual(undefined) 204 | expect(fn({ foo: true, bar: false }, 13)).toEqual(undefined) 205 | ``` 206 | 207 | #### Supports compound declarations: 208 | ```javascript 209 | when(fn).calledWith(1).mockReturnValue('no') 210 | when(fn).calledWith(2).mockReturnValue('way?') 211 | when(fn).calledWith(3).mockReturnValue('yes') 212 | when(fn).calledWith(4).mockReturnValue('way!') 213 | 214 | expect(fn(1)).toEqual('no') 215 | expect(fn(2)).toEqual('way?') 216 | expect(fn(3)).toEqual('yes') 217 | expect(fn(4)).toEqual('way!') 218 | expect(fn(5)).toEqual(undefined) 219 | ``` 220 | 221 | #### Supports matching or asserting against all of the arguments together using `when.allArgs`: 222 | 223 | Pass a single special matcher, `when.allArgs`, if you'd like to handle all of the arguments 224 | with one function matcher. The function will receive all of the arguments as an array and you 225 | are responsible for returning true if they are a match, or false if not. The function also is 226 | provided with the powerful `equals` utility from Jasmine. 227 | 228 | 229 | This allows some convenient patterns: 230 | - Less verbose for variable args where all need to be of a certain type or match (e.g. all numbers) 231 | - Can be useful for partial matching, because you can assert just the first arg for example and ignore the rest 232 | 233 | E.g. All args should be numbers: 234 | ```javascript 235 | const areNumbers = (args, equals) => args.every(arg => equals(arg, expect.any(Number))) 236 | when(fn).calledWith(when.allArgs(areNumbers)).mockReturnValue('yay!') 237 | 238 | expect(fn(3, 6, 9)).toEqual('yay!') 239 | expect(fn(3, 666)).toEqual('yay!') 240 | expect(fn(-100, 2, 3.234234, 234, 90e3)).toEqual('yay!') 241 | expect(fn(123, 'not a number')).toBeUndefined() 242 | ``` 243 | 244 | E.g. Single arg match: 245 | ```javascript 246 | const argAtIndex = (index, matcher) => when.allArgs((args, equals) => equals(args[index], matcher)) 247 | 248 | when(fn).calledWith(argAtIndex(0, expect.any(Number))).mockReturnValue('yay!') 249 | 250 | expect(fn(3, 6, 9)).toEqual('yay!') 251 | expect(fn(3, 666)).toEqual('yay!') 252 | expect(fn(-100, 2, 3.234234, 234, 90e3)).toEqual('yay!') 253 | expect(fn(123, 'not a number')).toBeUndefined() 254 | ``` 255 | 256 | E.g. Partial match, only first defined matching args matter: 257 | ```javascript 258 | const fn = jest.fn() 259 | const partialArgs = (...argsToMatch) => when.allArgs((args, equals) => equals(args, expect.arrayContaining(argsToMatch))) 260 | 261 | when(fn) 262 | .calledWith(partialArgs(1, 2, 3)) 263 | .mockReturnValue('x') 264 | 265 | expect(fn(1, 2, 3)).toEqual('x') 266 | expect(fn(1, 2, 3, 4, 5, 6)).toEqual('x') 267 | expect(fn(1, 2)).toBeUndefined() 268 | expect(fn(1, 2, 4)).toBeUndefined() 269 | ``` 270 | 271 | #### Assert the args: 272 | 273 | Use `expectCalledWith` instead to run an assertion that the `fn` was called with the provided 274 | args. Your test will fail if the jest mock function is ever called without those exact 275 | `expectCalledWith` params. 276 | 277 | Disclaimer: This won't really work very well with compound declarations, because one of them will 278 | always fail, and throw an assertion error. 279 | ```javascript 280 | when(fn).expectCalledWith(1).mockReturnValue('x') 281 | 282 | fn(2); // Will throw a helpful jest assertion error with args diff 283 | ``` 284 | 285 | #### Supports default behavior 286 | 287 | Use any of `defaultReturnValue`, `defaultResolvedValue`, `defaultRejectedValue`, `defaultImplementation` 288 | to set up a default behavior, which will serve as fallback if no matcher fits. 289 | 290 | ```javascript 291 | when(fn) 292 | .calledWith('foo').mockReturnValue('special') 293 | .defaultReturnValue('default') // This line can be placed anywhere, doesn't have to be at the end 294 | 295 | expect(fn('foo')).toEqual('special') 296 | expect(fn('bar')).toEqual('default') 297 | ``` 298 | 299 | Or if you use any of `mockReturnValue`, `mockResolvedValue`, `mockRejectedValue`, `mockImplementation` directly on the object 300 | before using `calledWith` it will also behave as a default fallback. 301 | 302 | ```javascript 303 | // Same as above example 304 | when(fn) 305 | .mockReturnValue('default') 306 | .calledWith('foo').mockReturnValue('special') 307 | 308 | expect(fn('foo')).toEqual('special') 309 | expect(fn('bar')).toEqual('default') 310 | ``` 311 | 312 | One idea is to set up a default implementation that throws an error if an improper call is made to the mock. 313 | 314 | ```javascript 315 | when(fn) 316 | .calledWith(correctArgs) 317 | .mockReturnValue(expectedValue) 318 | .defaultImplementation(unsupportedCallError) 319 | 320 | // A default implementation that fails your test 321 | function unsupportedCallError(...args) { 322 | throw new Error(`Wrong args: ${JSON.stringify(args, null, 2)}`); 323 | } 324 | ``` 325 | 326 | #### Supports custom mockImplementation 327 | 328 | You could use this to call callbacks passed to your mock fn or other custom functionality. 329 | 330 | ```javascript 331 | const cb = jest.fn() 332 | 333 | when(fn).calledWith(cb).mockImplementation(callbackArg => callbackArg()) 334 | 335 | fn(cb) 336 | 337 | expect(cb).toBeCalled() 338 | ``` 339 | 340 | Thanks to [@idan-at](https://github.com/idan-at). 341 | 342 | #### Supports reseting mocks between tests 343 | 344 | You could use this to prevent mocks from carrying state between tests or assertions. 345 | 346 | ```javascript 347 | const { when, resetAllWhenMocks } = require('jest-when') 348 | const fn = jest.fn() 349 | 350 | // Test 1 351 | when(fn).expectCalledWith(1).mockReturnValueOnce('x') 352 | expect(fn(1)).toEqual('x') 353 | 354 | resetAllWhenMocks() 355 | 356 | // Test 2 357 | when(fn).expectCalledWith(1).mockReturnValueOnce('z') 358 | expect(fn(1)).toEqual('z') 359 | ``` 360 | 361 | Thanks to [@whoaa512](https://github.com/whoaa512). 362 | 363 | #### Supports resetting individual mocks entirely or by matchers 364 | 365 | You can reset a single mocked function by calling `mockReset` on the mock function. 366 | 367 | ```javascript 368 | const fn = jest.fn() 369 | 370 | when(fn).calledWith(1).mockReturnValue('yay!') 371 | when(fn).calledWith(2).mockReturnValue('boo!') 372 | fn.mockReset() 373 | 374 | expect(fn(1)).toBeUndefined() // no mocks 375 | expect(fn(2)).toBeUndefined() // no mocks 376 | ``` 377 | 378 | You can reset a single set of matchers by calling `mockReset` after `calledWith`. The matchers 379 | passed to calledWith will be used to remove any existing `calledWith` trainings with the same mathers. 380 | 381 | ```javascript 382 | const fn = jest.fn() 383 | 384 | when(fn).calledWith(1, 2, 3).mockReturnValue('yay!') 385 | when(fn).calledWith(2).mockReturnValue('boo!') 386 | 387 | // Reset only the 1, 2, 3 mock call 388 | when(fn).calledWith(1, 2, 3).mockReset() 389 | 390 | expect(fn(1, 2, 3)).toBeUndefined() // no mock for 1, 2, 3 391 | expect(fn(2)).toEqual('boo!') // success! 392 | ``` 393 | 394 | #### Supports verifying that all mocked functions were called 395 | 396 | Call `verifyAllWhenMocksCalled` after your test to assert that all mocks were used. 397 | 398 | ```javascript 399 | const { when, verifyAllWhenMocksCalled } = require('jest-when') 400 | const fn = jest.fn() 401 | 402 | when(fn).expectCalledWith(1).mockReturnValueOnce('x') 403 | 404 | expect(fn(1)).toEqual('x') 405 | 406 | verifyAllWhenMocksCalled() // passes 407 | ``` 408 | 409 | ```javascript 410 | const { when, verifyAllWhenMocksCalled } = require('jest-when') 411 | const fn = jest.fn() 412 | 413 | when(fn).expectCalledWith(1).mockReturnValueOnce('x') 414 | 415 | verifyAllWhenMocksCalled() // fails 416 | ``` 417 | 418 | Thanks to [@roaclark](https://github.com/roaclark). 419 | 420 | 421 | ### Contributors (in order of contribution) 422 | * [@timkindberg](https://github.com/timkindberg/) (original author) 423 | * [@jonasholtkamp](https://github.com/jonasholtkamp) (forked @ https://github.com/jonasholtkamp/jest-when-xt) 424 | > Many thanks to @jonasholtkamp. He forked this repo when I was inactive and stewarded several key features and bug fixes! 425 | * [@fkloes](https://github.com/fkloes) 426 | * [@danielhusar](https://github.com/danielhusar) 427 | * [@idan-at](https://github.com/idan-at) 428 | * [@whoaa512](https://github.com/whoaa512). 429 | * [@roaclark](https://github.com/roaclark) 430 | 431 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-when", 3 | "version": "3.7.0", 4 | "description": "An extension lib for jest", 5 | "license": "MIT", 6 | "main": "src/when.js", 7 | "scripts": { 8 | "test": "jest", 9 | "lint": "eslint src/", 10 | "lint.fix": "eslint src/ --fix", 11 | "stryker": "stryker run", 12 | "release": "np" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/timkindberg/jest-when/" 17 | }, 18 | "contributors": [ 19 | "Tim Kindberg ", 20 | "Jonas Holtkamp " 21 | ], 22 | "dependencies": {}, 23 | "peerDependencies": { 24 | "jest": ">= 25" 25 | }, 26 | "files": [ 27 | "/src/when.js" 28 | ], 29 | "devDependencies": { 30 | "@stryker-mutator/core": "^2.0.0", 31 | "@stryker-mutator/html-reporter": "^2.0.0", 32 | "@stryker-mutator/javascript-mutator": "^2.0.0", 33 | "@stryker-mutator/jest-runner": "^2.0.0", 34 | "eslint": "^4.19.1", 35 | "eslint-config-standard": "^11.0.0", 36 | "eslint-plugin-import": "^2.12.0", 37 | "eslint-plugin-node": "^6.0.1", 38 | "eslint-plugin-promise": "^3.7.0", 39 | "eslint-plugin-standard": "^3.1.0", 40 | "jest": "^27.0.0", 41 | "np": "^7.3.0", 42 | "pre-commit": "^1.2.2" 43 | }, 44 | "jest": { 45 | "verbose": false, 46 | "collectCoverage": true, 47 | "collectCoverageFrom": [ 48 | "src/**/*.js" 49 | ], 50 | "coverageDirectory": "build/reports/coverage/", 51 | "coverageReporters": [ 52 | "html", 53 | "lcov", 54 | "text" 55 | ], 56 | "testEnvironment": "node", 57 | "resetModules": true 58 | }, 59 | "pre-commit": [ 60 | "lint", 61 | "test" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /src/when.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | let registry = new Set() 4 | 5 | const getCallLines = () => (new Error()).stack.split('\n').slice(4).join('\n') 6 | 7 | /** 8 | * A hack to capture a reference to the `equals` jasmineUtil 9 | */ 10 | let equals = () => {} 11 | expect.extend({ 12 | __capture_equals__ () { 13 | equals = this.equals 14 | return { pass: true } 15 | } 16 | }) 17 | expect().__capture_equals__() 18 | let JEST_MATCHERS_OBJECT = Symbol.for('$$jest-matchers-object') 19 | // Hackily reset assertionCalls back to zero incase dev's tests are using expect.assertions() 20 | global[JEST_MATCHERS_OBJECT].state.assertionCalls = 0 21 | // Hackily delete the custom matcher that we added 22 | delete global[JEST_MATCHERS_OBJECT].matchers.__capture_equals__ 23 | /** 24 | * End hack 25 | */ 26 | 27 | const checkArgumentMatchers = (expectCall, args) => (match, matcher, i) => { 28 | // Propagate failure to the end 29 | if (!match) { 30 | return false 31 | } 32 | 33 | const arg = args[i] 34 | 35 | const isFunctionMatcher = typeof matcher === 'function' && matcher._isFunctionMatcher 36 | 37 | // Assert the match for better messaging during a failure 38 | if (expectCall) { 39 | if (isFunctionMatcher) { 40 | const isMatch = matcher(arg) 41 | const msg = `Failed function matcher within expectCalledWith: ${matcher.name}(${JSON.stringify(arg)}) did not return true\n\n\n...rest of the stack...` 42 | assert.equal(isMatch, true, msg) 43 | } else { 44 | expect(arg).toEqual(matcher) 45 | } 46 | } 47 | 48 | if (isFunctionMatcher) { 49 | return matcher(arg, equals) 50 | } 51 | 52 | return equals(arg, matcher) 53 | } 54 | 55 | const NO_CALLED_WITH_YET = Symbol('NO_CALLED_WITH') 56 | 57 | class WhenMock { 58 | constructor (fn) { 59 | // Incrementing ids assigned to each call mock to help with sorting as new mocks are added 60 | this.nextCallMockId = 0 61 | this.fn = fn 62 | fn.__whenMock__ = this 63 | this.callMocks = [] 64 | this._origMock = fn.getMockImplementation() 65 | this._defaultImplementation = null 66 | 67 | const _mockImplementation = (matchers, expectCall, once = false) => (mockImplementation) => { 68 | if (matchers[0] === NO_CALLED_WITH_YET) { 69 | this._defaultImplementation = mockImplementation 70 | } 71 | // To enable dynamic replacement during a test: 72 | // * call mocks with equal matchers are removed 73 | // * `once` mocks are used prioritized 74 | this.callMocks = this.callMocks 75 | .filter((callMock) => once || callMock.once || !equals(callMock.matchers, matchers)) 76 | .concat({ matchers, mockImplementation, expectCall, once, called: false, id: this.nextCallMockId, callLines: getCallLines() }) 77 | .sort((a, b) => { 78 | // Once mocks should appear before the rest 79 | if (a.once !== b.once) { 80 | return a.once ? -1 : 1 81 | } 82 | return a.id - b.id 83 | }) 84 | 85 | this.nextCallMockId++ 86 | 87 | const instance = this 88 | this.fn.mockImplementation(function (...args) { 89 | for (let i = 0; i < instance.callMocks.length; i++) { 90 | const { matchers, mockImplementation, expectCall, once, called } = instance.callMocks[i] 91 | 92 | // Do not let a once mock match more than once 93 | if (once && called) continue 94 | 95 | let isMatch = false 96 | 97 | if (matchers && matchers[0] && 98 | // is a possible all args matcher object 99 | (typeof matchers[0] === 'function' || typeof matchers[0] === 'object') && 100 | // ensure not a proxy 101 | '_isAllArgsFunctionMatcher' in matchers[0] && 102 | // check for the special property name 103 | matchers[0]._isAllArgsFunctionMatcher === true 104 | ) { 105 | if (matchers.length > 1) throw new Error('When using when.allArgs, it must be the one and only matcher provided to calledWith. You have incorrectly provided other matchers along with when.allArgs.') 106 | isMatch = checkArgumentMatchers(expectCall, [args])(true, matchers[0], 0) 107 | } else { 108 | isMatch = 109 | args.length === matchers.length && 110 | matchers.reduce(checkArgumentMatchers(expectCall, args), true) 111 | } 112 | 113 | if (isMatch && typeof mockImplementation === 'function') { 114 | instance.callMocks[i].called = true 115 | return mockImplementation.call(this, ...args) 116 | } 117 | } 118 | 119 | if (instance._defaultImplementation) { 120 | return instance._defaultImplementation.call(this, ...args) 121 | } 122 | if (typeof fn.__whenMock__._origMock === 'function') { 123 | return fn.__whenMock__._origMock.call(this, ...args) 124 | } 125 | return undefined 126 | }) 127 | 128 | return { 129 | ...this, 130 | ...mockFunctions(matchers, expectCall) 131 | } 132 | } 133 | 134 | const mockFunctions = (matchers, expectCall) => ({ 135 | mockReturnValue: returnValue => _mockImplementation(matchers, expectCall)(() => returnValue), 136 | mockReturnValueOnce: returnValue => _mockImplementation(matchers, expectCall, true)(() => returnValue), 137 | mockResolvedValue: returnValue => _mockImplementation(matchers, expectCall)(() => Promise.resolve(returnValue)), 138 | mockResolvedValueOnce: returnValue => _mockImplementation(matchers, expectCall, true)(() => Promise.resolve(returnValue)), 139 | mockRejectedValue: err => _mockImplementation(matchers, expectCall)(() => Promise.reject(err)), 140 | mockRejectedValueOnce: err => _mockImplementation(matchers, expectCall, true)(() => Promise.reject(err)), 141 | mockImplementation: implementation => _mockImplementation(matchers, expectCall)(implementation), 142 | mockImplementationOnce: implementation => _mockImplementation(matchers, expectCall, true)(implementation), 143 | defaultImplementation: implementation => this.defaultImplementation(implementation), 144 | defaultReturnValue: returnValue => this.defaultReturnValue(returnValue), 145 | defaultResolvedValue: returnValue => this.defaultResolvedValue(returnValue), 146 | defaultRejectedValue: err => this.defaultRejectedValue(err), 147 | mockReset: () => { 148 | this.callMocks = this.callMocks 149 | .filter((callMock) => !equals(callMock.matchers, matchers)) 150 | return { 151 | ...this, 152 | ...mockFunctions(matchers, expectCall) 153 | } 154 | } 155 | }) 156 | 157 | // These four functions are only used when the dev has not used `.calledWith` before calling one of the mock return functions 158 | this.defaultImplementation = mockImplementation => { 159 | // Set up an implementation with a special matcher that can never be matched because it uses a private symbol 160 | // Additionally the symbols existence can be checked to see if a calledWith was omitted. 161 | return _mockImplementation([NO_CALLED_WITH_YET], false)(mockImplementation) 162 | } 163 | this.defaultReturnValue = returnValue => this.defaultImplementation(() => returnValue) 164 | this.defaultResolvedValue = returnValue => this.defaultReturnValue(Promise.resolve(returnValue)) 165 | this.defaultRejectedValue = err => this.defaultReturnValue(Promise.reject(err)) 166 | this.mockImplementation = this.defaultImplementation 167 | this.mockReturnValue = this.defaultReturnValue 168 | this.mockResolvedValue = this.defaultResolvedValue 169 | this.mockRejectedValue = this.defaultRejectedValue 170 | 171 | this.calledWith = (...matchers) => ({ ...mockFunctions(matchers, false) }) 172 | this.expectCalledWith = (...matchers) => ({ ...mockFunctions(matchers, true) }) 173 | 174 | this.resetWhenMocks = () => { 175 | resetWhenMocksOnFn(fn) 176 | } 177 | } 178 | } 179 | 180 | const when = (fn) => { 181 | // This bit is for when you use `when` to make a WhenMock 182 | // when(fn) <-- This one 183 | // .calledWith(when(numberIsGreaterThanZero)) <-- Not this one 184 | if (fn._isMockFunction) { 185 | if (fn.__whenMock__ instanceof WhenMock) return fn.__whenMock__ 186 | const whenMock = new WhenMock(fn) 187 | registry.add(fn) 188 | fn._origMockReset = fn.mockReset 189 | fn.mockReset = () => { 190 | resetWhenMocksOnFn(fn) 191 | fn.mockReset = fn._origMockReset 192 | fn._origMockReset = undefined 193 | fn.mockReset() 194 | } 195 | return whenMock 196 | } 197 | 198 | // This bit is for when you use `when` as a function matcher 199 | // when(fn) <-- Not this one 200 | // .calledWith(when(numberIsGreaterThanZero)) <-- This one 201 | if (typeof fn === 'function') { 202 | fn._isFunctionMatcher = true 203 | return fn 204 | } 205 | } 206 | 207 | when.allArgs = (fn) => { 208 | fn._isFunctionMatcher = true 209 | fn._isAllArgsFunctionMatcher = true 210 | return fn 211 | } 212 | 213 | const resetAllWhenMocks = () => { 214 | registry.forEach(resetWhenMocksOnFn) 215 | registry = new Set() 216 | } 217 | 218 | function resetWhenMocksOnFn (fn) { 219 | fn.mockImplementation(fn.__whenMock__._origMock) 220 | fn.__whenMock__ = undefined 221 | registry.delete(fn) 222 | } 223 | 224 | const verifyAllWhenMocksCalled = () => { 225 | const [allMocks, calledMocks, uncalledMocks] = Array.from(registry).reduce((acc, fn) => { 226 | const mocks = fn.__whenMock__.callMocks 227 | const [calledMocks, uncalledMocks] = mocks.reduce((memo, mock) => { 228 | memo[mock.called ? 0 : 1].push(mock) 229 | return memo 230 | }, [[], []]) 231 | return [[...acc[0], ...mocks], [...acc[1], ...calledMocks], [...acc[2], ...uncalledMocks]] 232 | }, [[], [], []]) 233 | 234 | const callLines = uncalledMocks 235 | .filter(m => Boolean(m.callLines)) 236 | .map(m => `\n ${String(m.callLines).trim()}`) 237 | .join('') 238 | 239 | const msg = `Failed verifyAllWhenMocksCalled: ${uncalledMocks.length} not called: ${callLines}\n\n\n...rest of the stack...` 240 | 241 | assert.equal(`called mocks: ${calledMocks.length}`, `called mocks: ${allMocks.length}`, msg) 242 | } 243 | 244 | when.resetAllWhenMocks = resetAllWhenMocks 245 | when.verifyAllWhenMocksCalled = verifyAllWhenMocksCalled 246 | 247 | module.exports = { 248 | when, 249 | resetAllWhenMocks, 250 | verifyAllWhenMocksCalled, 251 | WhenMock 252 | } 253 | -------------------------------------------------------------------------------- /src/when.test.js: -------------------------------------------------------------------------------- 1 | const errMsg = ({ expect, actual }) => 2 | new RegExp(`Expected.*${expect}.*\\nReceived.*${actual}`) 3 | 4 | describe('When', () => { 5 | let when, WhenMock, resetAllWhenMocks, verifyAllWhenMocksCalled 6 | 7 | beforeEach(() => { 8 | when = require('./when').when 9 | resetAllWhenMocks = require('./when').resetAllWhenMocks 10 | verifyAllWhenMocksCalled = require('./when').verifyAllWhenMocksCalled 11 | WhenMock = require('./when').WhenMock 12 | }) 13 | 14 | afterEach(() => { 15 | jest.restoreAllMocks() 16 | jest.resetModules() 17 | }) 18 | 19 | describe('when', () => { 20 | it('returns a WhenMock', () => { 21 | const fn = jest.fn() 22 | const whenFn = when(fn) 23 | 24 | expect(whenFn).toBeInstanceOf(WhenMock) 25 | expect(whenFn.fn).toBe(fn) 26 | }) 27 | 28 | it('returns existing WhenMock if fn was already whenified', () => { 29 | const fn = jest.fn() 30 | const whenFn1 = when(fn) 31 | const whenFn2 = when(fn) 32 | 33 | expect(whenFn1).toBeInstanceOf(WhenMock) 34 | expect(whenFn2).toBeInstanceOf(WhenMock) 35 | expect(whenFn1).toBe(whenFn2) 36 | }) 37 | 38 | it('allows reset of mocks to enable overrides later', () => { 39 | const fn = jest.fn() 40 | 41 | when(fn).expectCalledWith(1).mockReturnValueOnce('x') 42 | 43 | resetAllWhenMocks() 44 | 45 | when(fn).expectCalledWith(1).mockReturnValueOnce('z') 46 | 47 | expect(fn(1)).toEqual('z') 48 | }) 49 | 50 | it('reset of mocks restores original implementation', () => { 51 | const fn = jest.fn(() => 'a') 52 | 53 | when(fn).expectCalledWith(1).mockReturnValueOnce('x') 54 | 55 | resetAllWhenMocks() 56 | 57 | expect(fn(1)).toEqual('a') 58 | }) 59 | 60 | it('allows reset of mocks for one function', () => { 61 | const fn = jest.fn(() => 'a') 62 | 63 | const mock = when(fn).expectCalledWith(1).mockReturnValueOnce('x') 64 | 65 | mock.resetWhenMocks() 66 | 67 | expect(fn(1)).toEqual('a') 68 | }) 69 | 70 | it('allows checking that all mocks were called', () => { 71 | const fn1 = jest.fn() 72 | const fn2 = jest.fn() 73 | 74 | when(fn1).expectCalledWith(1).mockReturnValue('z') 75 | when(fn2).expectCalledWith(1).mockReturnValueOnce('x') 76 | when(fn2).expectCalledWith(1).mockReturnValueOnce('y') 77 | when(fn2).expectCalledWith(1).mockReturnValue('z') 78 | 79 | fn1(1) 80 | fn2(1) 81 | fn2(1) 82 | fn2(1) 83 | 84 | expect(verifyAllWhenMocksCalled).not.toThrow() 85 | }) 86 | 87 | it('fails verification check if all mocks were not called', () => { 88 | const fn1 = jest.fn() 89 | const fn2 = jest.fn() 90 | 91 | when(fn1).expectCalledWith(expect.anything()).mockReturnValue('z') 92 | when(fn2).expectCalledWith(expect.anything()).mockReturnValueOnce('x') 93 | when(fn2).expectCalledWith(expect.anything()).mockReturnValueOnce('y') 94 | when(fn2).expectCalledWith(expect.anything()).mockReturnValue('z') 95 | 96 | fn1(1) 97 | fn2(1) 98 | 99 | let caughtErr 100 | 101 | try { 102 | verifyAllWhenMocksCalled() 103 | } catch (e) { 104 | caughtErr = e 105 | } 106 | 107 | expect(caughtErr.expected).toEqual('called mocks: 4') 108 | expect(caughtErr.actual).toEqual('called mocks: 2') 109 | expect(caughtErr.message).toMatch(/Failed verifyAllWhenMocksCalled: 2 not called/) 110 | }) 111 | 112 | it('should print a full stacktrace if verification check fails', () => { 113 | const fn1 = jest.fn() 114 | 115 | function extractedWhenConfiguration () { 116 | when(fn1).expectCalledWith(expect.anything()).mockReturnValueOnce('z') 117 | } 118 | 119 | extractedWhenConfiguration() 120 | extractedWhenConfiguration() 121 | 122 | fn1(1) 123 | 124 | let caughtErr 125 | 126 | try { 127 | verifyAllWhenMocksCalled() 128 | } catch (e) { 129 | caughtErr = e 130 | } 131 | 132 | expect(caughtErr.message).toContain('at Object.extractedWhenConfiguration') 133 | }) 134 | 135 | it('fails verification check if all mocks were not called with line numbers', () => { 136 | const fn1 = jest.fn() 137 | const fn2 = jest.fn() 138 | 139 | when(fn1).expectCalledWith(expect.anything()).mockReturnValue('z') 140 | when(fn2).expectCalledWith(expect.anything()).mockReturnValueOnce('x') 141 | when(fn2).expectCalledWith(expect.anything()).mockReturnValueOnce('y') 142 | when(fn2).expectCalledWith(expect.anything()).mockReturnValue('z') 143 | 144 | fn1(1) 145 | fn2(1) 146 | 147 | try { 148 | verifyAllWhenMocksCalled() 149 | } catch (e) { 150 | const errorLines = e.message.split('\n') 151 | const currentFilePathPattern = /src(?:\\|\/)when\.test\.js:\d{3}(.|\s)*/ 152 | const numberOfMatches = errorLines.filter(line => currentFilePathPattern.test(line)).length 153 | expect(numberOfMatches).toBe(2) 154 | } 155 | }) 156 | }) 157 | 158 | describe('mock implementation', () => { 159 | it('only matches exact sets of args, too little or too many args do not trigger mock return', () => { 160 | const fn = jest.fn() 161 | 162 | when(fn) 163 | .calledWith(1, 'foo', true, expect.any(String), undefined) 164 | .mockReturnValue('x') 165 | 166 | expect(fn(1, 'foo', true, 'whatever', undefined)).toEqual('x') 167 | 168 | expect(fn(1, 'foo', true, 'whatever')).toEqual(undefined) 169 | expect(fn(1, 'foo', true, 'whatever', undefined, undefined)).toEqual(undefined) 170 | expect(fn(1, 'foo', true, 'whatever', undefined, 'oops')).toEqual(undefined) 171 | }) 172 | 173 | it('able to call with null', () => { 174 | const fn = jest.fn() 175 | 176 | when(fn) 177 | .calledWith(null) 178 | .mockReturnValue('x') 179 | 180 | expect(fn(null)).toEqual('x') 181 | }) 182 | 183 | describe('function matcher', () => { 184 | it('works with custom function args', () => { 185 | const fn = jest.fn() 186 | 187 | const allValuesTrue = (arg) => Object.values(arg).every(Boolean) 188 | const numberDivisibleBy3 = (arg) => arg % 3 === 0 189 | 190 | when(fn) 191 | .calledWith(when(allValuesTrue), when(numberDivisibleBy3)) 192 | .mockReturnValue('x') 193 | 194 | expect(fn({ foo: true, bar: true }, 9)).toEqual('x') 195 | expect(fn({ foo: true, bar: false }, 9)).toEqual(undefined) 196 | expect(fn({ foo: true, bar: false }, 13)).toEqual(undefined) 197 | }) 198 | 199 | it('custom function args get access to the "equals" jasmine util', () => { 200 | const fn = jest.fn() 201 | 202 | const arrMatch = (arg, equals) => equals(arg, [1, 2, 3]) 203 | 204 | when(fn) 205 | .calledWith(when(arrMatch)) 206 | .mockReturnValue('x') 207 | 208 | expect(fn([1, 2, 3])).toEqual('x') 209 | }) 210 | 211 | it('expects with custom function args', () => { 212 | const fn = jest.fn() 213 | 214 | const allValuesTrue = (arg) => Object.values(arg).every(Boolean) 215 | const numberDivisibleBy3 = (arg) => arg % 3 === 0 216 | 217 | when(fn) 218 | .expectCalledWith(when(allValuesTrue), when(numberDivisibleBy3)) 219 | .mockReturnValue('x') 220 | 221 | expect(fn({ foo: true, bar: true }, 9)).toEqual('x') 222 | expect(() => fn({ foo: false, bar: true }, 9)).toThrow(/Failed function matcher within expectCalledWith: allValuesTrue\(\{"foo":false,"bar":true\}\) did not return true/) 223 | expect(() => fn({ foo: true, bar: true }, 13)).toThrow(/Failed function matcher within expectCalledWith: numberDivisibleBy3\(13\) did not return true/) 224 | }) 225 | 226 | it('does not call regular functions as function matchers', () => { 227 | const fn = jest.fn() 228 | 229 | const doNotCallMeBro = () => { 230 | throw new Error('BOOM') 231 | } 232 | 233 | when(fn) 234 | .expectCalledWith(doNotCallMeBro) 235 | .mockReturnValue('x') 236 | 237 | expect(fn(doNotCallMeBro)).toEqual('x') 238 | expect(() => fn(doNotCallMeBro)).not.toThrow() 239 | }) 240 | }) 241 | 242 | describe('when.allArgs', () => { 243 | it('throws an error if you try to use other matches with it', () => { 244 | const fn = jest.fn() 245 | 246 | when(fn) 247 | .calledWith(when.allArgs(() => true), 1, 2, 3) 248 | .mockReturnValue('x') 249 | 250 | expect(() => fn(3, 6, 9)).toThrow(/When using when.allArgs, it must be the one and only matcher provided to calledWith. You have incorrectly provided other matchers along with when.allArgs./) 251 | }) 252 | 253 | it('allows matching against all the args at once with when.allArgs', () => { 254 | const fn = jest.fn() 255 | 256 | const numberDivisibleBy3 = (args) => args.every(arg => arg % 3 === 0) 257 | 258 | when(fn) 259 | .calledWith(when.allArgs(numberDivisibleBy3)) 260 | .mockReturnValue('x') 261 | 262 | expect(fn(3, 6, 9)).toEqual('x') 263 | expect(fn(3, 6, 10)).toBeUndefined() 264 | expect(fn(1, 2, 3)).toBeUndefined() 265 | }) 266 | 267 | it('all args are numbers example', () => { 268 | const fn = jest.fn() 269 | const areNumbers = (args, equals) => args.every(arg => equals(arg, expect.any(Number))) 270 | 271 | when(fn) 272 | .calledWith(when.allArgs(areNumbers)) 273 | .mockReturnValue('x') 274 | 275 | expect(fn(3, 6, 9)).toEqual('x') 276 | expect(fn(3, 666)).toEqual('x') 277 | expect(fn(-100, 2, 3.234234, 234, 90e3)).toEqual('x') 278 | expect(fn(123, 'not a number')).toBeUndefined() 279 | }) 280 | 281 | it('first matcher is a proxy', () => { 282 | const fn = jest.fn() 283 | 284 | const proxy = new Proxy({}, { 285 | get: () => true 286 | }) 287 | 288 | when(fn) 289 | .calledWith(proxy) 290 | .mockReturnValue('x') 291 | 292 | expect(proxy._isAllArgsFunctionMatcher).toEqual(true) 293 | 294 | expect(fn(proxy)).toEqual('x') 295 | }) 296 | 297 | it('single arg match example', () => { 298 | const fn = jest.fn() 299 | const argAtIndex = (index, matcher) => when.allArgs((args, equals) => equals(args[index], matcher)) 300 | 301 | when(fn) 302 | .calledWith(argAtIndex(0, expect.any(Number))) 303 | .mockReturnValue('x') 304 | 305 | expect(fn(1, 2, 3)).toEqual('x') 306 | expect(fn(-123123, 'string', false, null)).toEqual('x') 307 | expect(fn('not a string', 2, 3)).toBeUndefined() 308 | }) 309 | 310 | it('partial match example', () => { 311 | const fn = jest.fn() 312 | const partialArgs = (...argsToMatch) => when.allArgs((args, equals) => equals(args, expect.arrayContaining(argsToMatch))) 313 | 314 | when(fn) 315 | .calledWith(partialArgs(1, 2, 3)) 316 | .mockReturnValue('x') 317 | 318 | expect(fn(1, 2, 3)).toEqual('x') 319 | expect(fn(1, 2, 3, 4, 5, 6)).toEqual('x') 320 | expect(fn(1, 2)).toBeUndefined() 321 | expect(fn(1, 2, 4)).toBeUndefined() 322 | }) 323 | 324 | it('react use case from github', () => { 325 | // SEE: https://github.com/timkindberg/jest-when/issues/66 326 | 327 | const SomeChild = jest.fn() 328 | 329 | when(SomeChild) 330 | .calledWith({ xyz: '123' }) 331 | .mockReturnValue('hello world') 332 | 333 | const propsOf = propsToMatch => when.allArgs(([props, refOrContext], equals) => equals(props, propsToMatch)) 334 | 335 | when(SomeChild) 336 | .calledWith(propsOf({ xyz: '123' })) 337 | .mockReturnValue('hello world') 338 | 339 | expect(SomeChild({ xyz: '123' })).toEqual('hello world') 340 | }) 341 | 342 | it('allows matching against all the args at once with when.allArgs using expect matchers', () => { 343 | const fn = jest.fn() 344 | 345 | when(fn) 346 | .calledWith(when.allArgs(expect.arrayContaining([42]))) 347 | .mockReturnValue('x') 348 | .calledWith(when.allArgs(expect.arrayContaining([expect.objectContaining({ foo: true })]))) 349 | .mockReturnValue('y') 350 | 351 | expect(fn(3, 6, 42)).toEqual('x') 352 | expect(fn({ foo: true, bar: true }, 'a', 'b', 'c')).toEqual('y') 353 | expect(fn(1, 2, 3)).toBeUndefined() 354 | }) 355 | 356 | it('allows asserting against all the args at once with when.allArgs', () => { 357 | const fn = jest.fn() 358 | 359 | const numberDivisibleBy3 = (args) => args.every(arg => arg % 3 === 0) 360 | 361 | when(fn) 362 | .expectCalledWith(when.allArgs(numberDivisibleBy3)) 363 | .mockReturnValue('x') 364 | 365 | expect(fn(3, 6, 9)).toEqual('x') 366 | expect(() => fn(3, 6, 10)).toThrow(/Failed function matcher within expectCalledWith: numberDivisibleBy3\(\[3,6,10]\) did not return true/) 367 | expect(() => fn(1, 2, 3)).toThrow(/Failed function matcher within expectCalledWith: numberDivisibleBy3\(\[1,2,3]\) did not return true/) 368 | }) 369 | }) 370 | 371 | it('supports compound when declarations', () => { 372 | const fn = jest.fn() 373 | 374 | when(fn).calledWith(1).mockReturnValue('x') 375 | when(fn).calledWith('foo', 'bar').mockReturnValue('y') 376 | when(fn).calledWith(false, /asdf/g).mockReturnValue('z') 377 | 378 | expect(fn(1)).toEqual('x') 379 | expect(fn('foo', 'bar')).toEqual('y') 380 | expect(fn(false, /asdf/g)).toEqual('z') 381 | }) 382 | 383 | it('supports chaining of when declarations', () => { 384 | const fn = jest.fn() 385 | 386 | when(fn) 387 | .calledWith(1) 388 | .mockReturnValue('x') 389 | 390 | when(fn).calledWith('foo', 'bar') 391 | .mockReturnValue('y') 392 | .calledWith(false, /asdf/g) 393 | .mockReturnValue('z') 394 | 395 | expect(fn(1)).toEqual('x') 396 | expect(fn('foo', 'bar')).toEqual('y') 397 | expect(fn(false, /asdf/g)).toEqual('z') 398 | }) 399 | 400 | it('supports replacement of when declarations', () => { 401 | const fn = jest.fn() 402 | 403 | when(fn).calledWith('foo', 'bar').mockReturnValue('x') 404 | when(fn).calledWith(false, /asdf/g).mockReturnValue('y') 405 | when(fn).calledWith('foo', 'bar').mockReturnValue('z') 406 | 407 | expect(fn('foo', 'bar')).toEqual('z') 408 | }) 409 | 410 | it('returns a declared value repeatedly', () => { 411 | const fn = jest.fn() 412 | 413 | when(fn).calledWith(1).mockReturnValue('x') 414 | when(fn).calledWith(2).mockReturnValueOnce('x').mockReturnValue('y') 415 | 416 | expect(fn(1)).toEqual('x') 417 | expect(fn(1)).toEqual('x') 418 | expect(fn(1)).toEqual('x') 419 | expect(fn(2)).toEqual('x') 420 | expect(fn(2)).toEqual('y') 421 | }) 422 | 423 | it('should handle symbol matchers', () => { 424 | const fn = jest.fn() 425 | const symbol = Symbol.for(`sym`) 426 | when(fn).calledWith(symbol, 2).mockReturnValue('x') 427 | 428 | expect(fn(5)).toBeUndefined() 429 | expect(fn(symbol, 2)).toBe('x') 430 | }) 431 | 432 | it('returns nothing if no declared value matches', () => { 433 | const fn = jest.fn() 434 | 435 | when(fn).calledWith(1, 2).mockReturnValue('x') 436 | 437 | expect(fn(5, 6)).toBeUndefined() 438 | }) 439 | 440 | it('expectCalledWith: fails a test with error messaging if argument does not match', () => { 441 | const fn1 = jest.fn() 442 | const fn2 = jest.fn() 443 | 444 | when(fn1).expectCalledWith(1).mockReturnValue('x') 445 | when(fn2).calledWith('foo').mockReturnValue('y') 446 | 447 | expect(() => fn1(2)).toThrow(errMsg({ expect: 1, actual: 2 })) 448 | expect(() => fn2('bar')).not.toThrow() 449 | }) 450 | 451 | it('mockReturnValue: should return a function', () => { 452 | const fn = jest.fn() 453 | const returnValue = () => {} 454 | 455 | when(fn).calledWith('foo').mockReturnValue(returnValue) 456 | 457 | expect(fn('foo')).toBe(returnValue) 458 | }) 459 | 460 | it('mockReturnValueOnce: should return a function', () => { 461 | const fn = jest.fn() 462 | const returnValue = () => {} 463 | 464 | when(fn).calledWith('foo').mockReturnValueOnce(returnValue) 465 | 466 | expect(fn('foo')).toBe(returnValue) 467 | }) 468 | 469 | it('mockReturnValueOnce: should return specified value only once', () => { 470 | const fn = jest.fn() 471 | 472 | when(fn).calledWith('foo').mockReturnValueOnce('bar') 473 | when(fn).calledWith('foo').mockReturnValueOnce('cbs') 474 | 475 | expect(fn('foo')).toEqual('bar') 476 | expect(fn('foo')).toEqual('cbs') 477 | expect(fn('foo')).toBeUndefined() 478 | }) 479 | 480 | it('mockReturnValueOnce: should return specified value only once and the regular value after that', () => { 481 | const fn = jest.fn() 482 | 483 | when(fn).calledWith('foo').mockReturnValue('bar') 484 | expect(fn('foo')).toEqual('bar') 485 | 486 | when(fn).calledWith('foo').mockReturnValueOnce('cbs') 487 | expect(fn('foo')).toEqual('cbs') 488 | 489 | expect(fn('foo')).toEqual('bar') 490 | }) 491 | 492 | it('mockReturnValueOnce: works with expectCalledWith', () => { 493 | const fn = jest.fn() 494 | 495 | when(fn).expectCalledWith('foo').mockReturnValueOnce('bar') 496 | 497 | expect(fn('foo')).toEqual('bar') 498 | }) 499 | 500 | it('mockResolvedValue: should return a Promise', async () => { 501 | const fn = jest.fn() 502 | 503 | when(fn).calledWith('foo').mockResolvedValue('bar') 504 | 505 | await expect(fn('foo')).resolves.toEqual('bar') 506 | }) 507 | 508 | it('mockResolvedValue: works with expectCalledWith', async () => { 509 | const fn = jest.fn() 510 | 511 | when(fn).expectCalledWith('foo').mockResolvedValue('bar') 512 | 513 | await expect(fn('foo')).resolves.toEqual('bar') 514 | }) 515 | 516 | it('mockResolvedValueOnce: should return a Promise only once', async () => { 517 | const fn = jest.fn() 518 | 519 | when(fn).calledWith('foo').mockResolvedValueOnce('bar') 520 | 521 | await expect(fn('foo')).resolves.toEqual('bar') 522 | expect(await fn('foo')).toBeUndefined() 523 | }) 524 | 525 | it('mockResolvedValueOnce: should return specified value only once and the regular value after that', async () => { 526 | const fn = jest.fn() 527 | 528 | when(fn).calledWith('foo').mockResolvedValue('bar') 529 | expect(await fn('foo')).toEqual('bar') 530 | 531 | when(fn).calledWith('foo').mockResolvedValueOnce('cbs') 532 | expect(await fn('foo')).toEqual('cbs') 533 | 534 | expect(await fn('foo')).toEqual('bar') 535 | }) 536 | 537 | it('mockResolvedValueOnce: works with expectCalledWith', async () => { 538 | const fn = jest.fn() 539 | 540 | when(fn).expectCalledWith('foo').mockResolvedValueOnce('bar') 541 | 542 | await expect(fn('foo')).resolves.toEqual('bar') 543 | expect(await fn('foo')).toBeUndefined() 544 | }) 545 | 546 | it('mockRejectedValue: should return a rejected Promise', async () => { 547 | const fn = jest.fn() 548 | 549 | when(fn).calledWith('foo').mockRejectedValue(new Error('bar')) 550 | 551 | await expect(fn('foo')).rejects.toThrow('bar') 552 | }) 553 | 554 | it('mockRejectedValue: does not reject the Promise until the function is called', done => { 555 | const fn = jest.fn() 556 | 557 | when(fn).calledWith('foo').mockRejectedValue(new Error('bar')) 558 | 559 | setTimeout(async () => { 560 | await expect(fn('foo')).rejects.toThrow('bar') 561 | done() 562 | }, 0) 563 | }) 564 | 565 | it('mockRejectedValue: works with expectCalledWith', async () => { 566 | const fn = jest.fn() 567 | 568 | when(fn).expectCalledWith('foo').mockRejectedValue(new Error('bar')) 569 | 570 | await expect(fn('foo')).rejects.toThrow('bar') 571 | }) 572 | 573 | it('mockRejectedValueOnce: should return a rejected Promise only once', async () => { 574 | const fn = jest.fn() 575 | 576 | when(fn).calledWith('foo').mockRejectedValueOnce(new Error('bar')) 577 | 578 | await expect(fn('foo')).rejects.toThrow('bar') 579 | expect(await fn('foo')).toBeUndefined() 580 | }) 581 | 582 | it('mockRejectedValueOnce: does not reject the Promise until the function is called', done => { 583 | const fn = jest.fn() 584 | 585 | when(fn).calledWith('foo').mockRejectedValueOnce(new Error('bar')) 586 | 587 | setTimeout(async () => { 588 | await expect(fn('foo')).rejects.toThrow('bar') 589 | done() 590 | }, 0) 591 | }) 592 | 593 | it('mockRejectedValueOnce: works with expectCalledWith', async () => { 594 | const fn = jest.fn() 595 | 596 | when(fn).expectCalledWith('foo').mockRejectedValueOnce(new Error('bar')) 597 | 598 | await expect(fn('foo')).rejects.toThrow('bar') 599 | expect(await fn('foo')).toBeUndefined() 600 | }) 601 | 602 | it('can be reset via `mockReset`', () => { 603 | const fn = jest.fn() 604 | 605 | when(fn).calledWith(1).mockReturnValue('return 1') 606 | expect(fn(1)).toEqual('return 1') 607 | 608 | fn.mockReset() 609 | expect(fn(1)).toBeUndefined() 610 | 611 | when(fn).calledWith(1).mockReturnValue('return 2') 612 | expect(fn(1)).toEqual('return 2') 613 | }) 614 | 615 | it('`mockReset` on unused when calls', () => { 616 | const fn = jest.fn() 617 | 618 | when(fn) 619 | .calledWith('test') 620 | .mockReturnValueOnce(1) 621 | .mockReturnValueOnce(2) 622 | // .mockReturnValueOnce(3) 623 | expect(fn('test')).toBe(1) 624 | expect(fn.mock.calls.length).toBe(1) 625 | 626 | fn.mockReset() 627 | 628 | when(fn).calledWith('test').mockReturnValueOnce(1) 629 | expect(fn('test')).toBe(1) 630 | expect(fn.mock.calls.length).toBe(1) 631 | }) 632 | 633 | describe('Default Behavior (no called with)', () => { 634 | describe('defaultX methods', () => { 635 | it('has a default and a non-default behavior (defaultReturnValue alias)', () => { 636 | let fn = jest.fn() 637 | 638 | when(fn) 639 | .defaultReturnValue('default') 640 | .calledWith('foo') 641 | .mockReturnValue('special') 642 | 643 | expect(fn('bar')).toEqual('default') 644 | expect(fn('foo')).toEqual('special') 645 | expect(fn('bar')).toEqual('default') 646 | 647 | fn = jest.fn() 648 | 649 | when(fn) 650 | .calledWith('foo') 651 | .mockReturnValue('special') 652 | .defaultReturnValue('default') 653 | 654 | expect(fn('bar')).toEqual('default') 655 | expect(fn('foo')).toEqual('special') 656 | expect(fn('bar')).toEqual('default') 657 | }) 658 | 659 | it('has a default which is falsy (defaultReturnValue alias)', () => { 660 | let fn = jest.fn() 661 | 662 | when(fn) 663 | .defaultReturnValue(false) 664 | .calledWith('foo') 665 | .mockReturnValue('special') 666 | 667 | expect(fn('bar')).toEqual(false) 668 | expect(fn('foo')).toEqual('special') 669 | expect(fn('bar')).toEqual(false) 670 | 671 | fn = jest.fn() 672 | 673 | when(fn) 674 | .calledWith('foo') 675 | .mockReturnValue('special') 676 | .defaultReturnValue(false) 677 | 678 | expect(fn('bar')).toEqual(false) 679 | expect(fn('foo')).toEqual('special') 680 | expect(fn('bar')).toEqual(false) 681 | }) 682 | 683 | it('has a default value which is a function (defaultReturnValue alias)', () => { 684 | const fn = jest.fn() 685 | const defaultValue = () => { 686 | } 687 | 688 | when(fn) 689 | .defaultReturnValue(defaultValue) 690 | .calledWith('bar').mockReturnValue('baz') 691 | 692 | expect(fn('foo')).toBe(defaultValue) 693 | }) 694 | 695 | it('has a default implementation (defaultReturnValue alias)', () => { 696 | let fn = jest.fn() 697 | 698 | when(fn) 699 | .defaultImplementation(() => 1) 700 | .calledWith('bar').mockReturnValue('baz') 701 | 702 | expect(fn('foo')).toBe(1) 703 | expect(fn('bar')).toBe('baz') 704 | 705 | fn = jest.fn() 706 | 707 | when(fn) 708 | .calledWith('bar').mockReturnValue('baz') 709 | .defaultImplementation(() => 1) 710 | 711 | expect(fn('foo')).toBe(1) 712 | expect(fn('bar')).toBe('baz') 713 | }) 714 | 715 | it('has access to args in a default implementation (defaultReturnValue alias)', () => { 716 | const fn = jest.fn() 717 | 718 | when(fn) 719 | .defaultImplementation(({ name }) => `Hello ${name}`) 720 | .calledWith({ name: 'bar' }).mockReturnValue('Goodbye bar') 721 | 722 | expect(fn({ name: 'foo' })).toBe('Hello foo') 723 | expect(fn({ name: 'bar' })).toBe('Goodbye bar') 724 | }) 725 | 726 | it('keeps the default with a lot of matchers (defaultReturnValue alias)', () => { 727 | const fn = jest.fn() 728 | 729 | when(fn) 730 | .calledWith('in1').mockReturnValue('out1') 731 | .calledWith('in2').mockReturnValue('out2') 732 | .calledWith('in3').mockReturnValue('out3') 733 | .calledWith('in4').mockReturnValueOnce('out4') 734 | .defaultReturnValue('default') 735 | 736 | expect(fn('foo')).toEqual('default') 737 | expect(fn('in2')).toEqual('out2') 738 | expect(fn('in4')).toEqual('out4') 739 | expect(fn('in1')).toEqual('out1') 740 | expect(fn('in3')).toEqual('out3') 741 | expect(fn('in4')).toEqual('default') 742 | }) 743 | 744 | it('has a default and non-default resolved value (defaultReturnValue alias)', async () => { 745 | const fn = jest.fn() 746 | 747 | when(fn) 748 | .calledWith('foo').mockResolvedValue('special') 749 | .defaultResolvedValue('default') 750 | 751 | await expect(fn('bar')).resolves.toEqual('default') 752 | await expect(fn('foo')).resolves.toEqual('special') 753 | }) 754 | 755 | it('can default a resolved value alone', async () => { 756 | const fn = jest.fn() 757 | 758 | when(fn) 759 | .defaultResolvedValue('default') 760 | 761 | await expect(fn('bar')).resolves.toEqual('default') 762 | await expect(fn('foo')).resolves.toEqual('default') 763 | }) 764 | 765 | it('has a default and non-default rejected value (defaultReturnValue alias)', async () => { 766 | const fn = jest.fn() 767 | 768 | when(fn) 769 | .calledWith('foo').mockRejectedValue(new Error('special')) 770 | .defaultRejectedValue(new Error('default')) 771 | 772 | await expect(fn('bar')).rejects.toThrow('default') 773 | await expect(fn('foo')).rejects.toThrow('special') 774 | }) 775 | 776 | it('can default a rejected value alone', async () => { 777 | const fn = jest.fn() 778 | 779 | when(fn) 780 | .defaultRejectedValue(new Error('default')) 781 | 782 | await expect(fn('bar')).rejects.toThrow('default') 783 | await expect(fn('foo')).rejects.toThrow('default') 784 | }) 785 | 786 | it('default reject interoperates with resolve (defaultReturnValue alias)', async () => { 787 | const fn = jest.fn() 788 | 789 | when(fn) 790 | .calledWith('foo').mockResolvedValue('mocked') 791 | .defaultRejectedValue(new Error('non-mocked interaction')) 792 | 793 | await expect(fn('foo')).resolves.toEqual('mocked') 794 | await expect(fn('bar')).rejects.toThrow('non-mocked interaction') 795 | }) 796 | 797 | it('can override default (defaultReturnValue alias)', () => { 798 | const fn = jest.fn() 799 | 800 | when(fn) 801 | .defaultReturnValue('oldDefault') 802 | .calledWith('foo').mockReturnValue('bar') 803 | .defaultReturnValue('newDefault') 804 | 805 | expect(fn('foo')).toEqual('bar') 806 | expect(fn('foo2')).toEqual('newDefault') 807 | }) 808 | 809 | it('allows defining the default NOT in a chained case (defaultReturnValue alias)', async () => { 810 | const fn = jest.fn() 811 | 812 | when(fn).defaultRejectedValue(false) 813 | 814 | when(fn) 815 | .calledWith(expect.anything()) 816 | .mockResolvedValue(true) 817 | 818 | await expect(fn('anything')).resolves.toEqual(true) 819 | await expect(fn()).rejects.toEqual(false) 820 | }) 821 | 822 | it('allows overriding the default NOT in a chained case (defaultReturnValue alias)', () => { 823 | const fn = jest.fn() 824 | 825 | when(fn) 826 | .calledWith(expect.anything()) 827 | .mockReturnValue(true) 828 | 829 | when(fn).defaultReturnValue(1) 830 | when(fn).defaultReturnValue(2) 831 | 832 | expect(fn()).toEqual(2) 833 | }) 834 | }) 835 | 836 | describe('legacy methods', () => { 837 | it('has a default and a non-default behavior', () => { 838 | const fn = jest.fn() 839 | 840 | when(fn) 841 | .mockReturnValue('default') 842 | .calledWith('foo') 843 | .mockReturnValue('special') 844 | 845 | expect(fn('bar')).toEqual('default') 846 | expect(fn('foo')).toEqual('special') 847 | expect(fn('bar')).toEqual('default') 848 | }) 849 | 850 | it('has a default which is falsy', () => { 851 | const fn = jest.fn() 852 | 853 | when(fn) 854 | .mockReturnValue(false) 855 | .calledWith('foo') 856 | .mockReturnValue('special') 857 | 858 | expect(fn('bar')).toEqual(false) 859 | expect(fn('foo')).toEqual('special') 860 | expect(fn('bar')).toEqual(false) 861 | }) 862 | 863 | it('has a default value which is a function', () => { 864 | const fn = jest.fn() 865 | const defaultValue = () => { 866 | } 867 | 868 | when(fn) 869 | .mockReturnValue(defaultValue) 870 | .calledWith('bar').mockReturnValue('baz') 871 | 872 | expect(fn('foo')).toBe(defaultValue) 873 | }) 874 | 875 | it('has a default implementation', () => { 876 | const fn = jest.fn() 877 | 878 | when(fn) 879 | .mockImplementation(() => 1) 880 | .calledWith('bar').mockReturnValue('baz') 881 | 882 | expect(fn('foo')).toBe(1) 883 | expect(fn('bar')).toBe('baz') 884 | }) 885 | 886 | it('has access to args in a default implementation', () => { 887 | const fn = jest.fn() 888 | 889 | when(fn) 890 | .mockImplementation(({ name }) => `Hello ${name}`) 891 | .calledWith({ name: 'bar' }).mockReturnValue('Goodbye bar') 892 | 893 | expect(fn({ name: 'foo' })).toBe('Hello foo') 894 | expect(fn({ name: 'bar' })).toBe('Goodbye bar') 895 | }) 896 | 897 | it('keeps the default with a lot of matchers', () => { 898 | const fn = jest.fn() 899 | 900 | when(fn) 901 | .mockReturnValue('default') 902 | .calledWith('in1').mockReturnValue('out1') 903 | .calledWith('in2').mockReturnValue('out2') 904 | .calledWith('in3').mockReturnValue('out3') 905 | .calledWith('in4').mockReturnValueOnce('out4') 906 | 907 | expect(fn('foo')).toEqual('default') 908 | expect(fn('in2')).toEqual('out2') 909 | expect(fn('in4')).toEqual('out4') 910 | expect(fn('in1')).toEqual('out1') 911 | expect(fn('in3')).toEqual('out3') 912 | expect(fn('in4')).toEqual('default') 913 | }) 914 | 915 | it('has a default and non-default resolved value', async () => { 916 | const fn = jest.fn() 917 | 918 | when(fn) 919 | .mockResolvedValue('default') 920 | .calledWith('foo').mockResolvedValue('special') 921 | 922 | await expect(fn('bar')).resolves.toEqual('default') 923 | await expect(fn('foo')).resolves.toEqual('special') 924 | }) 925 | 926 | it('has a default and non-default rejected value', async () => { 927 | const fn = jest.fn() 928 | 929 | when(fn) 930 | .mockRejectedValue(new Error('default')) 931 | .calledWith('foo').mockRejectedValue(new Error('special')) 932 | 933 | await expect(fn('bar')).rejects.toThrow('default') 934 | await expect(fn('foo')).rejects.toThrow('special') 935 | }) 936 | 937 | it('default reject interoperates with resolve', async () => { 938 | const fn = jest.fn() 939 | 940 | when(fn) 941 | .mockRejectedValue(new Error('non-mocked interaction')) 942 | .calledWith('foo').mockResolvedValue('mocked') 943 | 944 | await expect(fn('foo')).resolves.toEqual('mocked') 945 | await expect(fn('bar')).rejects.toThrow('non-mocked interaction') 946 | }) 947 | 948 | it('can override default', () => { 949 | const fn = jest.fn() 950 | 951 | when(fn) 952 | .mockReturnValue('oldDefault') 953 | .mockReturnValue('newDefault') 954 | .calledWith('foo').mockReturnValue('bar') 955 | 956 | expect(fn('foo')).toEqual('bar') 957 | expect(fn('foo2')).toEqual('newDefault') 958 | }) 959 | 960 | it('allows defining the default NOT in a chained case', async () => { 961 | const fn = jest.fn() 962 | 963 | when(fn).mockRejectedValue(false) 964 | 965 | when(fn) 966 | .calledWith(expect.anything()) 967 | .mockResolvedValue(true) 968 | 969 | await expect(fn('anything')).resolves.toEqual(true) 970 | await expect(fn()).rejects.toEqual(false) 971 | }) 972 | 973 | it('allows overriding the default NOT in a chained case', () => { 974 | const fn = jest.fn() 975 | 976 | when(fn).mockReturnValue(1) 977 | when(fn).mockReturnValue(2) 978 | 979 | when(fn) 980 | .calledWith(expect.anything()) 981 | .mockReturnValue(true) 982 | 983 | expect(fn()).toEqual(2) 984 | }) 985 | }) 986 | }) 987 | 988 | it('will throw because of unintended usage', () => { 989 | const fn = jest.fn() 990 | 991 | when(fn) 992 | .mockReturnValue('default') 993 | 994 | expect(fn()).toEqual('default') 995 | }) 996 | 997 | it('will not throw on old non-throwing case', () => { 998 | const fn = jest.fn() 999 | 1000 | when(fn) 1001 | 1002 | expect(fn).not.toThrow() 1003 | }) 1004 | 1005 | it('allows using mockImplementation', () => { 1006 | const fn = jest.fn() 1007 | 1008 | when(fn).calledWith('foo', 'bar').mockImplementation((...args) => args) 1009 | 1010 | expect(fn('foo', 'bar')).toEqual(['foo', 'bar']) 1011 | expect(fn('foo', 'bar')).toEqual(['foo', 'bar']) 1012 | 1013 | expect(fn('not-foo')).toBeUndefined() 1014 | }) 1015 | 1016 | it('allows using mockImplementationOnce', () => { 1017 | const fn = jest.fn() 1018 | 1019 | when(fn).calledWith('foo', 'bar').mockImplementationOnce((...args) => args) 1020 | 1021 | expect(fn('foo', 'bar')).toEqual(['foo', 'bar']) 1022 | expect(fn('foo')).toBeUndefined() 1023 | }) 1024 | 1025 | it('accepts a spied method:', () => { 1026 | class TheClass { 1027 | theMethod (theArgument) { 1028 | return 'real' 1029 | } 1030 | } 1031 | 1032 | const theInstance = new TheClass() 1033 | 1034 | const theSpiedMethod = jest.spyOn(theInstance, 'theMethod') 1035 | when(theSpiedMethod) 1036 | .calledWith(1) 1037 | .mockReturnValue('mock') 1038 | const returnValue = theInstance.theMethod(1) 1039 | expect(returnValue).toBe('mock') 1040 | }) 1041 | 1042 | it('keeps default function implementation when not matched', () => { 1043 | class TheClass { 1044 | fn () { 1045 | return 'real' 1046 | } 1047 | } 1048 | const instance = new TheClass() 1049 | const spy = jest.spyOn(instance, 'fn') 1050 | when(spy) 1051 | .calledWith(1) 1052 | .mockReturnValue('mock') 1053 | expect(instance.fn(2)).toBe('real') 1054 | }) 1055 | 1056 | it('keeps default mock implementation when not matched', () => { 1057 | const fn = jest.fn(() => { 1058 | return 'real' 1059 | }) 1060 | when(fn) 1061 | .calledWith(1) 1062 | .mockReturnValue('mock') 1063 | expect(fn(1)).toBe('mock') 1064 | expect(fn(2)).toBe('real') 1065 | }) 1066 | 1067 | it('keeps default mockReturnValue when not matched', () => { 1068 | const fn = jest.fn() 1069 | 1070 | when(fn).calledWith(1).mockReturnValue('a') 1071 | when(fn).mockReturnValue('b') 1072 | 1073 | expect(fn(1)).toEqual('a') // fails and will still return 'b' (as before my change) 1074 | expect(fn(2)).toEqual('b') 1075 | }) 1076 | 1077 | it('keeps call context when not matched', () => { 1078 | class TheClass { 1079 | call () { 1080 | return 'ok' 1081 | } 1082 | 1083 | request (...args) { 1084 | return this.call(...args) 1085 | } 1086 | } 1087 | 1088 | const theInstance = new TheClass() 1089 | 1090 | const theSpiedMethod = jest.spyOn(theInstance, 'request') 1091 | 1092 | when(theSpiedMethod) 1093 | .calledWith(1) 1094 | .mockReturnValue('mock') 1095 | 1096 | const unhandledCall = theInstance.request() 1097 | expect(unhandledCall).toBe('ok') 1098 | }) 1099 | 1100 | it('keeps call context when matched', () => { 1101 | class TheClass { 1102 | call () { 1103 | return 'ok' 1104 | } 1105 | 1106 | request (...args) { 1107 | return this.call(...args) 1108 | } 1109 | } 1110 | 1111 | const theInstance = new TheClass() 1112 | 1113 | const theSpiedMethod = jest.spyOn(theInstance, 'request') 1114 | 1115 | when(theSpiedMethod) 1116 | .calledWith(1) 1117 | .mockImplementation(function () { 1118 | return this.call() + '!' 1119 | }) 1120 | 1121 | const unhandledCall = theInstance.request(1) 1122 | expect(unhandledCall).toBe('ok!') 1123 | }) 1124 | 1125 | it('does not add to the number of assertion calls', () => { 1126 | expect.assertions(0) 1127 | }) 1128 | 1129 | it('allows reset of specific mock with specific args by just setting to undefined', () => { 1130 | const mock = jest.fn() 1131 | 1132 | when(mock).calledWith('hello').mockReturnValue('default') 1133 | expect(mock.__whenMock__.callMocks).toHaveLength(1) 1134 | when(mock).calledWith('hello').mockReturnValue(undefined) 1135 | expect(mock.__whenMock__.callMocks).toHaveLength(1) 1136 | when(mock).calledWith('hello').mockReturnValueOnce('a') 1137 | expect(mock.__whenMock__.callMocks).toHaveLength(2) 1138 | when(mock).calledWith('hello').mockReturnValueOnce('b') 1139 | expect(mock.__whenMock__.callMocks).toHaveLength(3) 1140 | 1141 | expect(mock('hello')).toEqual('a') 1142 | expect(mock('hello')).toEqual('b') 1143 | expect(mock('hello')).toEqual(undefined) 1144 | 1145 | console.log(mock.__whenMock__.callMocks) 1146 | 1147 | // There are still 3 when mocks, we just set the return value of the first one to undefined 1148 | expect(mock.__whenMock__.callMocks).toHaveLength(3) 1149 | }) 1150 | 1151 | it('allows reset of specific mock with specific args by using mockReset after calledWith', () => { 1152 | const mock = jest.fn() 1153 | 1154 | when(mock).calledWith('hello').mockReturnValue('default') 1155 | expect(mock.__whenMock__.callMocks).toHaveLength(1) 1156 | when(mock).calledWith('hello').mockReset() 1157 | expect(mock.__whenMock__.callMocks).toHaveLength(0) 1158 | when(mock).calledWith('hello').mockReturnValueOnce('a') 1159 | expect(mock.__whenMock__.callMocks).toHaveLength(1) 1160 | when(mock).calledWith('hello').mockReturnValueOnce('b') 1161 | expect(mock.__whenMock__.callMocks).toHaveLength(2) 1162 | 1163 | expect(mock('hello')).toEqual('a') 1164 | expect(mock('hello')).toEqual('b') 1165 | expect(mock('hello')).toEqual(undefined) 1166 | 1167 | // The first one was removed so only 2 remain 1168 | expect(mock.__whenMock__.callMocks).toHaveLength(2) 1169 | 1170 | // 1171 | // Try again with multiple matchers 1172 | const fn = jest.fn() 1173 | 1174 | when(fn).calledWith(1, 2, 3).mockReturnValue('yay!') 1175 | when(fn).calledWith(2).mockReturnValue('boo!') 1176 | 1177 | // Reset only the 1, 2, 3 mock call 1178 | when(fn).calledWith(1, 2, 3).mockReset() 1179 | 1180 | expect(fn(1, 2, 3)).toBeUndefined() // no mock for 1, 2, 3 1181 | expect(fn(2)).toEqual('boo!') // success! 1182 | }) 1183 | }) 1184 | }) 1185 | -------------------------------------------------------------------------------- /stryker.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | testRunner: 'jest', 4 | testFramework: 'jest', 5 | mutator: { name: 'javascript', excludedMutations: ['StringLiteral'] }, 6 | transpilers: [], 7 | reporter: ['clear-text', 'progress', 'html'], 8 | coverageAnalysis: 'off', 9 | mutate: ['src/**/*.js', '!src/**/*.test.js', '!src/**/*.testdata.js'], 10 | maxConcurrentTestRunners: 4 11 | }) 12 | } 13 | --------------------------------------------------------------------------------