├── .github └── workflows │ ├── publish-on-push.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── codecov.yml ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── MethodAction.ts ├── MethodStubCollection.ts ├── MethodStubSetter.ts ├── MethodStubVerificator.ts ├── MethodToStub.ts ├── Mock.ts ├── Spy.ts ├── capture │ └── ArgCaptor.ts ├── matcher │ ├── ArgsToMatchersValidator.ts │ └── type │ │ ├── AnyFunctionMatcher.ts │ │ ├── AnyNumberMatcher.ts │ │ ├── AnyOfClassMatcher.ts │ │ ├── AnyStringMatcher.ts │ │ ├── AnythingMatcher.ts │ │ ├── BetweenMatcher.ts │ │ ├── DeepEqualMatcher.ts │ │ ├── Matcher.ts │ │ ├── MatchingStringMatcher.ts │ │ ├── NotNullMatcher.ts │ │ ├── ObjectContainingMatcher.ts │ │ └── StrictEqualMatcher.ts ├── spy │ └── RealMethod.ts ├── stub │ ├── AbstractMethodStub.ts │ ├── CallFunctionMethodStub.ts │ ├── CallThroughMethodStub.ts │ ├── MethodStub.ts │ ├── RejectPromiseMethodStub.ts │ ├── ResolvePromiseMethodStub.ts │ ├── ReturnValueMethodStub.ts │ └── ThrowErrorMethodStub.ts ├── ts-mockito.ts ├── tsconfig.json └── utils │ ├── MethodCallToStringConverter.ts │ ├── MockableFunctionsFinder.ts │ ├── ObjectInspector.ts │ ├── ObjectPropertyCodeRetriever.ts │ └── types.ts ├── test ├── MethodAction.spec.ts ├── MethodStub.spec.ts ├── MethodStubCollection.spec.ts ├── capturing.spec.ts ├── defaultExport.spec.ts ├── instance.spec.ts ├── interface.spec.ts ├── matcher │ └── type │ │ ├── AnyFunctionMatcher.spec.ts │ │ ├── AnyNumberMatcher.spec.ts │ │ ├── AnyOfClassMatcher.spec.ts │ │ ├── AnyStringMatcher.spec.ts │ │ ├── AnythingMatcher.spec.ts │ │ ├── BetweenMatcher.spec.ts │ │ ├── DeepEqualMatcher.spec.ts │ │ ├── MatchingStringMatcher.spec.ts │ │ ├── NotNullMatcher.spec.ts │ │ ├── ObjectContainingMatcher.spec.ts │ │ └── StrictEqualMatcher.spec.ts ├── mocking.getter.spec.ts ├── mocking.properties.spec.ts ├── mocking.types.spec.ts ├── recording.multiple.behaviors.spec.ts ├── reseting.calls.spec.ts ├── reseting.spec.ts ├── spy.spec.ts ├── stubbing.method.spec.ts ├── utils │ ├── Bar.ts │ ├── Foo.ts │ ├── FooInterface.ts │ ├── MockableFunctionsFinder.spec.ts │ ├── ObjectInspector.spec.ts │ └── Thenable.ts └── verification.spec.ts ├── tsconfig.json └── tslint.json /.github/workflows/publish-on-push.yml: -------------------------------------------------------------------------------- 1 | name: Publish on push to master 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 16.10.0 15 | - run: npm ci 16 | - run: npm run build:ci 17 | 18 | - id: publish 19 | uses: JS-DevTools/npm-publish@v1 20 | with: 21 | token: ${{ secrets.NPM_PUBLISH_TOKEN }} 22 | package : ./dist/ts-mockito/package.json 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [pull_request, push] 3 | jobs: 4 | lint-build: 5 | name: "Lint & Build" 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | node: [ '16.x' ] 10 | os: [ ubuntu-latest, macOS-latest ] 11 | 12 | steps: 13 | # checkout code 14 | - uses: actions/checkout@v2 15 | # install node 16 | - name: Use Node.js ${{ matrix.node }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node }} 20 | # lint, build, test 21 | - run: npm install 22 | - run: npm run test:karma 23 | - run: npm run test:coverage 24 | - run: npm run lint 25 | - name: Codecov 26 | uses: codecov/codecov-action@v1 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | lib 3 | dist 4 | node_modules 5 | .idea 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib/**/* 3 | !package.json 4 | !tsconfig.json 5 | !README.md -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "14.17.1" 5 | 6 | install: 7 | - "npm install" 8 | 9 | script: 10 | - "npm run test:karma" 11 | - "npm run test:coverage" 12 | - "npm run lint" 13 | 14 | before_install: 15 | - pip install --user codecov 16 | 17 | after_success: 18 | - codecov 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | This is a fork of [`ts-mockito`](https://github.com/NagRock/ts-mockito/), published separately as `@typestrong/ts-mockito` 2 | 3 | We hope to eventually merge back with upstream `ts-mockito` and publish to the `ts-mockito` npm name, but until then, a fork lets us add new features. 4 | 5 | # Publishing 6 | 7 | Release notes / changelogs are published to Github Releases. 8 | For examples of how this looks, see here: https://github.com/NagRock/ts-mockito/releases 9 | and here: https://github.com/TypeStrong/ts-mockito/releases 10 | 11 | We publish to npm using `np`, which is a CLI tool that handles the repetitive tasks of publishing safely. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 NagRock 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @typestrong/ts-mockito [![build badge](https://github.com/TypeStrong/ts-mockito/actions/workflows/test.yml/badge.svg)](https://github.com/TypeStrong/ts-mockito/actions?query=branch%3Amaster) [![codecov](https://codecov.io/gh/TypeStrong/ts-mockito/branch/master/graph/badge.svg)](https://codecov.io/gh/TypeStrong/ts-mockito) 2 | 3 | > This is a fork of https://github.com/NagRock/ts-mockito. We hope to eventually re-merge and publish as ts-mockito. 4 | 5 | Mocking library for TypeScript inspired by http://mockito.org/ 6 | 7 | ## 1.x to 2.x migration guide 8 | [1.x to 2.x migration guide](https://github.com/cspotcode/ts-mockito/wiki/ts-mockito-1.x-to-2.x-migration-guide) 9 | 10 | ## Main features 11 | 12 | 13 | * Strongly typed 14 | * IDE autocomplete 15 | * Mock creation (`mock`) (also abstract classes) [#example](#basics) 16 | * Spying on real objects (`spy`) [#example](#spying-on-real-objects) 17 | * Changing mock behavior (`when`) via: 18 | * `thenReturn` - return value [#example](#stubbing-method-calls) 19 | * `thenThrow` - throw an error [#example](#throwing-errors) 20 | * `thenCall` - call custom method [#example](#custom-function) 21 | * `thenResolve` - resolve promise [#example](#resolving--rejecting-promises) 22 | * `thenReject` - rejects promise [#example](#resolving--rejecting-promises) 23 | * Checking if methods were called with given arguments (`verify`) 24 | * `anything`, `notNull`, `anyString`, `anyOfClass` etc. - for more flexible comparision 25 | * `once`, `twice`, `times`, `atLeast` etc. - allows call count verification [#example](#call-count-verification) 26 | * `calledBefore`, `calledAfter` - allows call order verification [#example](#call-order-verification) 27 | * Resetting mock (`reset`, `resetCalls`) [#example](#resetting-mock-calls), [#example](#resetting-mock) 28 | * Capturing arguments passed to method (`capture`) [#example](#capturing-method-arguments) 29 | * Recording multiple behaviors [#example](#recording-multiple-behaviors) 30 | * Readable error messages (ex. `'Expected "convertNumberToString(strictEqual(3))" to be called 2 time(s). But has been called 1 time(s).'`) 31 | 32 | ## Installation 33 | 34 | `npm install @typestrong/ts-mockito --save-dev` 35 | 36 | ## Usage 37 | 38 | ### Basics 39 | ``` typescript 40 | // Creating mock 41 | let mockedFoo:Foo = mock(Foo); 42 | 43 | // Getting instance from mock 44 | let foo:Foo = instance(mockedFoo); 45 | 46 | // Using instance in source code 47 | foo.getBar(3); 48 | foo.getBar(5); 49 | 50 | // Explicit, readable verification 51 | verify(mockedFoo.getBar(3)).called(); 52 | verify(mockedFoo.getBar(anything())).called(); 53 | ``` 54 | 55 | ### Stubbing method calls 56 | 57 | ``` typescript 58 | // Creating mock 59 | let mockedFoo:Foo = mock(Foo); 60 | 61 | // stub method before execution 62 | when(mockedFoo.getBar(3)).thenReturn('three'); 63 | 64 | // Getting instance 65 | let foo:Foo = instance(mockedFoo); 66 | 67 | // prints three 68 | console.log(foo.getBar(3)); 69 | 70 | // prints null, because "getBar(999)" was not stubbed 71 | console.log(foo.getBar(999)); 72 | ``` 73 | 74 | ### Stubbing getter value 75 | 76 | ``` typescript 77 | // Creating mock 78 | let mockedFoo:Foo = mock(Foo); 79 | 80 | // stub getter before execution 81 | when(mockedFoo.sampleGetter).thenReturn('three'); 82 | 83 | // Getting instance 84 | let foo:Foo = instance(mockedFoo); 85 | 86 | // prints three 87 | console.log(foo.sampleGetter); 88 | ``` 89 | 90 | ### Stubbing property values that have no getters 91 | 92 | Syntax is the same as with getter values. 93 | 94 | Please note, that stubbing properties that don't have getters only works if [Proxy](http://www.ecma-international.org/ecma-262/6.0/#sec-proxy-objects) object is available (ES6). 95 | 96 | ### Call count verification 97 | 98 | ``` typescript 99 | // Creating mock 100 | let mockedFoo:Foo = mock(Foo); 101 | 102 | // Getting instance 103 | let foo:Foo = instance(mockedFoo); 104 | 105 | // Some calls 106 | foo.getBar(1); 107 | foo.getBar(2); 108 | foo.getBar(2); 109 | foo.getBar(3); 110 | 111 | // Call count verification 112 | verify(mockedFoo.getBar(1)).once(); // was called with arg === 1 only once 113 | verify(mockedFoo.getBar(2)).twice(); // was called with arg === 2 exactly two times 114 | verify(mockedFoo.getBar(between(2, 3))).thrice(); // was called with arg between 2-3 exactly three times 115 | verify(mockedFoo.getBar(anyNumber()).times(4); // was called with any number arg exactly four times 116 | verify(mockedFoo.getBar(2)).atLeast(2); // was called with arg === 2 min two times 117 | verify(mockedFoo.getBar(anything())).atMost(4); // was called with any argument max four times 118 | verify(mockedFoo.getBar(4)).never(); // was never called with arg === 4 119 | ``` 120 | 121 | ### Call order verification 122 | 123 | ``` typescript 124 | // Creating mock 125 | let mockedFoo:Foo = mock(Foo); 126 | let mockedBar:Bar = mock(Bar); 127 | 128 | // Getting instance 129 | let foo:Foo = instance(mockedFoo); 130 | let bar:Bar = instance(mockedBar); 131 | 132 | // Some calls 133 | foo.getBar(1); 134 | bar.getFoo(2); 135 | 136 | // Call order verification 137 | verify(mockedFoo.getBar(1)).calledBefore(mockedBar.getFoo(2)); // foo.getBar(1) has been called before bar.getFoo(2) 138 | verify(mockedBar.getFoo(2)).calledAfter(mockedFoo.getBar(1)); // bar.getFoo(2) has been called before foo.getBar(1) 139 | verify(mockedFoo.getBar(1)).calledBefore(mockedBar.getFoo(999999)); // throws error (mockedBar.getFoo(999999) has never been called) 140 | ``` 141 | 142 | ### Throwing errors 143 | 144 | ``` typescript 145 | let mockedFoo:Foo = mock(Foo); 146 | 147 | when(mockedFoo.getBar(10)).thenThrow(new Error('fatal error')); 148 | 149 | let foo:Foo = instance(mockedFoo); 150 | try { 151 | foo.getBar(10); 152 | } catch (error:Error) { 153 | console.log(error.message); // 'fatal error' 154 | } 155 | ``` 156 | 157 | ### Custom function 158 | 159 | You can also stub method with your own implementation 160 | 161 | ``` typescript 162 | let mockedFoo:Foo = mock(Foo); 163 | let foo:Foo = instance(mockedFoo); 164 | 165 | when(mockedFoo.sumTwoNumbers(anyNumber(), anyNumber())).thenCall((arg1:number, arg2:number) => { 166 | return arg1 * arg2; 167 | }); 168 | 169 | // prints '50' because we've changed sum method implementation to multiply! 170 | console.log(foo.sumTwoNumbers(5, 10)); 171 | ``` 172 | 173 | ### Resolving / rejecting promises 174 | 175 | You can also stub method to resolve / reject promise 176 | 177 | ``` typescript 178 | let mockedFoo:Foo = mock(Foo); 179 | 180 | when(mockedFoo.fetchData("a")).thenResolve({id: "a", value: "Hello world"}); 181 | when(mockedFoo.fetchData("b")).thenReject(new Error("b does not exist")); 182 | ``` 183 | 184 | ### Resetting mock calls 185 | 186 | You can reset just mock call counter 187 | 188 | ``` typescript 189 | // Creating mock 190 | let mockedFoo:Foo = mock(Foo); 191 | 192 | // Getting instance 193 | let foo:Foo = instance(mockedFoo); 194 | 195 | // Some calls 196 | foo.getBar(1); 197 | foo.getBar(1); 198 | verify(mockedFoo.getBar(1)).twice(); // getBar with arg "1" has been called twice 199 | 200 | // Reset mock 201 | resetCalls(mockedFoo); 202 | 203 | // Call count verification 204 | verify(mockedFoo.getBar(1)).never(); // has never been called after reset 205 | ``` 206 | 207 | You can also reset calls of multiple mocks at once `resetCalls(firstMock, secondMock, thirdMock)` 208 | 209 | ### Resetting mock 210 | 211 | Or reset mock call counter with all stubs 212 | 213 | ``` typescript 214 | // Creating mock 215 | let mockedFoo:Foo = mock(Foo); 216 | when(mockedFoo.getBar(1)).thenReturn("one"). 217 | 218 | // Getting instance 219 | let foo:Foo = instance(mockedFoo); 220 | 221 | // Some calls 222 | console.log(foo.getBar(1)); // "one" - as defined in stub 223 | console.log(foo.getBar(1)); // "one" - as defined in stub 224 | verify(mockedFoo.getBar(1)).twice(); // getBar with arg "1" has been called twice 225 | 226 | // Reset mock 227 | reset(mockedFoo); 228 | 229 | // Call count verification 230 | verify(mockedFoo.getBar(1)).never(); // has never been called after reset 231 | console.log(foo.getBar(1)); // null - previously added stub has been removed 232 | ``` 233 | 234 | You can also reset multiple mocks at once `reset(firstMock, secondMock, thirdMock)` 235 | 236 | ### Capturing method arguments 237 | 238 | ``` typescript 239 | let mockedFoo:Foo = mock(Foo); 240 | let foo:Foo = instance(mockedFoo); 241 | 242 | // Call method 243 | foo.sumTwoNumbers(1, 2); 244 | 245 | // Check first arg captor values 246 | const [firstArg, secondArg] = capture(mockedFoo.sumTwoNumbers).last(); 247 | console.log(firstArg); // prints 1 248 | console.log(secondArg); // prints 2 249 | ``` 250 | 251 | You can also get other calls using `first()`, `second()`, `byCallIndex(3)` and more... 252 | 253 | ### Recording multiple behaviors 254 | 255 | You can set multiple returning values for same matching values 256 | 257 | ``` typescript 258 | const mockedFoo:Foo = mock(Foo); 259 | 260 | when(mockedFoo.getBar(anyNumber())).thenReturn('one').thenReturn('two').thenReturn('three'); 261 | 262 | const foo:Foo = instance(mockedFoo); 263 | 264 | console.log(foo.getBar(1)); // one 265 | console.log(foo.getBar(1)); // two 266 | console.log(foo.getBar(1)); // three 267 | console.log(foo.getBar(1)); // three - last defined behavior will be repeated infinitely 268 | ``` 269 | 270 | Another example with specific values 271 | 272 | ``` typescript 273 | let mockedFoo:Foo = mock(Foo); 274 | 275 | when(mockedFoo.getBar(1)).thenReturn('one').thenReturn('another one'); 276 | when(mockedFoo.getBar(2)).thenReturn('two'); 277 | 278 | let foo:Foo = instance(mockedFoo); 279 | 280 | console.log(foo.getBar(1)); // one 281 | console.log(foo.getBar(2)); // two 282 | console.log(foo.getBar(1)); // another one 283 | console.log(foo.getBar(1)); // another one - this is last defined behavior for arg '1' so it will be repeated 284 | console.log(foo.getBar(2)); // two 285 | console.log(foo.getBar(2)); // two - this is last defined behavior for arg '2' so it will be repeated 286 | ``` 287 | 288 | Short notation: 289 | 290 | ``` typescript 291 | const mockedFoo:Foo = mock(Foo); 292 | 293 | // You can specify return values as multiple thenReturn args 294 | when(mockedFoo.getBar(anyNumber())).thenReturn('one', 'two', 'three'); 295 | 296 | const foo:Foo = instance(mockedFoo); 297 | 298 | console.log(foo.getBar(1)); // one 299 | console.log(foo.getBar(1)); // two 300 | console.log(foo.getBar(1)); // three 301 | console.log(foo.getBar(1)); // three - last defined behavior will be repeated infinity 302 | ``` 303 | 304 | Possible errors: 305 | 306 | ``` typescript 307 | const mockedFoo:Foo = mock(Foo); 308 | 309 | // When multiple matchers, matches same result: 310 | when(mockedFoo.getBar(anyNumber())).thenReturn('one'); 311 | when(mockedFoo.getBar(3)).thenReturn('one'); 312 | 313 | const foo:Foo = instance(mockedFoo); 314 | foo.getBar(3); // MultipleMatchersMatchSameStubError will be thrown, two matchers match same method call 315 | 316 | ``` 317 | 318 | ### Mocking interfaces 319 | 320 | You can mock interfaces too, just instead of passing type to `mock` function, set `mock` function generic type 321 | Mocking interfaces requires `Proxy` implementation 322 | 323 | ``` typescript 324 | let mockedFoo:Foo = mock(); // instead of mock(FooInterface) 325 | const foo: SampleGeneric = instance(mockedFoo); 326 | ``` 327 | 328 | ### Mocking types 329 | 330 | You can mock abstract classes 331 | 332 | ``` typescript 333 | const mockedFoo: SampleAbstractClass = mock(SampleAbstractClass); 334 | const foo: SampleAbstractClass = instance(mockedFoo); 335 | ``` 336 | 337 | You can also mock generic classes, but note that generic type is just needed by mock type definition 338 | 339 | ``` typescript 340 | const mockedFoo: SampleGeneric = mock(SampleGeneric); 341 | const foo: SampleGeneric = instance(mockedFoo); 342 | 343 | ``` 344 | 345 | ### Spying on real objects 346 | 347 | You can partially mock an existing instance: 348 | 349 | ``` typescript 350 | const foo: Foo = new Foo(); 351 | const spiedFoo = spy(foo); 352 | 353 | when(spiedFoo.getBar(3)).thenReturn('one'); 354 | 355 | console.log(foo.getBar(3)); // 'one' 356 | console.log(foo.getBaz()); // call to a real method 357 | ``` 358 | 359 | You can spy on plain objects too: 360 | 361 | ``` typescript 362 | const foo = { bar: () => 42 }; 363 | const spiedFoo = spy(foo); 364 | 365 | foo.bar(); 366 | 367 | console.log(capture(spiedFoo.bar).last()); // [42] 368 | ``` 369 | 370 | ### Thanks 371 | 372 | * Szczepan Faber (https://www.linkedin.com/in/szczepiq) 373 | * Sebastian Konkol (https://www.linkedin.com/in/sebastiankonkol) 374 | * Clickmeeting (http://clickmeeting.com) 375 | * Michał Stocki (https://github.com/michalstocki) 376 | * Łukasz Bendykowski (https://github.com/viman) 377 | * Andrey Ermakov (https://github.com/dreef3) 378 | * Markus Ende (https://github.com/Markus-Ende) 379 | * Thomas Hilzendegen (https://github.com/thomashilzendegen) 380 | * Johan Blumenberg (https://github.com/johanblumenberg) 381 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | ignore: 3 | - "test/**/*" 4 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | 4 | frameworks: ["jasmine", "karma-typescript"], 5 | 6 | files: [ 7 | "node_modules/babel-polyfill/dist/polyfill.js", 8 | "./src/**/*.ts", 9 | "./test/**/*.ts" 10 | ], 11 | 12 | preprocessors: { 13 | "**/*.ts": ["karma-typescript"] 14 | }, 15 | 16 | karmaTypescriptConfig: { 17 | bundlerOptions: { 18 | entrypoints: /\.spec\.(ts|tsx)$/, 19 | resolve: { 20 | directories: ["src", "node_modules"] 21 | } 22 | }, 23 | 24 | tsconfig: "./tsconfig.json" 25 | }, 26 | 27 | reporters: ["progress", "mocha"], 28 | 29 | browsers: ["CustomChromeHeadless"], 30 | 31 | mochaReporter: { 32 | output: 'minimal' 33 | }, 34 | 35 | customLaunchers: { 36 | 'CustomChromeHeadless': { 37 | base: 'ChromeHeadless', 38 | flags: [ 39 | '--no-sandbox', 40 | '--disable-setuid-sandbox' 41 | ], 42 | debug: true 43 | } 44 | }, 45 | 46 | logLevel: config.LOG_INFO, 47 | autoWatch: true, 48 | singleRun: false 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@typestrong/ts-mockito", 3 | "version": "2.7.12", 4 | "description": "Mocking library for TypeScript", 5 | "main": "lib/ts-mockito.js", 6 | "typings": "lib/ts-mockito", 7 | "scripts": { 8 | "compile": "npm run test:compile-check && npm run cleanup && ./node_modules/.bin/tsc -p ./src", 9 | "cleanup": "rimraf 'lib' && rimraf 'dist'", 10 | "mkdir:dist": "mkdir -p dist/ts-mockito/", 11 | "test": "npm run test:jest && npm run test:karma", 12 | "test:watch": "npm run test:jest:watch", 13 | "test:coverage": "jest --coverage --maxWorkers=4", 14 | "test:jest": "jest", 15 | "test:jest:watch": "jest --watch", 16 | "test:karma": "./node_modules/.bin/karma start karma.conf.js --single-run", 17 | "test:karma:watch": "./node_modules/.bin/karma start karma.conf.js", 18 | "test:compile-check": "tsc --noEmit -p tsconfig.json", 19 | "lint": "tslint -c tslint.json 'src/**/*.ts' 'test/**/*.ts'", 20 | "cp:readme": "cp -rf README.md dist/ts-mockito/", 21 | "cp:license": "cp -rf LICENSE dist/ts-mockito/", 22 | "mv:lib": "mv lib dist/ts-mockito/", 23 | "cp:pkjson": "cp -rf package.json dist/ts-mockito/", 24 | "cp:all": "npm run cp:readme && npm run cp:license && npm run cp:pkjson", 25 | "build": "npm run bump && npm run compile && npm run mkdir:dist && npm run cp:all && npm run mv:lib", 26 | "build:ci": "npm run compile && npm run mkdir:dist && npm run cp:all && npm run mv:lib", 27 | "bump": "npm version patch --force" 28 | }, 29 | "author": "kuster.maciej@gmail.com", 30 | "contributors": [ 31 | "Andrew Bradley ", 32 | "Liron Hazan" 33 | ], 34 | "license": "MIT", 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/TypeStrong/ts-mockito" 38 | }, 39 | "keywords": [ 40 | "mock", 41 | "typescript", 42 | "tests", 43 | "fake", 44 | "stub", 45 | "spy", 46 | "javascript" 47 | ], 48 | "devDependencies": { 49 | "@types/jasmine": "^4.0.3", 50 | "@types/lodash": "^4.14.182", 51 | "@types/node": "^18.6.1", 52 | "@types/safe-json-stringify": "^1.1.2", 53 | "babel-polyfill": "^6.26.0", 54 | "jest": "^28.1.3", 55 | "karma": "^6.4.0", 56 | "karma-chrome-launcher": "^3.1.1", 57 | "karma-cli": "^2.0.0", 58 | "karma-jasmine": "^5.1.0", 59 | "karma-mocha-reporter": "^2.2.5", 60 | "karma-typescript": "^5.5.3", 61 | "karma-typescript-preprocessor": "^0.4.0", 62 | "rimraf": "^3.0.2", 63 | "ts-jest": "^28.0.7", 64 | "tslint": "^6.1.3", 65 | "typescript": "^4.7.4" 66 | }, 67 | "dependencies": { 68 | "@babel/parser": "^7.24.7", 69 | "lodash": "^4.17.5", 70 | "safe-json-stringify": "^1.2.0" 71 | }, 72 | "resolutions": { 73 | "write-file-atomic": "2.4.1" 74 | }, 75 | "jest": { 76 | "testEnvironment": "node", 77 | "transform": { 78 | "^.+\\.ts$": "ts-jest" 79 | }, 80 | "testRegex": "(/__tests__/.*|\\.(spec))\\.(ts|js)$", 81 | "moduleFileExtensions": [ 82 | "ts", 83 | "js", 84 | "json" 85 | ], 86 | "collectCoverageFrom": [ 87 | "src/**/*.ts" 88 | ], 89 | "coverageReporters": [ 90 | "lcov" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/MethodAction.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "./matcher/type/Matcher"; 2 | 3 | export class MethodAction { 4 | private static globalCallIndex: number = 0; 5 | private callIndex: number; 6 | 7 | constructor(public methodName: string, public args: any[]) { 8 | this.callIndex = ++MethodAction.globalCallIndex; 9 | } 10 | 11 | public isApplicable(methodName: string, matchers: Matcher[]): boolean { 12 | const methodNameMatch = this.methodName === methodName; 13 | const argumentsCountMatch = this.args.length === matchers.length; 14 | if (!methodNameMatch || !argumentsCountMatch) { 15 | return false; 16 | } 17 | return matchers.every((matcher: Matcher, index: number) => matcher.match(this.args[index])); 18 | } 19 | 20 | public getCallIndex(): number { 21 | return this.callIndex; 22 | } 23 | 24 | public hasBeenCalledBefore(action: MethodAction): boolean { 25 | return this.getCallIndex() < action.getCallIndex(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/MethodStubCollection.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import {MethodStub} from "./stub/MethodStub"; 3 | 4 | export class MethodStubCollection { 5 | private items: MethodStub[] = []; 6 | 7 | public add(item: MethodStub) { 8 | this.items.push(item); 9 | } 10 | 11 | public getLastMatchingGroupIndex(args): number { 12 | const matchingGroup = _.clone(this.items).reverse().find((item: MethodStub) => item.isApplicable(args)); 13 | return matchingGroup ? matchingGroup.getGroupIndex() : -1; 14 | } 15 | 16 | public getFirstMatchingFromGroupAndRemoveIfNotLast(groupIndex: number, args: any[]): MethodStub | null { 17 | const result = this.getFirstMatchingFromGroup(groupIndex, args); 18 | this.removeIfNotLast(groupIndex, args); 19 | return result; 20 | } 21 | 22 | public hasMatchingInAnyGroup(args: any[]): boolean { 23 | return this.items.some((item: MethodStub) => item.isApplicable(args)); 24 | } 25 | 26 | private removeIfNotLast(groupIndex: number, args: any[]): void { 27 | const index = this.getFirstMatchingIndexFromGroup(groupIndex, args); 28 | if (index > -1 && this.getItemsCountInGroup(groupIndex) > 1) { 29 | this.items.splice(index, 1); 30 | } 31 | } 32 | 33 | private getFirstMatchingFromGroup(groupIndex: number, args: any[]): MethodStub | null { 34 | return this.items.find((item: MethodStub) => item.getGroupIndex() === groupIndex && item.isApplicable(args)) ?? null; 35 | } 36 | 37 | private getFirstMatchingIndexFromGroup(groupIndex: number, args: any[]): number { 38 | return this.items.findIndex((item: MethodStub) => item.getGroupIndex() === groupIndex && item.isApplicable(args)); 39 | } 40 | 41 | private getItemsCountInGroup(groupIndex: number): number { 42 | return this.items.filter((item: MethodStub) => item.getGroupIndex() === groupIndex).length; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/MethodStubSetter.ts: -------------------------------------------------------------------------------- 1 | import {MethodToStub} from "./MethodToStub"; 2 | import {CallFunctionMethodStub} from "./stub/CallFunctionMethodStub"; 3 | import {RejectPromiseMethodStub} from "./stub/RejectPromiseMethodStub"; 4 | import {ResolvePromiseMethodStub} from "./stub/ResolvePromiseMethodStub"; 5 | import {ReturnValueMethodStub} from "./stub/ReturnValueMethodStub"; 6 | import {ThrowErrorMethodStub} from "./stub/ThrowErrorMethodStub"; 7 | 8 | export class MethodStubSetter { 9 | private static globalGroupIndex: number = 0; 10 | private groupIndex: number; 11 | 12 | constructor(private methodToStub: MethodToStub) { 13 | this.groupIndex = ++MethodStubSetter.globalGroupIndex; 14 | } 15 | 16 | public thenReturn(...rest: T[]): this { 17 | this.convertToPropertyIfIsNotAFunction(); 18 | rest.forEach(value => { 19 | this.methodToStub.methodStubCollection.add(new ReturnValueMethodStub(this.groupIndex, this.methodToStub.matchers, value)); 20 | }); 21 | return this; 22 | } 23 | 24 | public thenThrow(...rest: Error[]): this { 25 | this.convertToPropertyIfIsNotAFunction(); 26 | rest.forEach(error => { 27 | this.methodToStub.methodStubCollection.add(new ThrowErrorMethodStub(this.groupIndex, this.methodToStub.matchers, error)); 28 | }); 29 | return this; 30 | } 31 | 32 | public thenCall(func: (...args: any[]) => any): this { 33 | this.convertToPropertyIfIsNotAFunction(); 34 | this.methodToStub.methodStubCollection.add(new CallFunctionMethodStub(this.groupIndex, this.methodToStub.matchers, func)); 35 | return this; 36 | } 37 | 38 | public thenResolve(...rest: (ResolveType | undefined)[]): this { 39 | this.convertToPropertyIfIsNotAFunction(); 40 | // Resolves undefined if no resolve values are given. 41 | if (rest.length === 0) { 42 | rest.push(undefined); 43 | } 44 | rest.forEach(value => { 45 | this.methodToStub.methodStubCollection.add(new ResolvePromiseMethodStub(this.groupIndex, this.methodToStub.matchers, value)); 46 | }); 47 | return this; 48 | } 49 | 50 | public thenReject(...rest: Error[]): this { 51 | this.convertToPropertyIfIsNotAFunction(); 52 | // Resolves undefined if no resolve values are given. 53 | if (rest.length === 0) { 54 | rest.push(new Error(`mocked '${this.methodToStub.name}' rejected`)); 55 | } 56 | rest.forEach(value => { 57 | this.methodToStub.methodStubCollection.add(new RejectPromiseMethodStub(this.groupIndex, this.methodToStub.matchers, value)); 58 | }); 59 | return this; 60 | } 61 | 62 | private thenDoNothing(...rest: T[]): this { 63 | this.convertToPropertyIfIsNotAFunction(); 64 | return this; 65 | } 66 | 67 | private convertToPropertyIfIsNotAFunction(): void { 68 | if (!this.methodToStub.methodStubCollection) { 69 | const info = (this.methodToStub as any)("__tsMockitoGetInfo"); 70 | delete info.mocker.mock[info.key]; 71 | delete info.mocker.instance[info.key]; 72 | 73 | info.mocker.createPropertyStub(info.key); 74 | info.mocker.createInstancePropertyDescriptorListener(info.key, {}, undefined); 75 | info.mocker.createInstanceActionListener(info.key, undefined); 76 | this.methodToStub = info.mocker.mock[info.key]; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/MethodStubVerificator.ts: -------------------------------------------------------------------------------- 1 | import {MethodToStub} from "./MethodToStub"; 2 | import {MethodCallToStringConverter} from "./utils/MethodCallToStringConverter"; 3 | 4 | export class MethodStubVerificator { 5 | private methodCallToStringConverter: MethodCallToStringConverter = new MethodCallToStringConverter(); 6 | 7 | constructor(private methodToVerify: MethodToStub) { 8 | 9 | } 10 | 11 | public called(): void { 12 | this.atLeast(1); 13 | } 14 | 15 | public never(): void { 16 | this.times(0); 17 | } 18 | 19 | public once(): void { 20 | this.times(1); 21 | } 22 | 23 | public twice(): void { 24 | this.times(2); 25 | } 26 | 27 | public thrice(): void { 28 | this.times(3); 29 | } 30 | 31 | public times(value: number): void { 32 | const allMatchingActions = this.methodToVerify.mocker.getAllMatchingActions(this.methodToVerify.name, this.methodToVerify.matchers); 33 | if (value !== allMatchingActions.length) { 34 | const methodToVerifyAsString = this.methodCallToStringConverter.convert(this.methodToVerify); 35 | const msg = `Expected "${methodToVerifyAsString}to be called ${value} time(s). But has been called ${allMatchingActions.length} time(s).`; 36 | throw new Error(`${msg} 37 | ${this.actualCalls()}`); 38 | } 39 | } 40 | 41 | public atLeast(value: number): void { 42 | const allMatchingActions = this.methodToVerify.mocker.getAllMatchingActions(this.methodToVerify.name, this.methodToVerify.matchers); 43 | if (value > allMatchingActions.length) { 44 | const methodToVerifyAsString = this.methodCallToStringConverter.convert(this.methodToVerify); 45 | throw new Error(`Expected "${methodToVerifyAsString}to be called at least ${value} time(s). But has been called ${allMatchingActions.length} time(s).`); 46 | } 47 | } 48 | 49 | public atMost(value: number): void { 50 | const allMatchingActions = this.methodToVerify.mocker.getAllMatchingActions(this.methodToVerify.name, this.methodToVerify.matchers); 51 | if (value < allMatchingActions.length) { 52 | const methodToVerifyAsString = this.methodCallToStringConverter.convert(this.methodToVerify); 53 | throw new Error(`Expected "${methodToVerifyAsString}to be called at least ${value} time(s). But has been called ${allMatchingActions.length} time(s).`); 54 | } 55 | } 56 | 57 | public calledBefore(method: any): void { 58 | const firstMethodAction = this.methodToVerify.mocker.getFirstMatchingAction(this.methodToVerify.name, this.methodToVerify.matchers); 59 | const secondMethodAction = method.mocker.getFirstMatchingAction(method.name, method.matchers); 60 | const mainMethodToVerifyAsString = this.methodCallToStringConverter.convert(this.methodToVerify); 61 | const secondMethodAsString = this.methodCallToStringConverter.convert(method); 62 | const errorBeginning = `Expected "${mainMethodToVerifyAsString} to be called before ${secondMethodAsString}`; 63 | 64 | if (firstMethodAction && secondMethodAction) { 65 | if (!firstMethodAction.hasBeenCalledBefore(secondMethodAction)) { 66 | throw new Error(`${errorBeginning}but has been called after.`); 67 | } 68 | } else if (firstMethodAction && !secondMethodAction) { 69 | throw new Error(`${errorBeginning}but ${secondMethodAsString}has never been called.`); 70 | } else if (!firstMethodAction && secondMethodAction) { 71 | throw new Error(`${errorBeginning}but ${mainMethodToVerifyAsString}has never been called.`); 72 | } else { 73 | throw new Error(`${errorBeginning}but none of them has been called.`); 74 | } 75 | } 76 | 77 | public calledAfter(method: any): void { 78 | const firstMethodAction = this.methodToVerify.mocker.getFirstMatchingAction(this.methodToVerify.name, this.methodToVerify.matchers); 79 | const secondMethodAction = method.mocker.getFirstMatchingAction(method.name, method.matchers); 80 | const mainMethodToVerifyAsString = this.methodCallToStringConverter.convert(this.methodToVerify); 81 | const secondMethodAsString = this.methodCallToStringConverter.convert(method); 82 | const errorBeginning = `Expected "${mainMethodToVerifyAsString}to be called after ${secondMethodAsString}`; 83 | 84 | if (firstMethodAction && secondMethodAction) { 85 | if (firstMethodAction.hasBeenCalledBefore(secondMethodAction)) { 86 | throw new Error(`${errorBeginning}but has been called before.`); 87 | } 88 | } else if (firstMethodAction && !secondMethodAction) { 89 | throw new Error(`${errorBeginning}but ${secondMethodAsString}has never been called.`); 90 | } else if (!firstMethodAction && secondMethodAction) { 91 | throw new Error(`${errorBeginning}but ${mainMethodToVerifyAsString}has never been called.`); 92 | } else { 93 | throw new Error(`${errorBeginning}but none of them has been called.`); 94 | } 95 | } 96 | 97 | private actualCalls() { 98 | const calls = this.methodToVerify.mocker.getActionsByName(this.methodToVerify.name); 99 | return `Actual calls: 100 | ${this.methodCallToStringConverter.convertActualCalls(calls).join("\n ")}`; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/MethodToStub.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "./matcher/type/Matcher"; 2 | import {MethodStubCollection} from "./MethodStubCollection"; 3 | import {Mocker} from "./Mock"; 4 | 5 | export class MethodToStub { 6 | constructor(public methodStubCollection: MethodStubCollection, 7 | public matchers: Matcher[], 8 | public mocker: Mocker, 9 | public name: string) { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Mock.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import {Matcher} from "./matcher/type/Matcher"; 3 | import {MethodAction} from "./MethodAction"; 4 | import {MethodStubCollection} from "./MethodStubCollection"; 5 | import {MethodToStub} from "./MethodToStub"; 6 | import {MethodStub} from "./stub/MethodStub"; 7 | import {ReturnValueMethodStub} from "./stub/ReturnValueMethodStub"; 8 | import {strictEqual} from "./ts-mockito"; 9 | import {MockableFunctionsFinder} from "./utils/MockableFunctionsFinder"; 10 | import {ObjectInspector} from "./utils/ObjectInspector"; 11 | 12 | export class Mocker { 13 | public mock: any = {}; 14 | private methodStubCollections: any = {}; 15 | private methodActions: MethodAction[] = []; 16 | private mockableFunctionsFinder = new MockableFunctionsFinder(); 17 | private excludedPropertyNames: string[] = ["hasOwnProperty"]; 18 | private defaultedPropertyNames: string[] = ["Symbol(Symbol.toPrimitive)", "then", "catch"]; 19 | 20 | constructor(private clazz: any, public instance: any = {}, isSpy: boolean = false) { 21 | this.mock.__tsmockitoInstance = this.instance; 22 | this.mock.__tsmockitoMocker = this; 23 | if (_.isObject(this.clazz) && _.isObject(this.instance)) { 24 | this.processProperties((this.clazz as any).prototype); 25 | if (!isSpy || typeof Proxy === "undefined") { 26 | this.processClassCode(this.clazz); 27 | } 28 | } 29 | if (typeof Proxy !== "undefined" && this.clazz) { 30 | this.mock.__tsmockitoInstance = new Proxy(this.instance, this.createCatchAllHandlerForRemainingPropertiesWithoutGetters()); 31 | } else if (typeof Proxy !== "undefined" && !this.clazz) { 32 | this.instance = new Proxy(this.instance, { 33 | get: (target: any, name: PropertyKey) => { 34 | if (this.excludedPropertyNames.indexOf(name.toString()) >= 0) { 35 | return target[name]; 36 | } 37 | 38 | const hasMethodStub = name in target; 39 | 40 | if (!hasMethodStub) { 41 | if (this.defaultedPropertyNames.indexOf(name.toString()) >= 0) { 42 | return undefined; 43 | } 44 | return this.createActionListener(name.toString()); 45 | } 46 | return target[name]; 47 | }, 48 | }); 49 | this.mock.__tsmockitoInstance = this.instance; 50 | } 51 | } 52 | 53 | public getMock(): any { 54 | if (typeof Proxy === "undefined") { 55 | return this.mock; 56 | } 57 | if (typeof Proxy !== "undefined" && this.clazz) { 58 | return new Proxy(this.mock, this.createCatchAllHandlerForRemainingPropertiesWithoutGetters()); 59 | } 60 | return new Proxy(this.mock, { 61 | get: (target: any, name: PropertyKey) => { 62 | const hasProp = name in target; 63 | if (hasProp) { 64 | return target[name]; 65 | } 66 | 67 | const hasMethodStub = name in target; 68 | if (!hasMethodStub) { 69 | this.createMethodStub(name.toString()); 70 | this.createInstanceActionListener(name.toString(), {}); 71 | } 72 | return this.mock[name.toString()]; 73 | }, 74 | }); 75 | } 76 | 77 | public createCatchAllHandlerForRemainingPropertiesWithoutGetters(): any { 78 | return { 79 | get: (target: any, name: PropertyKey) => { 80 | const hasMethodStub = name in target; 81 | if (!hasMethodStub) { 82 | if (this.defaultedPropertyNames.indexOf(name.toString()) >= 0) { 83 | return undefined; 84 | } 85 | this.createPropertyStub(name.toString()); 86 | this.createInstancePropertyDescriptorListener(name.toString(), {}, this.clazz.prototype); 87 | } 88 | return target[name]; 89 | }, 90 | }; 91 | } 92 | 93 | public reset(): void { 94 | this.methodStubCollections = {}; 95 | this.methodActions = []; 96 | } 97 | 98 | public resetCalls(): void { 99 | this.methodActions = []; 100 | } 101 | 102 | public getAllMatchingActions(methodName: string, matchers: Array): Array { 103 | const result: MethodAction[] = []; 104 | 105 | this.methodActions.forEach((item: MethodAction) => { 106 | if (item.isApplicable(methodName, matchers)) { 107 | result.push(item); 108 | } 109 | }); 110 | return result; 111 | } 112 | 113 | public getFirstMatchingAction(methodName: string, matchers: Array): MethodAction { 114 | return this.getAllMatchingActions(methodName, matchers)[0]; 115 | } 116 | 117 | public getActionsByName(name: string): MethodAction[] { 118 | return this.methodActions.filter(action => action.methodName === name); 119 | } 120 | 121 | protected processProperties(object: any): void { 122 | ObjectInspector.getObjectPrototypes(object).forEach((obj: any) => { 123 | ObjectInspector.getObjectOwnPropertyNames(obj).forEach((name: string) => { 124 | if (this.excludedPropertyNames.indexOf(name) >= 0) { 125 | return; 126 | } 127 | const descriptor = Object.getOwnPropertyDescriptor(obj, name); 128 | if (descriptor?.get) { 129 | this.createPropertyStub(name); 130 | this.createInstancePropertyDescriptorListener(name, descriptor, obj); 131 | this.createInstanceActionListener(name, obj); 132 | } else if (typeof descriptor?.value === "function") { 133 | this.createMethodStub(name); 134 | this.createInstanceActionListener(name, obj); 135 | } else { 136 | // no need to reassign properties 137 | } 138 | }); 139 | }); 140 | } 141 | 142 | protected createInstancePropertyDescriptorListener(key: string, 143 | descriptor: PropertyDescriptor, 144 | prototype: any): void { 145 | if (this.instance.hasOwnProperty(key)) { 146 | return; 147 | } 148 | 149 | Object.defineProperty(this.instance, key, { 150 | get: this.createActionListener(key), 151 | }); 152 | } 153 | 154 | protected createInstanceActionListener(key: string, prototype: any): void { 155 | if (this.instance.hasOwnProperty(key)) { 156 | return; 157 | } 158 | 159 | this.instance[key] = this.createActionListener(key); 160 | } 161 | 162 | protected createActionListener(key: string): () => any { 163 | return (...args) => { 164 | const action: MethodAction = new MethodAction(key, args); 165 | this.methodActions.push(action); 166 | const methodStub = this.getMethodStub(key, args); 167 | methodStub?.execute(args); 168 | return methodStub?.getValue(); 169 | }; 170 | } 171 | 172 | protected getEmptyMethodStub(key: string, args: any[]): MethodStub { 173 | return new ReturnValueMethodStub(-1, [], null); 174 | } 175 | 176 | private processClassCode(clazz: any): void { 177 | const functionNames = this.mockableFunctionsFinder.find(clazz); 178 | functionNames.forEach((functionName: string) => { 179 | this.createMethodStub(functionName); 180 | this.createInstanceActionListener(functionName, this.clazz.prototype); 181 | }); 182 | } 183 | 184 | private createPropertyStub(key: string): void { 185 | if (this.mock.hasOwnProperty(key)) { 186 | return; 187 | } 188 | 189 | Object.defineProperty(this.mock, key, { 190 | get: this.createMethodToStub(key), 191 | }); 192 | } 193 | 194 | private createMethodStub(key) { 195 | if (this.mock.hasOwnProperty(key)) { 196 | return; 197 | } 198 | 199 | this.mock[key] = this.createMethodToStub(key); 200 | } 201 | 202 | private createMethodToStub(key: string): () => any { 203 | return (...args: Array) => { 204 | if (args.length === 1 && args[0] === "__tsMockitoGetInfo") { 205 | return { 206 | key, 207 | mocker: this, 208 | }; 209 | } 210 | if (!this.methodStubCollections[key]) { 211 | this.methodStubCollections[key] = new MethodStubCollection(); 212 | } 213 | 214 | const matchers: Matcher[] = []; 215 | 216 | for (const arg of args) { 217 | if (!(arg instanceof Matcher)) { 218 | matchers.push(strictEqual(arg)); 219 | } else { 220 | matchers.push(arg); 221 | } 222 | } 223 | 224 | return new MethodToStub(this.methodStubCollections[key], matchers, this, key); 225 | }; 226 | } 227 | 228 | private getMethodStub(key: string, args: any[]): MethodStub | null { 229 | const methodStub: MethodStubCollection = this.methodStubCollections[key]; 230 | if (methodStub && methodStub.hasMatchingInAnyGroup(args)) { 231 | const groupIndex = methodStub.getLastMatchingGroupIndex(args); 232 | return methodStub.getFirstMatchingFromGroupAndRemoveIfNotLast(groupIndex, args); 233 | } else { 234 | return this.getEmptyMethodStub(key, args); 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/Spy.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import {Mocker} from "./Mock"; 3 | import {RealMethod} from "./spy/RealMethod"; 4 | import {CallThroughMethodStub} from "./stub/CallThroughMethodStub"; 5 | import {MethodStub} from "./stub/MethodStub"; 6 | 7 | export class Spy extends Mocker { 8 | private realMethods: { [key: string]: RealMethod }; 9 | 10 | constructor(instance: any) { 11 | super(instance.constructor, instance, true); 12 | 13 | if (_.isObject(instance)) { 14 | this.processProperties(instance); 15 | } 16 | } 17 | 18 | public reset(): void { 19 | _.forEach(this.realMethods, (method, key) => { 20 | if (method.instance) { 21 | Object.defineProperty(this.instance, key, method.descriptor ?? {}); 22 | } else { 23 | delete this.instance[key]; 24 | } 25 | }); 26 | 27 | super.reset(); 28 | } 29 | 30 | protected getEmptyMethodStub(key: string, args: any[]): MethodStub { 31 | const realMethod = this.realMethods[key]; 32 | 33 | if (realMethod) { 34 | const method = realMethod.descriptor?.get || realMethod.descriptor?.value; 35 | return new CallThroughMethodStub(this.instance, method); 36 | } 37 | 38 | return super.getEmptyMethodStub(key, args); 39 | } 40 | 41 | protected createInstancePropertyDescriptorListener(key: string, 42 | descriptor: PropertyDescriptor, 43 | prototype: any): void { 44 | if (!this.realMethods) { 45 | this.realMethods = {}; 46 | } 47 | 48 | if (this.realMethods[key]) { 49 | return; 50 | } 51 | 52 | this.realMethods[key] = new RealMethod(descriptor, prototype === this.instance); 53 | Object.defineProperty(this.instance, key, { 54 | get: this.createActionListener(key), 55 | configurable: true, 56 | }); 57 | } 58 | 59 | protected createInstanceActionListener(key: string, prototype: any): void { 60 | if (!this.realMethods) { 61 | this.realMethods = {}; 62 | } 63 | 64 | if (this.realMethods[key]) { 65 | return; 66 | } 67 | 68 | const descriptor = Object.getOwnPropertyDescriptor(prototype, key); 69 | this.realMethods[key] = new RealMethod(descriptor, prototype === this.instance); 70 | this.instance[key] = this.createActionListener(key); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/capture/ArgCaptor.ts: -------------------------------------------------------------------------------- 1 | import {MethodAction} from "../MethodAction"; 2 | 3 | export class ArgCaptor { 4 | constructor(private actions: MethodAction[]) { 5 | } 6 | 7 | public first(): any { 8 | return this.byCallIndex(0); 9 | } 10 | 11 | public second(): any { 12 | return this.byCallIndex(1); 13 | } 14 | 15 | public third(): any { 16 | return this.byCallIndex(2); 17 | } 18 | 19 | public beforeLast(): any { 20 | return this.byCallIndex(this.actions.length - 2); 21 | } 22 | 23 | public last(): any { 24 | return this.byCallIndex(this.actions.length - 1); 25 | } 26 | 27 | public byCallIndex(index: number): any { 28 | if (this.actions.length > index && index >= 0) { 29 | return this.actions[index].args; 30 | } 31 | throw new Error(`Cannot capture arguments, method has not been called so many times: ${index + 1}`); 32 | } 33 | } 34 | 35 | export interface ArgCaptor1 { 36 | first(): [T]; 37 | second(): [T]; 38 | third(): [T]; 39 | beforeLast(): [T]; 40 | last(): [T]; 41 | byCallIndex(index: number): [T]; 42 | } 43 | 44 | export interface ArgCaptor2 { 45 | first(): [T0, T1]; 46 | second(): [T0, T1]; 47 | third(): [T0, T1]; 48 | beforeLast(): [T0, T1]; 49 | last(): [T0, T1]; 50 | byCallIndex(index: number): [T0, T1]; 51 | } 52 | 53 | export interface ArgCaptor3 { 54 | first(): [T0, T1, T2]; 55 | second(): [T0, T1, T2]; 56 | third(): [T0, T1, T2]; 57 | beforeLast(): [T0, T1, T2]; 58 | last(): [T0, T1, T2]; 59 | byCallIndex(index: number): [T0, T1, T2]; 60 | } 61 | 62 | export interface ArgCaptor4 { 63 | first(): [T0, T1, T2, T3]; 64 | second(): [T0, T1, T2, T3]; 65 | third(): [T0, T1, T2, T3]; 66 | beforeLast(): [T0, T1, T2, T3]; 67 | last(): [T0, T1, T2, T3]; 68 | byCallIndex(index: number): [T0, T1, T2, T3]; 69 | } 70 | 71 | export interface ArgCaptor5 { 72 | first(): [T0, T1, T2, T3, T4]; 73 | second(): [T0, T1, T2, T3, T4]; 74 | third(): [T0, T1, T2, T3, T4]; 75 | beforeLast(): [T0, T1, T2, T3, T4]; 76 | last(): [T0, T1, T2, T3, T4]; 77 | byCallIndex(index: number): [T0, T1, T2, T3, T4]; 78 | } 79 | 80 | export interface ArgCaptor6 { 81 | first(): [T0, T1, T2, T3, T4, T5]; 82 | second(): [T0, T1, T2, T3, T4, T5]; 83 | third(): [T0, T1, T2, T3, T4, T5]; 84 | beforeLast(): [T0, T1, T2, T3, T4, T5]; 85 | last(): [T0, T1, T2, T3, T4, T5]; 86 | byCallIndex(index: number): [T0, T1, T2, T3, T4, T5]; 87 | } 88 | 89 | export interface ArgCaptor7 { 90 | first(): [T0, T1, T2, T3, T4, T5, T6]; 91 | second(): [T0, T1, T2, T3, T4, T5, T6]; 92 | third(): [T0, T1, T2, T3, T4, T5, T6]; 93 | beforeLast(): [T0, T1, T2, T3, T4, T5, T6]; 94 | last(): [T0, T1, T2, T3, T4, T5, T6]; 95 | byCallIndex(index: number): [T0, T1, T2, T3, T4, T5, T6]; 96 | } 97 | 98 | export interface ArgCaptor8 { 99 | first(): [T0, T1, T2, T3, T4, T5, T6, T7]; 100 | second(): [T0, T1, T2, T3, T4, T5, T6, T7]; 101 | third(): [T0, T1, T2, T3, T4, T5, T6, T7]; 102 | beforeLast(): [T0, T1, T2, T3, T4, T5, T6, T7]; 103 | last(): [T0, T1, T2, T3, T4, T5, T6, T7]; 104 | byCallIndex(index: number): [T0, T1, T2, T3, T4, T5, T6, T7]; 105 | } 106 | 107 | export interface ArgCaptor9 { 108 | first(): [T0, T1, T2, T3, T4, T5, T6, T7, T8]; 109 | second(): [T0, T1, T2, T3, T4, T5, T6, T7, T8]; 110 | third(): [T0, T1, T2, T3, T4, T5, T6, T7, T8]; 111 | beforeLast(): [T0, T1, T2, T3, T4, T5, T6, T7, T8]; 112 | last(): [T0, T1, T2, T3, T4, T5, T6, T7, T8]; 113 | byCallIndex(index: number): [T0, T1, T2, T3, T4, T5, T6, T7, T8]; 114 | } 115 | 116 | export interface ArgCaptor10 { 117 | first(): [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9]; 118 | second(): [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9]; 119 | third(): [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9]; 120 | beforeLast(): [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9]; 121 | last(): [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9]; 122 | byCallIndex(index: number): [T0, T1, T2, T3, T4, T5, T6, T7, T8, T9]; 123 | } 124 | -------------------------------------------------------------------------------- /src/matcher/ArgsToMatchersValidator.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "./type/Matcher"; 2 | 3 | export class ArgsToMatchersValidator { 4 | public validate(matchers: Matcher[], args: any[]): boolean { 5 | if (matchers.length !== args.length) { 6 | return false; 7 | } 8 | return matchers.every((matcher: Matcher, index: number) => matcher.match(args[index])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/matcher/type/AnyFunctionMatcher.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import {Matcher} from "./Matcher"; 3 | 4 | export class AnyFunctionMatcher extends Matcher { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | public match(value: any): boolean { 10 | return _.isFunction(value); 11 | } 12 | 13 | public toString(): string { 14 | return "anyFunction()"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/matcher/type/AnyNumberMatcher.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import {Matcher} from "./Matcher"; 3 | 4 | export class AnyNumberMatcher extends Matcher { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | public match(value: any): boolean { 10 | return _.isNumber(value); 11 | } 12 | 13 | public toString(): string { 14 | return "anyNumber()"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/matcher/type/AnyOfClassMatcher.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "./Matcher"; 2 | 3 | export class AnyOfClassMatcher extends Matcher { 4 | constructor(private expectedClass: new (...args: any[]) => T) { 5 | super(); 6 | if (expectedClass === null) { 7 | throw new Error("The expected class cannot be null."); 8 | } 9 | } 10 | 11 | public match(value: any): boolean { 12 | return value instanceof this.expectedClass; 13 | } 14 | 15 | public toString() { 16 | return `anyOfClass(${this.expectedClass["name"]})`; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/matcher/type/AnyStringMatcher.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import {Matcher} from "./Matcher"; 3 | 4 | export class AnyStringMatcher extends Matcher { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | public match(value: any): boolean { 10 | return _.isString(value); 11 | } 12 | 13 | public toString(): string { 14 | return "anyString()"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/matcher/type/AnythingMatcher.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "./Matcher"; 2 | 3 | export class AnythingMatcher extends Matcher { 4 | constructor() { 5 | super(); 6 | } 7 | 8 | public match(value: any): boolean { 9 | return true; 10 | } 11 | 12 | public toString(): string { 13 | return "anything()"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/matcher/type/BetweenMatcher.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "./Matcher"; 2 | 3 | export class BetweenMatcher extends Matcher { 4 | constructor(private min: number, private max: number) { 5 | super(); 6 | 7 | if (min > max) { 8 | throw new Error("between matcher error: min value can\'t be greater than max"); 9 | } 10 | } 11 | 12 | public match(value: any): boolean { 13 | return value >= this.min && value <= this.max; 14 | } 15 | 16 | public toString(): string { 17 | return `between(${this.min}, ${this.max})`; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/matcher/type/DeepEqualMatcher.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import {Matcher} from "./Matcher"; 3 | import * as safeJsonStringify from "safe-json-stringify"; 4 | export class DeepEqualMatcher extends Matcher { 5 | constructor(private expectedValue: T) { 6 | super(); 7 | } 8 | 9 | public match(value: any): boolean { 10 | return _.isEqualWith(this.expectedValue, value, 11 | (expected: any, actual: any) => { 12 | if (expected instanceof Matcher) { 13 | return expected.match(actual); 14 | } 15 | 16 | return undefined; 17 | }); 18 | } 19 | 20 | public toString(): string { 21 | if (this.expectedValue instanceof Array) { 22 | return `deepEqual([${this.expectedValue}])`; 23 | } else { 24 | return `deepEqual(${safeJsonStringify(this.expectedValue as unknown as object)})`; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/matcher/type/Matcher.ts: -------------------------------------------------------------------------------- 1 | export class Matcher { 2 | public match(value: any): boolean { 3 | return false; 4 | } 5 | 6 | public toString(): string { 7 | return ""; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/matcher/type/MatchingStringMatcher.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "./Matcher"; 2 | 3 | export class MatchingStringMatcher extends Matcher { 4 | constructor(private expectedValue: any) { 5 | super(); 6 | } 7 | 8 | public match(value: any): boolean { 9 | return value.match(this.expectedValue); 10 | } 11 | 12 | public toString(): string { 13 | return `match(${this.expectedValue})`; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/matcher/type/NotNullMatcher.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import {Matcher} from "./Matcher"; 3 | 4 | export class NotNullMatcher extends Matcher { 5 | public match(value: any): boolean { 6 | return !_.isNull(value); 7 | } 8 | 9 | public toString(): string { 10 | return "notNull()"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/matcher/type/ObjectContainingMatcher.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import {Matcher} from "./Matcher"; 3 | 4 | export class ObjectContainingMatcher extends Matcher { 5 | constructor(private expectedValue: any) { 6 | super(); 7 | } 8 | 9 | public match(value: Object): boolean { 10 | return _.isMatch(value, this.expectedValue); 11 | } 12 | 13 | public toString(): string { 14 | return `objectContaining(${JSON.stringify(this.expectedValue)})`; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/matcher/type/StrictEqualMatcher.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "./Matcher"; 2 | 3 | export class StrictEqualMatcher extends Matcher { 4 | constructor(private expectedValue: any) { 5 | super(); 6 | } 7 | 8 | public match(value: any): boolean { 9 | return this.expectedValue === value; 10 | } 11 | 12 | public toString(): string { 13 | if (this.expectedValue instanceof Array) { 14 | return `strictEqual([${this.expectedValue}])`; 15 | } else { 16 | return `strictEqual(${this.expectedValue})`; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/spy/RealMethod.ts: -------------------------------------------------------------------------------- 1 | export class RealMethod { 2 | constructor(public descriptor: PropertyDescriptor | undefined, 3 | public instance: boolean) {} 4 | } 5 | -------------------------------------------------------------------------------- /src/stub/AbstractMethodStub.ts: -------------------------------------------------------------------------------- 1 | export abstract class AbstractMethodStub { 2 | protected groupIndex: number; 3 | 4 | public getGroupIndex(): number { 5 | return this.groupIndex; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/stub/CallFunctionMethodStub.ts: -------------------------------------------------------------------------------- 1 | import {ArgsToMatchersValidator} from "../matcher/ArgsToMatchersValidator"; 2 | import {Matcher} from "../matcher/type/Matcher"; 3 | import {AbstractMethodStub} from "./AbstractMethodStub"; 4 | import {MethodStub} from "./MethodStub"; 5 | 6 | export class CallFunctionMethodStub extends AbstractMethodStub implements MethodStub { 7 | private validator: ArgsToMatchersValidator = new ArgsToMatchersValidator(); 8 | private functionResult: any; 9 | 10 | constructor(protected groupIndex: number, private matchers: Array, private func: any) { 11 | super(); 12 | } 13 | 14 | public isApplicable(args: any[]): boolean { 15 | return this.validator.validate(this.matchers, args); 16 | } 17 | 18 | public execute(args: any[]): void { 19 | this.functionResult = this.func(...args); 20 | } 21 | 22 | public getValue(): any { 23 | return this.functionResult; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/stub/CallThroughMethodStub.ts: -------------------------------------------------------------------------------- 1 | import {MethodStub} from "./MethodStub"; 2 | 3 | export class CallThroughMethodStub implements MethodStub { 4 | private result: any; 5 | 6 | constructor(private instance: any, private method: Function) { 7 | } 8 | 9 | public getGroupIndex(): number { 10 | return -1; 11 | } 12 | 13 | public isApplicable(args: any[]): boolean { 14 | return false; 15 | } 16 | 17 | public execute(args: any[]): void { 18 | this.result = this.method.apply(this.instance, args); 19 | } 20 | 21 | public getValue(): any { 22 | return this.result; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/stub/MethodStub.ts: -------------------------------------------------------------------------------- 1 | export interface MethodStub { 2 | isApplicable(args: any[]): boolean; 3 | execute(args: any[]): void; 4 | getValue(): any; 5 | getGroupIndex(): number; 6 | } 7 | -------------------------------------------------------------------------------- /src/stub/RejectPromiseMethodStub.ts: -------------------------------------------------------------------------------- 1 | import {ArgsToMatchersValidator} from "../matcher/ArgsToMatchersValidator"; 2 | import {Matcher} from "../matcher/type/Matcher"; 3 | import {AbstractMethodStub} from "./AbstractMethodStub"; 4 | import {MethodStub} from "./MethodStub"; 5 | 6 | export class RejectPromiseMethodStub extends AbstractMethodStub implements MethodStub { 7 | private validator: ArgsToMatchersValidator = new ArgsToMatchersValidator(); 8 | 9 | constructor(protected groupIndex: number, private matchers: Array, private value: any) { 10 | super(); 11 | } 12 | 13 | public isApplicable(args: any[]): boolean { 14 | return this.validator.validate(this.matchers, args); 15 | } 16 | 17 | public execute(args: any[]): void { 18 | 19 | } 20 | 21 | public getValue(): any { 22 | return Promise.reject(this.value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/stub/ResolvePromiseMethodStub.ts: -------------------------------------------------------------------------------- 1 | import {ArgsToMatchersValidator} from "../matcher/ArgsToMatchersValidator"; 2 | import {Matcher} from "../matcher/type/Matcher"; 3 | import {AbstractMethodStub} from "./AbstractMethodStub"; 4 | import {MethodStub} from "./MethodStub"; 5 | 6 | export class ResolvePromiseMethodStub extends AbstractMethodStub implements MethodStub { 7 | private validator: ArgsToMatchersValidator = new ArgsToMatchersValidator(); 8 | 9 | constructor(protected groupIndex: number, private matchers: Array, private value: any) { 10 | super(); 11 | } 12 | 13 | public isApplicable(args: any[]): boolean { 14 | return this.validator.validate(this.matchers, args); 15 | } 16 | 17 | public execute(args: any[]): void { 18 | 19 | } 20 | 21 | public getValue(): any { 22 | return Promise.resolve(this.value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/stub/ReturnValueMethodStub.ts: -------------------------------------------------------------------------------- 1 | import {ArgsToMatchersValidator} from "../matcher/ArgsToMatchersValidator"; 2 | import {Matcher} from "../matcher/type/Matcher"; 3 | import {AbstractMethodStub} from "./AbstractMethodStub"; 4 | import {MethodStub} from "./MethodStub"; 5 | 6 | export class ReturnValueMethodStub extends AbstractMethodStub implements MethodStub { 7 | private validator: ArgsToMatchersValidator = new ArgsToMatchersValidator(); 8 | 9 | constructor(protected groupIndex: number, private matchers: Array, private returns: any) { 10 | super(); 11 | } 12 | 13 | public isApplicable(args: any[]): boolean { 14 | return this.validator.validate(this.matchers, args); 15 | } 16 | 17 | public execute(args: any[]): void { 18 | 19 | } 20 | 21 | public getValue(): any { 22 | return this.returns; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/stub/ThrowErrorMethodStub.ts: -------------------------------------------------------------------------------- 1 | import {ArgsToMatchersValidator} from "../matcher/ArgsToMatchersValidator"; 2 | import {Matcher} from "../matcher/type/Matcher"; 3 | import {AbstractMethodStub} from "./AbstractMethodStub"; 4 | import {MethodStub} from "./MethodStub"; 5 | 6 | export class ThrowErrorMethodStub extends AbstractMethodStub implements MethodStub { 7 | private validator: ArgsToMatchersValidator = new ArgsToMatchersValidator(); 8 | 9 | constructor(protected groupIndex: number, private matchers: Array, private error: Error) { 10 | super(); 11 | } 12 | 13 | public isApplicable(args: any[]): boolean { 14 | return this.validator.validate(this.matchers, args); 15 | } 16 | 17 | public execute(args: any[]): void { 18 | throw this.error; 19 | } 20 | 21 | public getValue(): any { 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ts-mockito.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgCaptor, 3 | ArgCaptor1, 4 | ArgCaptor10, 5 | ArgCaptor2, 6 | ArgCaptor3, 7 | ArgCaptor4, 8 | ArgCaptor5, 9 | ArgCaptor6, 10 | ArgCaptor7, 11 | ArgCaptor8, 12 | ArgCaptor9, 13 | } from "./capture/ArgCaptor"; 14 | import {AnyFunctionMatcher} from "./matcher/type/AnyFunctionMatcher"; 15 | import {AnyNumberMatcher} from "./matcher/type/AnyNumberMatcher"; 16 | import {AnyOfClassMatcher} from "./matcher/type/AnyOfClassMatcher"; 17 | import {AnyStringMatcher} from "./matcher/type/AnyStringMatcher"; 18 | import {AnythingMatcher} from "./matcher/type/AnythingMatcher"; 19 | import {BetweenMatcher} from "./matcher/type/BetweenMatcher"; 20 | import {DeepEqualMatcher} from "./matcher/type/DeepEqualMatcher"; 21 | import {MatchingStringMatcher} from "./matcher/type/MatchingStringMatcher"; 22 | import {NotNullMatcher} from "./matcher/type/NotNullMatcher"; 23 | import {ObjectContainingMatcher} from "./matcher/type/ObjectContainingMatcher"; 24 | import {StrictEqualMatcher} from "./matcher/type/StrictEqualMatcher"; 25 | import {MethodStubSetter} from "./MethodStubSetter"; 26 | import {MethodStubVerificator} from "./MethodStubVerificator"; 27 | import {MethodToStub} from "./MethodToStub"; 28 | import {Mocker} from "./Mock"; 29 | import {Spy} from "./Spy"; 30 | 31 | export function spy(instanceToSpy: T): T { 32 | return new Spy(instanceToSpy).getMock(); 33 | } 34 | 35 | export function mock(clazz: (new(...args: any[]) => T) | (Function & { prototype: T }) ): T; 36 | export function mock(clazz?: any): T; 37 | export function mock(clazz?: any): T { 38 | return new Mocker(clazz).getMock(); 39 | } 40 | 41 | export function verify(method: T): MethodStubVerificator { 42 | return new MethodStubVerificator(method as any); 43 | } 44 | 45 | export function when(method: Promise): MethodStubSetter, T, Error>; 46 | export function when(method: T): MethodStubSetter; 47 | export function when(method: any): any { 48 | return new MethodStubSetter(method); 49 | } 50 | 51 | export function instance(mockedValue: T): T { 52 | const tsmockitoInstance = (mockedValue as any).__tsmockitoInstance as T; 53 | return tsmockitoInstance; 54 | } 55 | 56 | export function capture(method: (a: T0, b: T1, c: T2, d: T3, e: T4, f: T5, g: T6, h: T7, i: T8, j: T9) => any): ArgCaptor10; 57 | export function capture(method: (a: T0, b: T1, c: T2, d: T3, e: T4, f: T5, g: T6, h: T7, i: T8) => any): ArgCaptor9; 58 | export function capture(method: (a: T0, b: T1, c: T2, d: T3, e: T4, f: T5, g: T6, h: T7) => any): ArgCaptor8; 59 | export function capture(method: (a: T0, b: T1, c: T2, d: T3, e: T4, f: T5, g: T6) => any): ArgCaptor7; 60 | export function capture(method: (a: T0, b: T1, c: T2, d: T3, e: T4, f: T5) => any): ArgCaptor6; 61 | export function capture(method: (a: T0, b: T1, c: T2, d: T3, e: T4) => any): ArgCaptor5; 62 | export function capture(method: (a: T0, b: T1, c: T2, d: T3) => any): ArgCaptor4; 63 | export function capture(method: (a: T0, b: T1, c: T2) => any): ArgCaptor3; 64 | export function capture(method: (a: T0, b: T1) => any): ArgCaptor2; 65 | export function capture(method: (a: T0) => any): ArgCaptor1; 66 | export function capture(method: (...args: any[]) => any): ArgCaptor { 67 | const methodStub: MethodToStub = method(); 68 | if (methodStub instanceof MethodToStub) { 69 | const actions = methodStub.mocker.getActionsByName(methodStub.name); 70 | return new ArgCaptor(actions); 71 | } else { 72 | throw Error("Cannot capture from not mocked object."); 73 | } 74 | } 75 | 76 | export function reset(...mockedValues: T[]): void { 77 | mockedValues.forEach(mockedValue => (mockedValue as any).__tsmockitoMocker.reset()); 78 | } 79 | 80 | export function resetCalls(...mockedValues: T[]): void { 81 | mockedValues.forEach(mockedValue => (mockedValue as any).__tsmockitoMocker.resetCalls()); 82 | } 83 | 84 | export function anyOfClass(expectedClass: new (...args: any[]) => T): any { 85 | return new AnyOfClassMatcher(expectedClass) as any; 86 | } 87 | 88 | export function anyFunction(): any { 89 | return new AnyFunctionMatcher() as any; 90 | } 91 | 92 | export function anyNumber(): any { 93 | return new AnyNumberMatcher() as any; 94 | } 95 | 96 | export function anyString(): any { 97 | return new AnyStringMatcher() as any; 98 | } 99 | 100 | export function anything(): any { 101 | return new AnythingMatcher() as any; 102 | } 103 | 104 | export function between(min: number, max: number): any { 105 | return new BetweenMatcher(min, max) as any; 106 | } 107 | 108 | export function deepEqual(expectedValue: T): T { 109 | return new DeepEqualMatcher(expectedValue) as any; 110 | } 111 | 112 | export function notNull(): any { 113 | return new NotNullMatcher() as any; 114 | } 115 | 116 | export function strictEqual(expectedValue: any): any { 117 | return new StrictEqualMatcher(expectedValue) as any; 118 | } 119 | 120 | export function match(expectedValue: RegExp | string): any { 121 | return new MatchingStringMatcher(expectedValue) as any; 122 | } 123 | 124 | export function objectContaining(expectedValue: T): any { 125 | return new ObjectContainingMatcher(expectedValue) as any; 126 | } 127 | 128 | // Export default object with all members (ember-browserify doesn't support named exports). 129 | export default { 130 | spy, 131 | mock, 132 | verify, 133 | when, 134 | instance, 135 | capture, 136 | reset, 137 | resetCalls, 138 | anyOfClass, 139 | anyFunction, 140 | anyNumber, 141 | anyString, 142 | anything, 143 | between, 144 | deepEqual, 145 | notNull, 146 | strictEqual, 147 | match, 148 | objectContaining, 149 | }; 150 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "../lib", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "target": "es5", 7 | "removeComments": true, 8 | "sourceMap": true, 9 | "strictNullChecks": true, 10 | "declaration": true, 11 | "lib": [ 12 | "es5", 13 | "es6", 14 | "dom" 15 | ], 16 | "types": [ 17 | "node", 18 | "lodash", 19 | "jasmine" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/MethodCallToStringConverter.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "../matcher/type/Matcher"; 2 | import {MethodAction} from "../MethodAction"; 3 | import {MethodToStub} from "../MethodToStub"; 4 | import * as safeJsonStringify from "safe-json-stringify"; 5 | 6 | export class MethodCallToStringConverter { 7 | public convert(method: MethodToStub): string { 8 | const stringifiedMatchers = method.matchers.map((matcher: Matcher) => matcher.toString()).join(", "); 9 | return `${method.name}(${stringifiedMatchers})" `; 10 | } 11 | 12 | public convertActualCalls(calls: MethodAction[]): string[] { 13 | return calls.map(call => { 14 | const methodName = call.methodName; 15 | const args = call.args.map(arg => this.objectIsStringable(arg) ? arg.toString() : safeJsonStringify(arg)); 16 | return `${methodName}(${args.join(', ')})`; 17 | }); 18 | } 19 | 20 | private objectIsStringable(arg) { 21 | return typeof arg !== 'object' || arg.hasOwnProperty('toString'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/MockableFunctionsFinder.ts: -------------------------------------------------------------------------------- 1 | import {parse} from "@babel/parser"; 2 | import * as _babel_types from "@babel/types"; 3 | import {ObjectInspector} from "./ObjectInspector"; 4 | import {ObjectPropertyCodeRetriever} from "./ObjectPropertyCodeRetriever"; 5 | import {uniq} from "lodash"; 6 | import { 7 | AssignmentExpression, 8 | Block, 9 | Class, 10 | ClassExpression, 11 | Expression, 12 | Identifier, 13 | LVal, 14 | NumericLiteral, ObjectExpression, 15 | PrivateName, Program, 16 | SpreadElement, 17 | Statement, 18 | StringLiteral, 19 | UnaryLike, 20 | } from "@babel/types"; 21 | 22 | const methodTokenName = new Set([ 23 | "ObjectMethod", 24 | "ClassMethod", 25 | "ClassPrivateMethod", 26 | "FunctionDeclaration", 27 | "FunctionExpression" 28 | ]); 29 | 30 | function getPropName(n: Identifier | StringLiteral | NumericLiteral | _babel_types.Expression | PrivateName): string[] { 31 | if ('name' in n) return [n.name]; 32 | if ('value' in n) return [n.value.toString()]; 33 | if (n.type === 'PrivateName') return getPropName(n.id); 34 | return handleExpression(n); 35 | } 36 | 37 | function handleAssignment(n: AssignmentExpression): string[] { 38 | if (methodTokenName.has(n.right.type)) 39 | return [...handleLVal(n.left), ...handleExpression(n.right)]; 40 | return handleExpression(n.right); 41 | } 42 | 43 | function handleObject(n: ObjectExpression): string[] { 44 | const names = [] as string[]; 45 | n.properties.forEach(p => { 46 | if ('key' in p) names.push(...getPropName(p.key)); 47 | if ('body' in p) names.push(...p.body.body.flatMap(handleStatement)); 48 | if ('value' in p) names.push(...handleExpression(p.value as Expression)); 49 | }); 50 | return names; 51 | } 52 | 53 | const isSpread = (n: { type:string }): n is SpreadElement => n.type === 'SpreadElement'; 54 | 55 | function handleExpression(n?: Expression | null): string[] { 56 | if (!n) return []; 57 | switch (n.type) { 58 | case "ArrayExpression": 59 | return n.elements.flatMap(e => e ? (isSpread(e) ? handleUnaryLike(e) : handleExpression(e)) : []); 60 | case "AssignmentExpression": 61 | return handleAssignment(n); 62 | case "BinaryExpression": 63 | return [...(n.left.type !== 'PrivateName' ? handleExpression(n.left) : []), ...handleExpression(n.right)]; 64 | case "CallExpression": 65 | return n.arguments.flatMap(a => { 66 | if (a.type === 'JSXNamespacedName') return []; 67 | if (a.type === 'ArgumentPlaceholder') return []; 68 | return isSpread(a) ? handleUnaryLike(a) : handleExpression(a); 69 | }); 70 | case "ConditionalExpression": 71 | return [...handleExpression(n.test), ...handleExpression(n.consequent), ...handleExpression(n.alternate)]; 72 | case "FunctionExpression": 73 | return handleBlock(n.body); 74 | case "Identifier": 75 | return [n.name]; 76 | case "LogicalExpression": 77 | return [...handleExpression(n.left), ...handleExpression(n.right)]; 78 | case "MemberExpression": 79 | return n.property.type === 'PrivateName' ? handleExpression(n.property.id) : handleExpression(n.property); 80 | case "ObjectExpression": 81 | return handleObject(n); 82 | case "SequenceExpression": 83 | return n.expressions.flatMap(handleExpression); 84 | case "ParenthesizedExpression": 85 | return handleExpression(n.expression); 86 | case "UnaryExpression": 87 | case "UpdateExpression": 88 | case "YieldExpression": 89 | case "AwaitExpression": 90 | return handleExpression(n.argument); 91 | case "ArrowFunctionExpression": 92 | return n.body.type === 'BlockStatement' ? handleBlock(n.body) : handleExpression(n.body); 93 | case "ClassExpression": 94 | return handleClass(n); 95 | case "TaggedTemplateExpression": 96 | return handleExpression(n.tag); 97 | case "TemplateLiteral": 98 | return n.expressions.flatMap(e => handleExpression(e as Expression)); 99 | case "OptionalMemberExpression": 100 | return [...(n.property.type !== 'Identifier' ? handleExpression(n.property) : [])]; 101 | case "OptionalCallExpression": 102 | return n.arguments.flatMap(a => { 103 | if (a.type === 'SpreadElement') return handleExpression(a.argument); 104 | if (a.type === 'JSXNamespacedName') return []; 105 | if (a.type === 'ArgumentPlaceholder') return []; 106 | return handleExpression(a); 107 | }); 108 | case "BindExpression": 109 | return handleExpression(n.object); 110 | case "DoExpression": 111 | return handleBlock(n.body); 112 | case "RecordExpression": 113 | case "NewExpression": 114 | case "StringLiteral": 115 | case "NumericLiteral": 116 | case "BooleanLiteral": 117 | case "RegExpLiteral": 118 | case "NullLiteral": 119 | case "ThisExpression": 120 | case "MetaProperty": 121 | case "Super": 122 | case "Import": 123 | case "BigIntLiteral": 124 | case "TypeCastExpression": 125 | case "JSXElement": 126 | case "JSXFragment": 127 | case "PipelinePrimaryTopicReference": 128 | case "TupleExpression": 129 | case "DecimalLiteral": 130 | case "ModuleExpression": 131 | case "TSAsExpression": 132 | case "TSTypeAssertion": 133 | case "TSNonNullExpression": 134 | default: 135 | return []; 136 | } 137 | } 138 | 139 | function handleBlock(n?: Block | null): string[] { 140 | if (!n) return []; 141 | return n.body.flatMap(handleStatement); 142 | } 143 | 144 | function handleStatement(n: Statement): string[] { 145 | switch (n.type) { 146 | case "BlockStatement": 147 | return n.body.flatMap(handleStatement); 148 | case "DoWhileStatement": 149 | return [...handleExpression(n.test), ...handleStatement(n.body)]; 150 | case "ExpressionStatement": 151 | return handleExpression(n.expression); 152 | case "ForInStatement": 153 | return [...handleExpression(n.right), ...handleStatement(n.body)]; 154 | case "ForStatement": 155 | return [...(n.test ? handleExpression(n.test) : []), ...(n.update ? handleExpression(n.update) : []), ...handleStatement(n.body)]; 156 | case "FunctionDeclaration": 157 | return [...(n.id ? [n.id.name] : []), ...handleBlock(n.body)]; 158 | case "IfStatement": 159 | return [...handleExpression(n.test), ...handleStatement(n.consequent), ...(n.alternate ? handleStatement(n.alternate) : [])]; 160 | case "LabeledStatement": 161 | return handleStatement(n.body); 162 | case "ReturnStatement": 163 | return [...(n.argument ? handleExpression(n.argument) : [])]; 164 | case "SwitchStatement": 165 | return [...handleExpression(n.discriminant), ...n.cases.flatMap(c => [...handleExpression(c.test), ...c.consequent.flatMap(handleStatement)])]; 166 | case "ThrowStatement": 167 | return handleExpression(n.argument); 168 | case "TryStatement": 169 | return [...handleBlock(n.block), ...handleBlock(n.handler?.body), ...handleBlock(n.finalizer)]; 170 | case "WhileStatement": 171 | return [...handleExpression(n.test), ...handleStatement(n.body)]; 172 | case "WithStatement": 173 | return [...handleExpression(n.object), ...handleStatement(n.body)]; 174 | case "ClassDeclaration": 175 | return handleClass(n); 176 | case "ForOfStatement": 177 | return [...handleExpression(n.right), ...handleStatement(n.body)]; 178 | case "VariableDeclaration": 179 | return n.declarations.flatMap(d => handleExpression(d.init)); 180 | case "DebuggerStatement": 181 | case "BreakStatement": 182 | case "ContinueStatement": 183 | case "EmptyStatement": 184 | case "ExportAllDeclaration": 185 | case "ExportDefaultDeclaration": 186 | case "ExportNamedDeclaration": 187 | case "ImportDeclaration": 188 | case "DeclareClass": 189 | case "DeclareFunction": 190 | case "DeclareInterface": 191 | case "DeclareModule": 192 | case "DeclareModuleExports": 193 | case "DeclareTypeAlias": 194 | case "DeclareOpaqueType": 195 | case "DeclareVariable": 196 | case "DeclareExportDeclaration": 197 | case "DeclareExportAllDeclaration": 198 | case "InterfaceDeclaration": 199 | case "OpaqueType": 200 | case "TypeAlias": 201 | case "EnumDeclaration": 202 | case "TSDeclareFunction": 203 | case "TSInterfaceDeclaration": 204 | case "TSTypeAliasDeclaration": 205 | case "TSEnumDeclaration": 206 | case "TSModuleDeclaration": 207 | case "TSImportEqualsDeclaration": 208 | case "TSExportAssignment": 209 | case "TSNamespaceExportDeclaration": 210 | default: 211 | return []; 212 | } 213 | 214 | } 215 | 216 | function handleLVal(n: LVal): string[] { 217 | if ('name' in n) return [n.name]; 218 | if ('property' in n) return getPropName(n.property); 219 | return []; 220 | } 221 | 222 | function handleUnaryLike(n: UnaryLike): string[] { 223 | return handleExpression(n.argument); 224 | } 225 | 226 | function handleClass(n: Class | ClassExpression): string[] { 227 | return n.body.body.flatMap(b => { 228 | switch (b.type) { 229 | case "ClassMethod": 230 | return [...getPropName(b.key), ...handleStatement(b.body)]; 231 | case "ClassPrivateMethod": 232 | return [...getPropName(b.key), ...handleStatement(b.body)]; 233 | case "ClassProperty": 234 | return [...getPropName(b.key), ...handleExpression(b.value)]; 235 | case "ClassPrivateProperty": 236 | return [...getPropName(b.key), ...handleExpression(b.value)]; 237 | case "TSIndexSignature": 238 | case "TSDeclareMethod": 239 | return []; 240 | } 241 | }); 242 | } 243 | 244 | function handleBody(n: Program) { 245 | return n.body.flatMap(handleStatement); 246 | } 247 | 248 | /** 249 | * Looking for all function calls and declarations and provides an array of their names. The mechanism is greedy 250 | * and tries to match as many function names as it can find and not only those of inspecting class. 251 | * 252 | * Matching occurrences are: 253 | * - [.]functionName( 254 | * - [.]functionName = ( 255 | * - [.]functionName = function( 256 | * - [.]functionName = function otherName( 257 | */ 258 | export class MockableFunctionsFinder { 259 | private excludedFunctionNames = new Set(["hasOwnProperty", "function"]); 260 | 261 | public find(clazz: any): string[] { 262 | const codes = this.getClassCodeAsStringWithInheritance(clazz); 263 | const asts = codes.map(code => parse(code)); 264 | const names = asts.flatMap(ast => handleBody(ast.program)); 265 | return uniq(names) 266 | .filter((functionName: string) => this.isMockable(functionName)); 267 | } 268 | 269 | private isMockable(name: string | null | undefined): boolean { 270 | if (!name) return false; 271 | return !this.excludedFunctionNames.has(name); 272 | } 273 | 274 | private getClassCodeAsStringWithInheritance(clazz: any) { 275 | return [`const clazz = ${this.getClassAsString(clazz)}`, ...ObjectInspector.getObjectPrototypes(clazz.prototype).map(ObjectPropertyCodeRetriever.getObject)]; 276 | } 277 | 278 | private getClassAsString(clazz: any) { 279 | const classCode: string = typeof clazz.toString !== "undefined" ? clazz.toString() : "{}"; 280 | if(classCode === '[object Object]') return '{}'; 281 | return classCode; 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/utils/ObjectInspector.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | 3 | 4 | export class ObjectInspector { 5 | public static getObjectPrototypes(prototype: any): any[] { 6 | const prototypes: any[] = []; 7 | while (_.isObject(prototype) && (prototype !== Object.prototype && prototype !== Function.prototype)) { 8 | prototypes.push(prototype); 9 | prototype = Object.getPrototypeOf(prototype); 10 | } 11 | return prototypes; 12 | } 13 | 14 | public static getObjectOwnPropertyNames(object: any): string[] { 15 | return _.isObject(object) ? Object.getOwnPropertyNames(object) : []; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/ObjectPropertyCodeRetriever.ts: -------------------------------------------------------------------------------- 1 | export class ObjectPropertyCodeRetriever { 2 | public static getObject(object: any) { 3 | if (object.constructor.name === 'Object') return ''; 4 | const props = Object.getOwnPropertyNames(object); 5 | return ` 6 | const ${object.constructor.name} = { 7 | ${props.flatMap(prop => { 8 | const descriptor = Object.getOwnPropertyDescriptor(object, prop); 9 | if (descriptor?.get || descriptor?.set) { 10 | return [ 11 | descriptor?.get ? descriptor?.get.toString() : '', 12 | descriptor?.set ? descriptor?.set.toString() : '', 13 | ]; 14 | } else if (typeof object[prop] === 'function') { 15 | let fnStr = String(object[prop]); 16 | fnStr = fnStr.replace(/\[native code]/, ''); 17 | const gx = new RegExp(`^(async)?\\s{0,}\\*?${prop}`); 18 | const isMethod = gx.test(fnStr); 19 | return ` 20 | ${isMethod ? fnStr : `"${prop}": ${fnStr}`} 21 | `; 22 | } 23 | return ''; 24 | }).filter(Boolean).join(',\n')} 25 | } 26 | `; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type TODO = any; -------------------------------------------------------------------------------- /test/MethodAction.spec.ts: -------------------------------------------------------------------------------- 1 | import {MethodAction} from "../src/MethodAction"; 2 | import {strictEqual} from "../src/ts-mockito"; 3 | 4 | describe("MethodAction", () => { 5 | describe("checking if method with matchers is applicable", () => { 6 | describe("when all matchers match", () => { 7 | it("returns true", () => { 8 | // given 9 | const methodName = "sampleMethodName"; 10 | const firstArg = 5; 11 | const secondArg = "sampleString"; 12 | const testObj: MethodAction = new MethodAction(methodName, [firstArg, secondArg]); 13 | 14 | // when 15 | const result = testObj.isApplicable(methodName, [strictEqual(firstArg), strictEqual(secondArg)]); 16 | 17 | // then 18 | expect(result).toBeTruthy(); 19 | }); 20 | }); 21 | describe("when one matcher doesn`t match", () => { 22 | it("returns false", () => { 23 | // given 24 | const methodName = "sampleMethodName"; 25 | const firstArg = 5; 26 | const secondArg = "sampleString"; 27 | const notMatchingArg = "notMatchingArg"; 28 | const testObj: MethodAction = new MethodAction(methodName, [firstArg, notMatchingArg]); 29 | 30 | // when 31 | const result = testObj.isApplicable(methodName, [strictEqual(firstArg), strictEqual(secondArg)]); 32 | 33 | // then 34 | expect(result).toBeFalsy(); 35 | }); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/MethodStub.spec.ts: -------------------------------------------------------------------------------- 1 | import {ReturnValueMethodStub} from "../src/stub/ReturnValueMethodStub"; 2 | import {strictEqual} from "../src/ts-mockito"; 3 | 4 | describe("ReturnValueMethodStub", () => { 5 | describe("checking if given arg is applicable", () => { 6 | it("returns true when arg match", () => { 7 | // given 8 | const testObj: ReturnValueMethodStub = new ReturnValueMethodStub(0, [strictEqual(10)], 50); 9 | 10 | // when 11 | const result = testObj.isApplicable([10]); 12 | 13 | // then 14 | expect(result).toBeTruthy(); 15 | }); 16 | 17 | it("returns false when arg doesn't match", () => { 18 | // given 19 | const testObj: ReturnValueMethodStub = new ReturnValueMethodStub(0, [strictEqual(10)], 50); 20 | 21 | // when 22 | const result = testObj.isApplicable([999]); 23 | 24 | // then 25 | expect(result).toBeFalsy(); 26 | }); 27 | }); 28 | 29 | describe("checking if more than one arg is applicable", () => { 30 | it("returns true when all matches", () => { 31 | // given 32 | const firstValue = 10; 33 | const secondValue = 20; 34 | const testObj: ReturnValueMethodStub = new ReturnValueMethodStub(0, [strictEqual(firstValue), strictEqual(secondValue)], 50); 35 | 36 | // when 37 | const result = testObj.isApplicable([firstValue, secondValue]); 38 | 39 | // then 40 | expect(result).toBeTruthy(); 41 | }); 42 | 43 | it("returns false when first arg doesn't match", () => { 44 | // given 45 | const firstValue = 10; 46 | const secondValue = 20; 47 | const testObj: ReturnValueMethodStub = new ReturnValueMethodStub(0, [strictEqual(firstValue), strictEqual(secondValue)], 50); 48 | 49 | // when 50 | const result = testObj.isApplicable([30, secondValue]); 51 | 52 | // then 53 | expect(result).toBeFalsy(); 54 | }); 55 | 56 | it("returns false when second arg doesn't match", () => { 57 | // given 58 | const firstValue = 10; 59 | const secondValue = 20; 60 | const testObj: ReturnValueMethodStub = new ReturnValueMethodStub(0, [strictEqual(firstValue), strictEqual(secondValue)], 50); 61 | 62 | // when 63 | const result = testObj.isApplicable([firstValue, 30]); 64 | 65 | // then 66 | expect(result).toBeFalsy(); 67 | }); 68 | 69 | it("returns false when both args doesn't match", () => { 70 | // given 71 | const firstValue = 10; 72 | const secondValue = 20; 73 | const testObj: ReturnValueMethodStub = new ReturnValueMethodStub(0, [strictEqual(firstValue), strictEqual(secondValue)], 50); 74 | 75 | // when 76 | const result = testObj.isApplicable([30, 40]); 77 | 78 | // then 79 | expect(result).toBeFalsy(); 80 | }); 81 | }); 82 | 83 | describe("getting mocked value", () => { 84 | it("returns it", () => { 85 | // given 86 | const mockedValue = 50; 87 | const testObj: ReturnValueMethodStub = new ReturnValueMethodStub(0, [], mockedValue); 88 | 89 | // when 90 | const result = testObj.getValue(); 91 | 92 | // then 93 | expect(result).toEqual(mockedValue); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/MethodStubCollection.spec.ts: -------------------------------------------------------------------------------- 1 | import {MethodStubCollection} from "../src/MethodStubCollection"; 2 | 3 | describe("MethodStubCollection", () => { 4 | describe("empty stub collection", () => { 5 | it("returns -1 if doesn't find method stub", () => { 6 | // given 7 | const methodStubCollection = new MethodStubCollection(); 8 | 9 | // when 10 | const index = methodStubCollection.getLastMatchingGroupIndex([]); 11 | 12 | // then 13 | expect(index).toBe(-1); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/capturing.spec.ts: -------------------------------------------------------------------------------- 1 | import {capture, instance, mock} from "../src/ts-mockito"; 2 | import {Foo} from "./utils/Foo"; 3 | 4 | describe("capturing method arguments", () => { 5 | let mockedFoo: Foo; 6 | let foo: Foo; 7 | 8 | beforeEach(() => { 9 | mockedFoo = mock(Foo); 10 | foo = instance(mockedFoo); 11 | }); 12 | 13 | describe("when method has NOT been called", () => { 14 | it("should NOT try to read call with negative index", () => { 15 | // given 16 | let error; 17 | 18 | // then 19 | try { 20 | capture(mockedFoo.concatStringWithNumber).last(); 21 | } catch (e) { 22 | error = e; 23 | } 24 | expect(error.message).toEqual('Cannot capture arguments, method has not been called so many times: 0'); 25 | }); 26 | }); 27 | 28 | describe("when method has been called", () => { 29 | it("captures all arguments passed to it", () => { 30 | // given 31 | 32 | // when 33 | foo.concatStringWithNumber("first", 1); 34 | foo.concatStringWithNumber("second", 2); 35 | foo.concatStringWithNumber("third", 3); 36 | 37 | // then 38 | const [firstCapturedValue, secondCapturedValue] = capture(mockedFoo.concatStringWithNumber).first(); 39 | expect(firstCapturedValue).toEqual("first"); 40 | expect(secondCapturedValue).toEqual(1); 41 | expect(capture(mockedFoo.concatStringWithNumber).first()).toEqual(["first", 1]); 42 | expect(capture(mockedFoo.concatStringWithNumber).second()).toEqual(["second", 2]); 43 | expect(capture(mockedFoo.concatStringWithNumber).third()).toEqual(["third", 3]); 44 | expect(capture(mockedFoo.concatStringWithNumber).beforeLast()).toEqual(["second", 2]); 45 | expect(capture(mockedFoo.concatStringWithNumber).last()).toEqual(["third", 3]); 46 | expect(capture(mockedFoo.concatStringWithNumber).byCallIndex(0)).toEqual(["first", 1]); 47 | expect(capture(mockedFoo.concatStringWithNumber).byCallIndex(1)).toEqual(["second", 2]); 48 | expect(capture(mockedFoo.concatStringWithNumber).byCallIndex(2)).toEqual(["third", 3]); 49 | }); 50 | }); 51 | 52 | describe("when method has been called twice", () => { 53 | describe("but we want check third call arguments", () => { 54 | it("throws error", () => { 55 | // given 56 | foo.concatStringWithNumber("first", 1); 57 | foo.concatStringWithNumber("second", 2); 58 | 59 | // when 60 | let error; 61 | try { 62 | capture(mockedFoo.concatStringWithNumber).third(); 63 | } catch (e) { 64 | error = e; 65 | } 66 | 67 | // then 68 | expect(error.message).toContain("Cannot capture arguments"); 69 | }); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/defaultExport.spec.ts: -------------------------------------------------------------------------------- 1 | import mockitoDefault from "../src/ts-mockito"; 2 | import {spy} from "../src/ts-mockito"; 3 | import * as asteriskStyleImport from "../src/ts-mockito"; 4 | 5 | describe("default export", () => { 6 | it("is an object", () => { 7 | expect(mockitoDefault).toBeDefined(); 8 | expect(typeof mockitoDefault === "object").toBeTruthy(); 9 | expect(mockitoDefault).toBe(asteriskStyleImport.default); 10 | }); 11 | 12 | it("contains proper member functions", () => { 13 | expect(typeof mockitoDefault.spy === "function").toBeTruthy(); 14 | expect(mockitoDefault.spy).toBe(spy); 15 | }); 16 | 17 | it("contains each module member function", () => { 18 | // Asterisk style import contains all member function + the default, so default export should have one 19 | // member less. 20 | expect(Object.keys(mockitoDefault).length).toBe(Object.keys(asteriskStyleImport).length - 1); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/instance.spec.ts: -------------------------------------------------------------------------------- 1 | import {instance, mock} from "../src/ts-mockito"; 2 | import {Foo} from "./utils/Foo"; 3 | 4 | describe("instance", () => { 5 | describe("getting instance of mock", () => { 6 | let mockedFoo: Foo; 7 | 8 | it("returns always same instance", () => { 9 | // given 10 | mockedFoo = mock(Foo); 11 | 12 | // when 13 | const firstFooInstance = instance(mockedFoo); 14 | const secondFooInstance = instance(mockedFoo); 15 | 16 | // then 17 | expect(firstFooInstance).toBe(secondFooInstance); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/interface.spec.ts: -------------------------------------------------------------------------------- 1 | import { instance, mock, when } from "../src/ts-mockito"; 2 | import { FooInterface } from "./utils/FooInterface"; 3 | import { ThenableInterface } from "./utils/Thenable"; 4 | 5 | describe("Interface", () => { 6 | if (typeof Proxy === "undefined") { 7 | pending("Testing browser doesn't support Proxy."); 8 | } 9 | 10 | describe("Basic", () => { 11 | let mockedFoo: FooInterface; 12 | let foo: FooInterface; 13 | 14 | beforeEach(() => { 15 | mockedFoo = mock(); 16 | foo = instance(mockedFoo); 17 | }); 18 | 19 | it("should mock interface function", () => { 20 | // Rest cases for interfaces are tested in verification.spec.ts 21 | 22 | // when 23 | when(mockedFoo.sumByInterface(2, 3)).thenReturn(1000); 24 | 25 | // then 26 | expect(foo.sumByInterface(2, 3)).toEqual(1000); 27 | }); 28 | }); 29 | describe("Promise", () => { 30 | let mockedThen: ThenableInterface; 31 | let inst: ThenableInterface; 32 | 33 | beforeEach(() => { 34 | mockedThen = mock(); 35 | inst = instance(mockedThen); 36 | }); 37 | 38 | it("does not create then descriptor for interface", () => { 39 | // given 40 | 41 | // when 42 | 43 | // then 44 | expect(inst.then).toBeUndefined(); 45 | }); 46 | it("creates then descriptor for interface", () => { 47 | // given 48 | 49 | // when 50 | when(mockedThen.then()).thenReturn("woof"); 51 | 52 | // then 53 | expect(inst.then).toBeDefined(); 54 | }); 55 | it("does not create catch descriptor for interface", () => { 56 | // given 57 | 58 | // when 59 | 60 | // then 61 | expect(inst.catch).toBeUndefined(); 62 | }); 63 | it("creates catch descriptor for interface", () => { 64 | // given 65 | 66 | // when 67 | when(mockedThen.catch()).thenReturn("woof"); 68 | 69 | // then 70 | expect(inst.catch).toBeDefined(); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/matcher/type/AnyFunctionMatcher.spec.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "../../../src/matcher/type/Matcher"; 2 | import {anyFunction} from "../../../src/ts-mockito"; 3 | 4 | describe("AnyFunctionMatcher", () => { 5 | 6 | describe("checking if function is function", () => { 7 | it("returns true", () => { 8 | // given 9 | const testObj: Matcher = anyFunction(); 10 | 11 | // when 12 | const result = testObj.match(() => "arbitrary return value"); 13 | 14 | // then 15 | expect(result).toBeTruthy(); 16 | }); 17 | }); 18 | 19 | describe("checking if string is function", () => { 20 | it("returns false", () => { 21 | // given 22 | const testObj: Matcher = anyFunction(); 23 | 24 | // when 25 | const result = testObj.match("some string"); 26 | 27 | // then 28 | expect(result).toBeFalsy(); 29 | }); 30 | }); 31 | 32 | describe("checking if number is function", () => { 33 | it("returns false", () => { 34 | // given 35 | const testObj: Matcher = anyFunction(); 36 | 37 | // when 38 | const result = testObj.match(5); 39 | 40 | // then 41 | expect(result).toBeFalsy(); 42 | }); 43 | }); 44 | 45 | describe("checking if object is function", () => { 46 | it("returns false", () => { 47 | // given 48 | const testObj: Matcher = anyFunction(); 49 | 50 | // when 51 | const result = testObj.match({prop1: "prop1Value"}); 52 | 53 | // then 54 | expect(result).toBeFalsy(); 55 | }); 56 | }); 57 | 58 | describe("checking if toString works", () => { 59 | it("returns 'anyFunction()'", () => { 60 | // given 61 | const testObj: Matcher = anyFunction(); 62 | 63 | // when 64 | const result = testObj.toString(); 65 | 66 | // then 67 | expect(result).toEqual("anyFunction()"); 68 | }); 69 | }); 70 | 71 | }); 72 | -------------------------------------------------------------------------------- /test/matcher/type/AnyNumberMatcher.spec.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "../../../src/matcher/type/Matcher"; 2 | import {anyNumber} from "../../../src/ts-mockito"; 3 | 4 | describe("AnyNumberMatcher", () => { 5 | describe("checking if positive number is matching", () => { 6 | it("returns true", () => { 7 | // given 8 | const testObj: Matcher = anyNumber(); 9 | 10 | // when 11 | const result = testObj.match(3); 12 | 13 | // then 14 | expect(result).toBeTruthy(); 15 | }); 16 | }); 17 | 18 | describe("checking if negative number is matching", () => { 19 | it("returns true", () => { 20 | // given 21 | const testObj: Matcher = anyNumber(); 22 | 23 | // when 24 | const result = testObj.match(-3); 25 | 26 | // then 27 | expect(result).toBeTruthy(); 28 | }); 29 | }); 30 | 31 | describe("checking if zero is matching", () => { 32 | it("returns true", () => { 33 | // given 34 | const testObj: Matcher = anyNumber(); 35 | 36 | // when 37 | const result = testObj.match(0); 38 | 39 | // then 40 | expect(result).toBeTruthy(); 41 | }); 42 | }); 43 | 44 | describe("checking if string representation of number is matching", () => { 45 | it("returns false", () => { 46 | // given 47 | const testObj: Matcher = anyNumber(); 48 | 49 | // when 50 | const result = testObj.match("5"); 51 | 52 | // then 53 | expect(result).toBeFalsy(); 54 | }); 55 | }); 56 | 57 | describe("checking if object is matching", () => { 58 | it("returns false", () => { 59 | // given 60 | const testObj: Matcher = anyNumber(); 61 | 62 | // when 63 | const result = testObj.match({}); 64 | 65 | // then 66 | expect(result).toBeFalsy(); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/matcher/type/AnyOfClassMatcher.spec.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "../../../src/matcher/type/Matcher"; 2 | import {anyOfClass} from "../../../src/ts-mockito"; 3 | 4 | describe("AnyOfClassMatcher", () => { 5 | 6 | class Car { 7 | constructor() { 8 | } 9 | } 10 | 11 | describe("checking if class matches", () => { 12 | it("returns true", () => { 13 | // given 14 | const testObj: Matcher = anyOfClass(Car); 15 | const value = new Car(); 16 | 17 | // when 18 | const result = testObj.match(value); 19 | 20 | // then 21 | expect(result).toBeTruthy(); 22 | }); 23 | }); 24 | 25 | describe("checking if null matches", () => { 26 | it("returns false", () => { 27 | // given 28 | const testObj: Matcher = anyOfClass(Car); 29 | const value = null; 30 | 31 | // when 32 | const result = testObj.match(value); 33 | 34 | // then 35 | expect(result).toBeFalsy(); 36 | }); 37 | }); 38 | 39 | describe("checking if null matches null", () => { 40 | it("throws error", () => { 41 | try { 42 | // @ts-ignore force type for test purposes 43 | anyOfClass(null); 44 | fail("If you reach this statement, the test failed."); 45 | } catch (e) { 46 | expect((e as Error).message).toEqual("The expected class cannot be null."); 47 | } 48 | }); 49 | }); 50 | 51 | describe("checking if different classes match", () => { 52 | it("returns false", () => { 53 | // given 54 | const testObj: Matcher = anyOfClass(Car); 55 | const value = "a string"; 56 | 57 | // when 58 | const result = testObj.match(value); 59 | 60 | // then 61 | expect(result).toBeFalsy(); 62 | }); 63 | }); 64 | 65 | describe("checking if toString works", () => { 66 | it("returns 'anyOfClass(Car)'", () => { 67 | // given 68 | const testObj: Matcher = anyOfClass(Car); 69 | 70 | // when 71 | const result = testObj.toString(); 72 | 73 | // then 74 | expect(result).toEqual("anyOfClass(Car)"); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/matcher/type/AnyStringMatcher.spec.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "../../../src/matcher/type/Matcher"; 2 | import {anyString} from "../../../src/ts-mockito"; 3 | 4 | describe("AnyStringMatcher", () => { 5 | describe("checking if number matches", () => { 6 | it("returns false", () => { 7 | // given 8 | const testObj: Matcher = anyString(); 9 | 10 | // when 11 | const result = testObj.match(3); 12 | 13 | // then 14 | expect(result).toBeFalsy(); 15 | }); 16 | }); 17 | 18 | describe("checking if object matches", () => { 19 | it("returns false", () => { 20 | // given 21 | const testObj: Matcher = anyString(); 22 | 23 | // when 24 | const result = testObj.match({}); 25 | 26 | // then 27 | expect(result).toBeFalsy(); 28 | }); 29 | }); 30 | 31 | describe("checking if empty string matches", () => { 32 | it("returns true", () => { 33 | // given 34 | const testObj: Matcher = anyString(); 35 | 36 | // when 37 | const result = testObj.match(""); 38 | 39 | // then 40 | expect(result).toBeTruthy(); 41 | }); 42 | }); 43 | 44 | describe("checking if sample string matches", () => { 45 | it("returns true", () => { 46 | // given 47 | const testObj: Matcher = anyString(); 48 | 49 | // when 50 | const result = testObj.match("sampleString"); 51 | 52 | // then 53 | expect(result).toBeTruthy(); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/matcher/type/AnythingMatcher.spec.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "../../../src/matcher/type/Matcher"; 2 | import {anything} from "../../../src/ts-mockito"; 3 | 4 | describe("AnythingMatcher", () => { 5 | describe("checking if number matches", () => { 6 | it("returns true", () => { 7 | // given 8 | const testObj: Matcher = anything(); 9 | 10 | // when 11 | const result = testObj.match(3); 12 | 13 | // then 14 | expect(result).toBeTruthy(); 15 | }); 16 | }); 17 | 18 | describe("checking if object matches", () => { 19 | it("returns true", () => { 20 | // given 21 | const testObj: Matcher = anything(); 22 | 23 | // when 24 | const result = testObj.match({}); 25 | 26 | // then 27 | expect(result).toBeTruthy(); 28 | }); 29 | }); 30 | 31 | describe("checking if empty string matches", () => { 32 | it("returns true", () => { 33 | // given 34 | const testObj: Matcher = anything(); 35 | 36 | // when 37 | const result = testObj.match(""); 38 | 39 | // then 40 | expect(result).toBeTruthy(); 41 | }); 42 | }); 43 | 44 | describe("checking if sample string matches", () => { 45 | it("returns true", () => { 46 | // given 47 | const testObj: Matcher = anything(); 48 | 49 | // when 50 | const result = testObj.match("sampleString"); 51 | 52 | // then 53 | expect(result).toBeTruthy(); 54 | }); 55 | }); 56 | 57 | describe("checking if null matches", () => { 58 | it("returns true", () => { 59 | // given 60 | const testObj: Matcher = anything(); 61 | 62 | // when 63 | const result = testObj.match(null); 64 | 65 | // then 66 | expect(result).toBeTruthy(); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/matcher/type/BetweenMatcher.spec.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "../../../src/matcher/type/Matcher"; 2 | import {between} from "../../../src/ts-mockito"; 3 | 4 | describe("BetweenMatcher", () => { 5 | describe("checking if value matches given min and max", () => { 6 | const testObj: Matcher = between(5, 10); 7 | 8 | describe("when given value is lower than min", () => { 9 | it("returns false", () => { 10 | // when 11 | const result = testObj.match(4); 12 | 13 | // then 14 | expect(result).toBeFalsy(); 15 | }); 16 | }); 17 | 18 | describe("when given value is equal to min ", () => { 19 | it("returns true", () => { 20 | // when 21 | const result = testObj.match(5); 22 | 23 | // then 24 | expect(result).toBeTruthy(); 25 | }); 26 | }); 27 | 28 | describe("when given value is equal grater then min but lower than max", () => { 29 | it("returns true", () => { 30 | // when 31 | const result = testObj.match(7); 32 | 33 | // then 34 | expect(result).toBeTruthy(); 35 | }); 36 | }); 37 | 38 | describe("when given value is equal to max", () => { 39 | it("returns true", () => { 40 | // when 41 | const result = testObj.match(10); 42 | 43 | // then 44 | expect(result).toBeTruthy(); 45 | }); 46 | }); 47 | 48 | describe("when given value is greater than max", () => { 49 | it("returns true", () => { 50 | // when 51 | const result = testObj.match(11); 52 | 53 | // then 54 | expect(result).toBeFalsy(); 55 | }); 56 | }); 57 | }); 58 | 59 | describe("when given min is greater than max", () => { 60 | it("it throws error", () => { 61 | // when 62 | let error = null; 63 | try { 64 | const testObj: Matcher = between(10, 9); 65 | } catch (e) { 66 | error = e; 67 | } 68 | 69 | // then 70 | expect(error).not.toBeNull(); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/matcher/type/DeepEqualMatcher.spec.ts: -------------------------------------------------------------------------------- 1 | import {DeepEqualMatcher} from "../../../src/matcher/type/DeepEqualMatcher"; 2 | import {Matcher} from "../../../src/matcher/type/Matcher"; 3 | import {anyString, deepEqual, instance, mock, verify} from "../../../src/ts-mockito"; 4 | 5 | describe("DeepEqualMatcher", () => { 6 | describe("checking if two different instances of same number matches", () => { 7 | it("returns true", () => { 8 | // given 9 | const firstValue = 3; 10 | const secondValue = 3; 11 | const testObj: Matcher = new DeepEqualMatcher(firstValue); 12 | 13 | // when 14 | const result = testObj.match(secondValue); 15 | 16 | // then 17 | expect(result).toBeTruthy(); 18 | }); 19 | }); 20 | 21 | describe("checking if two different instances of same string matches", () => { 22 | it("returns true", () => { 23 | // given 24 | const firstValue = "sampleString"; 25 | const secondValue = "sampleString"; 26 | const testObj: Matcher = new DeepEqualMatcher(firstValue); 27 | 28 | // when 29 | const result = testObj.match(secondValue); 30 | 31 | // then 32 | expect(result).toBeTruthy(); 33 | }); 34 | }); 35 | 36 | describe("checking if two different instances of same nested objects matches", () => { 37 | it("returns true", () => { 38 | // given 39 | const firstValue = {a: 1, b: {c: 2}}; 40 | const secondValue = {a: 1, b: {c: 2}}; 41 | const testObj: Matcher = new DeepEqualMatcher(firstValue); 42 | 43 | // when 44 | const result = testObj.match(secondValue); 45 | 46 | // then 47 | expect(result).toBeTruthy(); 48 | }); 49 | }); 50 | 51 | describe("checking if two nested objects matches when one leaf value is different", () => { 52 | it("returns true", () => { 53 | // given 54 | const firstValue = {a: 1, b: {c: 2}}; 55 | const secondValue = {a: 1, b: {c: 99999}}; 56 | const testObj: Matcher = new DeepEqualMatcher(firstValue); 57 | 58 | // when 59 | const result = testObj.match(secondValue); 60 | 61 | // then 62 | expect(result).toBeFalsy(); 63 | }); 64 | }); 65 | 66 | describe("checking if expected value has Matcher as a value", () => { 67 | it("returns true if matcher returns true", () => { 68 | // given 69 | const firstValue = {a: 1, b: anyString()}; 70 | const secondValue = {a: 1, b: "2"}; 71 | const testObj: Matcher = new DeepEqualMatcher(firstValue); 72 | 73 | // when 74 | const result = testObj.match(secondValue); 75 | 76 | // then 77 | expect(result).toBeTruthy(); 78 | }); 79 | 80 | it("returns false if matcher returns false", () => { 81 | // given 82 | const firstValue = {a: 1, b: anyString()}; 83 | const secondValue = {a: 1, b: 2}; 84 | const testObj: Matcher = new DeepEqualMatcher(firstValue); 85 | 86 | // when 87 | const result = testObj.match(secondValue); 88 | 89 | // then 90 | expect(result).toBeFalsy(); 91 | }); 92 | }); 93 | describe('when given circular dependency', () => { 94 | type Bar = { bar?: Bar; }; 95 | 96 | it('should verify successfully', async () => { 97 | // given 98 | const firstValue: Bar = {}; 99 | firstValue.bar = {bar: firstValue}; 100 | const testObj: Matcher = new DeepEqualMatcher(firstValue); 101 | 102 | // when 103 | const secondValue: Bar = {}; 104 | secondValue.bar = {bar: secondValue}; 105 | const result = testObj.match(secondValue); 106 | 107 | // then 108 | expect(result).toBeTruthy(); 109 | }); 110 | 111 | it('should reject gracefully', async () => { 112 | // given 113 | const firstValue: Bar = {}; 114 | firstValue.bar = {bar: firstValue}; 115 | const testObj: Matcher = new DeepEqualMatcher(firstValue); 116 | 117 | // when 118 | const secondValue: Bar = {}; 119 | const result = testObj.match(secondValue); 120 | 121 | // then 122 | expect(result).toBeFalsy(); 123 | }); 124 | }); 125 | }); 126 | 127 | describe("deepEqual", () => { 128 | describe("using in verify statements", () => { 129 | it("can be used for equality", () => { 130 | class Foo { 131 | public add = (str: string, num: number, obj: {a: string}): number | null => null; 132 | } 133 | const foo = mock(Foo); 134 | instance(foo).add("1", 2, {a: "sampleValue"}); 135 | verify(foo.add(deepEqual("1"), deepEqual(2), deepEqual({a: "sampleValue"}))).once(); 136 | }); 137 | 138 | describe('when given circular dependency', () => { 139 | type Bar = { bar?: Bar; }; 140 | class Foo { 141 | public something = (bar: Bar): number | null => null; 142 | } 143 | 144 | it('should reject gracefully', async () => { 145 | const bar: Bar = {}; 146 | bar.bar = {bar}; 147 | 148 | const foo = mock(Foo); 149 | instance(foo).something(bar); 150 | 151 | try { 152 | // when 153 | 154 | verify(foo.something(deepEqual({}))).once(); 155 | expect(true).toBe(false); // Above call should throw an exception 156 | } catch (e) { 157 | // then 158 | expect(e.message).toContain('Expected "something(deepEqual({}))" to be called 1 time(s). But has been called 0 time(s).\n'); 159 | expect(e.message).toContain("Actual calls:\n"); 160 | expect(e.message).toContain("something({\"bar\":{\"bar\":\"[Circular]\"}}"); 161 | } 162 | }); 163 | }); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /test/matcher/type/MatchingStringMatcher.spec.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "../../../src/matcher/type/Matcher"; 2 | import {match} from "../../../src/ts-mockito"; 3 | 4 | describe("MatchingStringMatcher", () => { 5 | describe("checking if value matches given regexp", () => { 6 | const testObj: Matcher = match(/\w123/); 7 | 8 | describe("when given value matches regexp", () => { 9 | it("returns true", () => { 10 | // when 11 | const result = testObj.match("a123"); 12 | 13 | // then 14 | expect(result).toBeTruthy(); 15 | }); 16 | }); 17 | 18 | describe("when given value doesn\'t match regexp", () => { 19 | it("returns false", () => { 20 | // when 21 | const result = testObj.match("123"); 22 | 23 | // then 24 | expect(result).toBeFalsy(); 25 | }); 26 | }); 27 | }); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /test/matcher/type/NotNullMatcher.spec.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "../../../src/matcher/type/Matcher"; 2 | import {notNull} from "../../../src/ts-mockito"; 3 | 4 | describe("NotNullMatcher", () => { 5 | describe("checking if null matches", () => { 6 | it("returns false", () => { 7 | // given 8 | const testObj: Matcher = notNull(); 9 | 10 | // when 11 | const result = testObj.match(null); 12 | 13 | // then 14 | expect(result).toBeFalsy(); 15 | }); 16 | }); 17 | 18 | describe("checking if false matches", () => { 19 | it("returns true", () => { 20 | // given 21 | const testObj: Matcher = notNull(); 22 | 23 | // when 24 | const result = testObj.match(false); 25 | 26 | // then 27 | expect(result).toBeTruthy(); 28 | }); 29 | }); 30 | 31 | describe("checking if zero matches", () => { 32 | it("returns true", () => { 33 | // given 34 | const testObj: Matcher = notNull(); 35 | 36 | // when 37 | const result = testObj.match(0); 38 | 39 | // then 40 | expect(result).toBeTruthy(); 41 | }); 42 | }); 43 | 44 | describe("checking if sample object matches", () => { 45 | it("returns true", () => { 46 | // given 47 | const testObj: Matcher = notNull(); 48 | 49 | // when 50 | const result = testObj.match({}); 51 | 52 | // then 53 | expect(result).toBeTruthy(); 54 | }); 55 | }); 56 | 57 | describe("checking if sample string matches", () => { 58 | it("returns true", () => { 59 | // given 60 | const testObj: Matcher = notNull(); 61 | 62 | // when 63 | const result = testObj.match("sampleString"); 64 | 65 | // then 66 | expect(result).toBeTruthy(); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/matcher/type/ObjectContainingMatcher.spec.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "../../../src/matcher/type/Matcher"; 2 | import {objectContaining} from "../../../src/ts-mockito"; 3 | 4 | describe("ObjectContainingMatcher", () => { 5 | describe("checking if source object contains given object", () => { 6 | const testObj: Matcher = objectContaining({b: {c: "c", d: {}}}); 7 | 8 | describe("when given value contains given object", () => { 9 | it("returns true", () => { 10 | // when 11 | const result = testObj.match({a: "a", b: {c: "c", d: {}}}); 12 | 13 | // then 14 | expect(result).toBeTruthy(); 15 | }); 16 | 17 | it("returns true", () => { 18 | // when 19 | const result = testObj.match({b: {c: "c", d: {}}}); 20 | 21 | // then 22 | expect(result).toBeTruthy(); 23 | }); 24 | }); 25 | 26 | describe("when given value doesn't contain given object", () => { 27 | describe("when given value is an object", () => { 28 | it("returns false", () => { 29 | // when 30 | const result = testObj.match({b: {c: "c"}}); 31 | 32 | // then 33 | expect(result).toBeFalsy(); 34 | }); 35 | 36 | it("represents the object as a json string", () => { 37 | // when 38 | const result = testObj.toString(); 39 | 40 | // then 41 | expect(result).toContain(`objectContaining({"b":{"c":"c","d":{}}`); 42 | }); 43 | }); 44 | 45 | describe("when given value is a scalar", () => { 46 | const testCases = [ 47 | { scalar: 123, expected: 123 }, 48 | { scalar: 123.456, expected: 123.456 }, 49 | { scalar: "a", expected: '"a"' }, 50 | ]; 51 | 52 | testCases.forEach(test => { 53 | it(`represents the scalar value '${test.scalar}' as string`, () => { 54 | // when 55 | const testValue: Matcher = objectContaining(test.scalar); 56 | const result = testValue.toString(); 57 | 58 | // then 59 | expect(result).toContain(`objectContaining(${test.expected})`); 60 | }); 61 | 62 | it("returns false", () => { 63 | // when 64 | const result = testObj.match(test.scalar); 65 | 66 | // then 67 | expect(result).toBeFalsy(); 68 | }); 69 | }); 70 | }); 71 | }); 72 | }); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /test/matcher/type/StrictEqualMatcher.spec.ts: -------------------------------------------------------------------------------- 1 | import {Matcher} from "../../../src/matcher/type/Matcher"; 2 | import {strictEqual} from "../../../src/ts-mockito"; 3 | 4 | describe("StrictEqualMatcher", () => { 5 | describe("checking if string representation of number matches with number", () => { 6 | it("returns false", () => { 7 | // given 8 | const testObj: Matcher = strictEqual("5"); 9 | 10 | // when 11 | const result = testObj.match(5); 12 | 13 | // then 14 | expect(result).toBeFalsy(); 15 | }); 16 | }); 17 | 18 | describe("checking if false matches with zero", () => { 19 | it("returns false", () => { 20 | // given 21 | const testObj: Matcher = strictEqual(false); 22 | 23 | // when 24 | const result = testObj.match(0); 25 | 26 | // then 27 | expect(result).toBeFalsy(); 28 | }); 29 | }); 30 | 31 | describe("checking if true matches with one", () => { 32 | it("returns false", () => { 33 | // given 34 | const testObj: Matcher = strictEqual(true); 35 | 36 | // when 37 | const result = testObj.match(1); 38 | 39 | // then 40 | expect(result).toBeFalsy(); 41 | }); 42 | }); 43 | 44 | describe("checking if same strings matches", () => { 45 | it("returns true", () => { 46 | // given 47 | const testObj: Matcher = strictEqual("5"); 48 | 49 | // when 50 | const result = testObj.match("5"); 51 | 52 | // then 53 | expect(result).toBeTruthy(); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/mocking.getter.spec.ts: -------------------------------------------------------------------------------- 1 | import {instance, mock, when} from "../src/ts-mockito"; 2 | import {Bar} from "./utils/Bar"; 3 | 4 | describe("mocking", () => { 5 | let mockedFoo: FooWithGetterAndSetter; 6 | let foo: FooWithGetterAndSetter; 7 | 8 | describe("mocking object with getters and setters", () => { 9 | it("does not execute getter or setter code (not throwing null pointer exception)", () => { 10 | // given 11 | 12 | // when 13 | mockedFoo = mock(FooWithGetterAndSetter); 14 | foo = instance(mockedFoo); 15 | 16 | // then 17 | 18 | }); 19 | 20 | it("does create own property descriptors on instance", () => { 21 | // given 22 | mockedFoo = mock(FooWithGetterAndSetter); 23 | foo = instance(mockedFoo); 24 | 25 | // when 26 | when(mockedFoo.twoPlusTwo).thenReturn(42); 27 | 28 | // then 29 | expect(foo.twoPlusTwo).toBe(42); 30 | }); 31 | 32 | it("does create inherited property descriptors on instance", () => { 33 | // given 34 | mockedFoo = mock(FooWithGetterAndSetter); 35 | foo = instance(mockedFoo); 36 | 37 | // when 38 | when(mockedFoo.sampleString).thenReturn("42"); 39 | 40 | // then 41 | expect(foo.sampleString).toBe("42"); 42 | }); 43 | }); 44 | 45 | describe("mocking object that extends abstract class", () => { 46 | it("does not throw null pointer when reading descriptor", () => { 47 | // given 48 | 49 | // when 50 | mockedFoo = mock(FooWithGetterAndSetter); 51 | foo = instance(mockedFoo); 52 | 53 | // then 54 | }); 55 | }); 56 | }); 57 | 58 | abstract class SampleAbstractClass { 59 | public get sampleString(): string { 60 | return "sampleString"; 61 | } 62 | } 63 | 64 | class FooWithGetterAndSetter extends SampleAbstractClass { 65 | constructor(private dependency: Bar) { 66 | super(); 67 | } 68 | 69 | public get twoPlusTwo(): number { 70 | return this.dependency.sumTwoNumbers(2, 2); 71 | } 72 | 73 | public set twoPlusTwo(value: number) { 74 | this.dependency.sumTwoNumbers(value, 0); 75 | } 76 | 77 | public sampleMethod(): number { 78 | return 4; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/mocking.properties.spec.ts: -------------------------------------------------------------------------------- 1 | import {MethodToStub} from "../src/MethodToStub"; 2 | import {instance, mock, verify, when} from "../src/ts-mockito"; 3 | 4 | describe("mocking", () => { 5 | let mockedFoo: FooWithProperties; 6 | let foo: FooWithProperties; 7 | 8 | if (typeof Proxy === "undefined") { 9 | pending("Testing browser doesn't support Proxy."); 10 | } 11 | 12 | describe("mocking object with properties (that don't have getters)", () => { 13 | it("does create own property descriptors on mock after when is called", () => { 14 | // given 15 | 16 | // when 17 | mockedFoo = mock(FooWithProperties); 18 | when(mockedFoo.sampleNumber).thenReturn(42); 19 | 20 | // then 21 | expect((mockedFoo.sampleNumber as any) instanceof MethodToStub).toBe(true); 22 | }); 23 | 24 | it("does create own property descriptors on instance", () => { 25 | // given 26 | mockedFoo = mock(FooWithProperties); 27 | foo = instance(mockedFoo); 28 | 29 | // when 30 | when(mockedFoo.sampleNumber).thenReturn(42); 31 | 32 | // then 33 | expect(foo.sampleNumber).toBe(42); 34 | }); 35 | 36 | it("works with verification if property is stubbed", () => { 37 | // given 38 | mockedFoo = mock(FooWithProperties); 39 | foo = instance(mockedFoo); 40 | when(mockedFoo.sampleNumber).thenReturn(42); 41 | 42 | // when 43 | const value = foo.sampleNumber; 44 | 45 | // then 46 | expect(() => verify(mockedFoo.sampleNumber).once()).not.toThrow(); 47 | }); 48 | 49 | it("works with verification if property is unstubbed", () => { 50 | // given 51 | mockedFoo = mock(FooWithProperties); 52 | foo = instance(mockedFoo); 53 | (when(mockedFoo.sampleNumber) as any).thenDoNothing(); 54 | 55 | // when 56 | const value = foo.sampleNumber; 57 | 58 | // then 59 | expect(() => verify(mockedFoo.sampleNumber).once()).not.toThrow(); 60 | }); 61 | }); 62 | 63 | }); 64 | 65 | class FooWithProperties { 66 | public readonly sampleNumber: number; 67 | } 68 | -------------------------------------------------------------------------------- /test/mocking.types.spec.ts: -------------------------------------------------------------------------------- 1 | import {MethodToStub} from "../src/MethodToStub"; 2 | import {instance, mock, when} from "../src/ts-mockito"; 3 | import {Bar} from "./utils/Bar"; 4 | import {ThenableClass} from "./utils/Thenable"; 5 | import {EventEmitter} from "events"; 6 | 7 | describe("mocking", () => { 8 | describe("mocking abstract class", () => { 9 | let mockedFoo: SampleAbstractClass; 10 | let foo: SampleAbstractClass; 11 | 12 | it("does not execute getter or setter code (not throwing null pointer exception)", () => { 13 | // given 14 | 15 | // when 16 | mockedFoo = mock(SampleAbstractClass); 17 | foo = instance(mockedFoo); 18 | 19 | // then 20 | 21 | }); 22 | 23 | it("does create own property descriptors on mock", () => { 24 | // given 25 | 26 | // when 27 | mockedFoo = mock(SampleAbstractClass); 28 | 29 | // then 30 | expect((mockedFoo.twoPlusTwo as any) instanceof MethodToStub).toBe(true); 31 | }); 32 | 33 | it("does create own property descriptors on instance", () => { 34 | // given 35 | mockedFoo = mock(SampleAbstractClass); 36 | foo = instance(mockedFoo); 37 | 38 | // when 39 | when(mockedFoo.twoPlusTwo).thenReturn(42); 40 | 41 | // then 42 | expect(foo.twoPlusTwo).toBe(42); 43 | }); 44 | 45 | it("does create inherited property descriptors on mock", () => { 46 | // given 47 | mockedFoo = mock(SampleAbstractClass); 48 | foo = instance(mockedFoo); 49 | 50 | // when 51 | 52 | // then 53 | expect((mockedFoo.sampleString as any) instanceof MethodToStub).toBe(true); 54 | }); 55 | 56 | it("does create inherited property descriptors on instance", () => { 57 | // given 58 | mockedFoo = mock(SampleAbstractClass); 59 | foo = instance(mockedFoo); 60 | 61 | // when 62 | when(mockedFoo.sampleString).thenReturn("42"); 63 | 64 | // then 65 | expect(foo.sampleString).toBe("42"); 66 | }); 67 | }); 68 | describe("mocking promise methods", () => { 69 | it("does not create then descriptor for class", () => { 70 | // given 71 | const mocked = mock(SampleAbstractClass); 72 | const inst = instance(mocked); 73 | 74 | // when 75 | 76 | // then 77 | expect((inst as any).then).toBeUndefined(); 78 | }); 79 | 80 | it("does create then descriptor", () => { 81 | // given 82 | const mocked = mock(ThenableClass); 83 | const inst = instance(mocked); 84 | 85 | // when 86 | when(mocked.then()).thenReturn("42"); 87 | 88 | // then 89 | expect(inst.then()).toEqual("42"); 90 | }); 91 | 92 | it("does not create catch descriptor", () => { 93 | // given 94 | const mocked = mock(SampleAbstractClass); 95 | const inst = instance(mocked); 96 | 97 | // when 98 | 99 | // then 100 | expect((inst as any).catch).toBeUndefined(); 101 | }); 102 | 103 | it("does create catch descriptor", () => { 104 | // given 105 | const mocked = mock(ThenableClass); 106 | const inst = instance(mocked); 107 | 108 | // when 109 | when(mocked.catch()).thenReturn("42"); 110 | 111 | // then 112 | expect(inst.catch()).toEqual("42"); 113 | }); 114 | 115 | it("default object formatting works", () => { 116 | // given 117 | const mocked = mock(ThenableClass); 118 | const inst = instance(mocked); 119 | 120 | // when 121 | 122 | // then 123 | const str = `"${inst}"`; 124 | expect(str).toEqual('"[object Object]"'); 125 | }); 126 | }); 127 | 128 | describe("mocking class with hasOwnProperty", () => { 129 | let mockedFoo: SampleClassWithHasOwnProperty; 130 | 131 | it("does not attempt to mock hasOwnProperty (which would throw)", () => { 132 | // given 133 | 134 | // when 135 | mockedFoo = mock(SampleClassWithHasOwnProperty); 136 | 137 | // then 138 | 139 | }); 140 | }); 141 | 142 | describe("mocking generic class", () => { 143 | let mockedFoo: SampleGeneric; 144 | let foo: SampleGeneric; 145 | 146 | it("does not execute getter or setter code (not throwing null pointer exception)", () => { 147 | // given 148 | 149 | // when 150 | mockedFoo = mock>(SampleGeneric); 151 | foo = instance(mockedFoo); 152 | 153 | // then 154 | 155 | }); 156 | 157 | it("does create own property descriptors on mock", () => { 158 | // given 159 | 160 | // when 161 | mockedFoo = mock>(SampleGeneric); 162 | 163 | // then 164 | expect((mockedFoo.twoPlusTwo as any) instanceof MethodToStub).toBe(true); 165 | }); 166 | 167 | it("allows to mock method with generic return type value (with IDE completion)", () => { 168 | // given 169 | mockedFoo = mock>(SampleGeneric); 170 | foo = instance(mockedFoo); 171 | const expectedResult = new SampleInterfaceImplementation(); 172 | when(mockedFoo.getGenericTypedValue()).thenReturn(expectedResult); 173 | 174 | // when 175 | const result = foo.getGenericTypedValue(); 176 | 177 | // then 178 | expect(expectedResult).toEqual(result); 179 | }); 180 | 181 | it("does create own property descriptors on instance", () => { 182 | // given 183 | mockedFoo = mock>(SampleGeneric); 184 | foo = instance(mockedFoo); 185 | 186 | // when 187 | when(mockedFoo.twoPlusTwo).thenReturn(42); 188 | 189 | // then 190 | expect(foo.twoPlusTwo).toBe(42); 191 | }); 192 | 193 | it("does create inherited property descriptors on mock", () => { 194 | // given 195 | mockedFoo = mock>(SampleGeneric); 196 | foo = instance(mockedFoo); 197 | 198 | // when 199 | 200 | // then 201 | expect((mockedFoo.sampleString as any) instanceof MethodToStub).toBe(true); 202 | }); 203 | 204 | it("does create inherited property descriptors on instance", () => { 205 | // given 206 | mockedFoo = mock>(SampleGeneric); 207 | foo = instance(mockedFoo); 208 | 209 | // when 210 | when(mockedFoo.sampleString).thenReturn("42"); 211 | 212 | // then 213 | expect(foo.sampleString).toBe("42"); 214 | }); 215 | }); 216 | 217 | describe("mocking object which doesn't inherit from anything", () => { 218 | it("does not execute getter or setter code (not throwing null pointer exception)", () => { 219 | // given 220 | const bareObject = Object.create(null); 221 | 222 | // when 223 | const mockedObject = mock(bareObject); 224 | instance(mockedObject); 225 | 226 | // then 227 | 228 | }); 229 | }); 230 | 231 | describe("mocking native class", () => { 232 | it("should mock", () => { 233 | const mocked = mock(TestEmitter); 234 | expect(mocked).toBeDefined(); 235 | }); 236 | }); 237 | 238 | describe("mocking anon class", () => { 239 | it("should mock", () => { 240 | const mocked = mock(TestAnonClass); 241 | expect(mocked).toBeDefined(); 242 | }); 243 | }); 244 | 245 | describe("mocking async class", () => { 246 | it("should mock", () => { 247 | const mocked = mock(AsyncClass); 248 | expect(mocked).toBeDefined(); 249 | }); 250 | }); 251 | 252 | describe("mocking generator class", () => { 253 | it("should mock", () => { 254 | const mocked = mock(getGeneratorClass()); 255 | expect(mocked).toBeDefined(); 256 | }); 257 | }); 258 | 259 | describe("mock empty object", () => { 260 | it("should mock", () => { 261 | const mocked = mock({}); 262 | expect(mocked).toBeDefined(); 263 | }); 264 | }); 265 | }); 266 | 267 | abstract class SampleAbstractClass { 268 | public dependency: Bar; 269 | 270 | public get sampleString(): string { 271 | return "sampleString"; 272 | } 273 | 274 | public get twoPlusTwo(): number { 275 | return this.dependency.sumTwoNumbers(2, 2); 276 | } 277 | 278 | public set twoPlusTwo(value: number) { 279 | this.dependency.sumTwoNumbers(value, 0); 280 | } 281 | 282 | public sampleMethod(): number { 283 | return 4; 284 | } 285 | } 286 | 287 | class SampleClassWithHasOwnProperty { 288 | public hasOwnProperty(name: string): boolean { 289 | return Object.prototype.hasOwnProperty.call(this, name); 290 | } 291 | } 292 | 293 | interface SampleInterface { 294 | dependency: Bar; 295 | 296 | sampleMethod(): number; 297 | } 298 | 299 | class SampleInterfaceImplementation implements SampleInterface { 300 | public dependency: Bar; 301 | 302 | public sampleMethod(): number { 303 | return 999; 304 | } 305 | } 306 | 307 | class SampleGeneric { 308 | public dependency: Bar; 309 | 310 | public get twoPlusTwo(): number { 311 | return this.dependency.sumTwoNumbers(2, 2); 312 | } 313 | 314 | public set twoPlusTwo(value: number) { 315 | this.dependency.sumTwoNumbers(value, 0); 316 | } 317 | 318 | public get sampleString(): string { 319 | return "sampleString"; 320 | } 321 | 322 | public sampleMethod(): number { 323 | return 4; 324 | } 325 | 326 | public getGenericTypedValue(): T { 327 | return null as unknown as T; 328 | } 329 | } 330 | 331 | class TestEmitter extends EventEmitter { 332 | } 333 | 334 | const TestAnonClass = class { 335 | private readonly foo = 'abc'; 336 | }; 337 | 338 | export class AsyncClass { 339 | public asyncValueArrowFn = async () => 'value'; 340 | 341 | public asyncValueFn = async function hello() { 342 | return 'value'; 343 | }; 344 | 345 | public async returnAsyncValue(): Promise { 346 | return 0; 347 | } 348 | } 349 | 350 | // Generator functions in eval to prevent downcompiling generators to classic functions 351 | // tslint:disable-next-line:no-eval 352 | const getGeneratorClass = () => eval(`class GeneratorClass { 353 | 354 | asyncValueFn = async function* hello() { 355 | return 'value'; 356 | }; 357 | 358 | valueFn = function* hello() { 359 | return 'value'; 360 | }; 361 | 362 | async *returnAsyncValue() { 363 | return 0; 364 | } 365 | 366 | *returnValue() { 367 | return 0; 368 | } 369 | } 370 | 371 | GeneratorClass 372 | `); 373 | -------------------------------------------------------------------------------- /test/recording.multiple.behaviors.spec.ts: -------------------------------------------------------------------------------- 1 | import {instance, mock, when} from "../src/ts-mockito"; 2 | import {Foo} from "./utils/Foo"; 3 | 4 | describe("recording multiple behaviors", () => { 5 | let mockedFoo: Foo; 6 | let foo: Foo; 7 | 8 | beforeEach(() => { 9 | mockedFoo = mock(Foo); 10 | foo = instance(mockedFoo); 11 | }); 12 | 13 | describe("when more than one behavior matches", () => { 14 | it("using added later", () => { 15 | // given 16 | const sampleValue = 3; 17 | const firstStubResult = "first"; 18 | const secondStubResult = "second"; 19 | when(mockedFoo.convertNumberToString(sampleValue)).thenReturn(firstStubResult); 20 | when(mockedFoo.convertNumberToString(sampleValue)).thenReturn(secondStubResult); 21 | 22 | // when 23 | const firstCallResult = foo.convertNumberToString(sampleValue); 24 | const secondCallResult = foo.convertNumberToString(sampleValue); 25 | 26 | // then 27 | expect(firstCallResult).toEqual(secondStubResult); 28 | expect(secondCallResult).toEqual(secondStubResult); 29 | }); 30 | }); 31 | 32 | describe("when one of behaviors doesn't match", () => { 33 | it("is skipped", () => { 34 | // given 35 | const sampleValue = 3; 36 | const firstMatchingStubResult = "first"; 37 | const secondMatchingStubResult = "second"; 38 | when(mockedFoo.convertNumberToString(sampleValue)).thenReturn(firstMatchingStubResult); 39 | when(mockedFoo.convertNumberToString(123)).thenReturn("not matching behavior"); 40 | when(mockedFoo.convertNumberToString(sampleValue)).thenReturn(secondMatchingStubResult); 41 | 42 | // when 43 | const firstCallResult = foo.convertNumberToString(sampleValue); 44 | const secondCallResult = foo.convertNumberToString(sampleValue); 45 | 46 | // then 47 | expect(firstCallResult).toEqual(secondMatchingStubResult); 48 | expect(secondCallResult).toEqual(secondMatchingStubResult); 49 | }); 50 | }); 51 | 52 | describe("when calling same method multiple times", () => { 53 | describe("used behaviors are removed", () => { 54 | describe("and when no more than one matcher left", () => { 55 | it("repeats last one", () => { 56 | // given 57 | const sampleValue = 3; 58 | const firstMatchingStubResult = "first"; 59 | const secondMatchingStubResult = "second"; 60 | when(mockedFoo.convertNumberToString(sampleValue)) 61 | .thenReturn(firstMatchingStubResult) 62 | .thenReturn(secondMatchingStubResult); 63 | 64 | // when 65 | const firstCallResult = foo.convertNumberToString(sampleValue); 66 | const secondCallResult = foo.convertNumberToString(sampleValue); 67 | const thirdCallResult = foo.convertNumberToString(sampleValue); 68 | const fourthCallResult = foo.convertNumberToString(sampleValue); 69 | const fifthCallResult = foo.convertNumberToString(sampleValue); 70 | 71 | // then 72 | expect(firstCallResult).toEqual(firstMatchingStubResult); 73 | expect(secondCallResult).toEqual(secondMatchingStubResult); 74 | expect(thirdCallResult).toEqual(secondMatchingStubResult); 75 | expect(fourthCallResult).toEqual(secondMatchingStubResult); 76 | expect(fifthCallResult).toEqual(secondMatchingStubResult); 77 | }); 78 | }); 79 | }); 80 | }); 81 | 82 | describe("when multiple results has been set by one method call", () => { 83 | it("uses one by another and repeat last one infinitely", () => { 84 | // given 85 | const sampleValue = 3; 86 | const firstMatchingStubResult = "first"; 87 | const secondMatchingStubResult = "second"; 88 | when(mockedFoo.convertNumberToString(sampleValue)).thenReturn(firstMatchingStubResult, secondMatchingStubResult); 89 | 90 | // when 91 | const firstCallResult = foo.convertNumberToString(sampleValue); 92 | const secondCallResult = foo.convertNumberToString(sampleValue); 93 | const thirdCallResult = foo.convertNumberToString(sampleValue); 94 | const fourthCallResult = foo.convertNumberToString(sampleValue); 95 | const fifthCallResult = foo.convertNumberToString(sampleValue); 96 | 97 | // then 98 | expect(firstCallResult).toEqual(firstMatchingStubResult); 99 | expect(secondCallResult).toEqual(secondMatchingStubResult); 100 | expect(thirdCallResult).toEqual(secondMatchingStubResult); 101 | expect(fourthCallResult).toEqual(secondMatchingStubResult); 102 | expect(fifthCallResult).toEqual(secondMatchingStubResult); 103 | }); 104 | }); 105 | 106 | describe("when return values are mixed with throw errors", () => { 107 | it("uses one by one and repeat last one infinitely", () => { 108 | // given 109 | const sampleValue = 3; 110 | const firstMatchingStubResult = "first"; 111 | const secondMatchingStubResult = "second"; 112 | const firstMatchingError = new Error("firstError"); 113 | when(mockedFoo.convertNumberToString(sampleValue)) 114 | .thenReturn(firstMatchingStubResult) 115 | .thenThrow(firstMatchingError) 116 | .thenReturn(secondMatchingStubResult); 117 | 118 | // when 119 | const firstCallResult = foo.convertNumberToString(sampleValue); 120 | let error: Error | null = null; 121 | try { 122 | foo.convertNumberToString(sampleValue); 123 | } catch (e) { 124 | error = e; 125 | } 126 | const thirdCallResult = foo.convertNumberToString(sampleValue); 127 | const fourthCallResult = foo.convertNumberToString(sampleValue); 128 | const fifthCallResult = foo.convertNumberToString(sampleValue); 129 | 130 | // then 131 | expect(firstCallResult).toEqual(firstMatchingStubResult); 132 | expect(error?.message).toEqual(firstMatchingError.message); 133 | expect(thirdCallResult).toEqual(secondMatchingStubResult); 134 | expect(fourthCallResult).toEqual(secondMatchingStubResult); 135 | expect(fifthCallResult).toEqual(secondMatchingStubResult); 136 | }); 137 | }); 138 | 139 | describe("when just one behavior was set", () => { 140 | it("behavior is not removed", () => { 141 | // given 142 | const sampleValue = 3; 143 | const firstCallExpectedResult = "first"; 144 | when(mockedFoo.convertNumberToString(sampleValue)).thenReturn(firstCallExpectedResult); 145 | 146 | // when 147 | const firstCallResult = foo.convertNumberToString(sampleValue); 148 | const secondCallResult = foo.convertNumberToString(sampleValue); 149 | const thirdCallResult = foo.convertNumberToString(sampleValue); 150 | 151 | // then 152 | expect(firstCallResult).toEqual(firstCallExpectedResult); 153 | expect(secondCallResult).toEqual(firstCallExpectedResult); 154 | expect(thirdCallResult).toEqual(firstCallExpectedResult); 155 | }); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /test/reseting.calls.spec.ts: -------------------------------------------------------------------------------- 1 | import { Mocker } from "../src/Mock"; 2 | import { anything, instance, mock, resetCalls, verify } from "../src/ts-mockito"; 3 | import {Foo} from "./utils/Foo"; 4 | 5 | describe("resetting mocked object", () => { 6 | let mockedFoo: Foo; 7 | let foo: Foo; 8 | 9 | beforeEach(() => { 10 | mockedFoo = mock(Foo); 11 | foo = instance(mockedFoo); 12 | }); 13 | 14 | describe("when method has been called once", () => { 15 | describe("but later stub has been reset", () => { 16 | it("shows that never been called", () => { 17 | // given 18 | foo.getBar(); 19 | verify(mockedFoo.getBar()).once(); 20 | 21 | // when 22 | resetCalls(mockedFoo); 23 | 24 | // then 25 | verify(mockedFoo.getBar()).never(); 26 | }); 27 | }); 28 | }); 29 | 30 | describe("when method has been called thrice", () => { 31 | describe("but later stub has been reset", () => { 32 | it("shows that never been called", () => { 33 | // given 34 | foo.getBar(); 35 | foo.getBar(); 36 | foo.getBar(); 37 | verify(mockedFoo.getBar()).thrice(); 38 | 39 | // when 40 | resetCalls(mockedFoo); 41 | 42 | // then 43 | verify(mockedFoo.getBar()).never(); 44 | }); 45 | }); 46 | }); 47 | 48 | describe("when method has been called with arguments twice", () => { 49 | describe("but later stub has been reset", () => { 50 | it("shows that never been called", () => { 51 | // given 52 | foo.sumTwoNumbers(2, 3); 53 | foo.sumTwoNumbers(2, 3); 54 | verify(mockedFoo.sumTwoNumbers(2, 3)).twice(); 55 | 56 | // when 57 | resetCalls(mockedFoo); 58 | 59 | // then 60 | verify(mockedFoo.sumTwoNumbers(2, 3)).never(); 61 | verify(mockedFoo.sumTwoNumbers(anything(), anything())).never(); 62 | }); 63 | }); 64 | }); 65 | 66 | describe("when two different methods has been called twice", () => { 67 | describe("but later stub has been reset", () => { 68 | it("shows that never been called", () => { 69 | // given 70 | foo.getBar(); 71 | foo.getBar(); 72 | foo.sumTwoNumbers(2, 3); 73 | foo.sumTwoNumbers(2, 3); 74 | verify(mockedFoo.getBar()).twice(); 75 | verify(mockedFoo.sumTwoNumbers(2, 3)).twice(); 76 | 77 | // when 78 | resetCalls(mockedFoo); 79 | 80 | // then 81 | verify(mockedFoo.getBar()).never(); 82 | verify(mockedFoo.sumTwoNumbers(2, 3)).never(); 83 | verify(mockedFoo.sumTwoNumbers(anything(), anything())).never(); 84 | }); 85 | }); 86 | }); 87 | 88 | describe("when two different methods has been called", () => { 89 | describe("but later stub has been reset", () => { 90 | it("throws exception with information that methods has not been called", () => { 91 | // given 92 | foo.getBar(); 93 | foo.sumTwoNumbers(2, 3); 94 | verify(mockedFoo.getBar()).calledBefore(mockedFoo.sumTwoNumbers(2, 3)); 95 | 96 | // when 97 | resetCalls(mockedFoo); 98 | 99 | // then 100 | try { 101 | verify(mockedFoo.getBar()).calledBefore(mockedFoo.sumTwoNumbers(2, 3)); 102 | fail(); 103 | } catch (e) { 104 | expect(e.message).toContain("to be called before"); 105 | expect(e.message).toContain("but none of them has been called"); 106 | } 107 | }); 108 | }); 109 | }); 110 | 111 | describe("when two different methods has been after", () => { 112 | describe("but later stub has been reset", () => { 113 | it("throws exception with information that methods has not been called", () => { 114 | // given 115 | foo.getBar(); 116 | foo.sumTwoNumbers(2, 3); 117 | verify(mockedFoo.sumTwoNumbers(2, 3)).calledAfter(mockedFoo.getBar()); 118 | 119 | // when 120 | resetCalls(mockedFoo); 121 | 122 | // then 123 | try { 124 | verify(mockedFoo.sumTwoNumbers(2, 3)).calledAfter(mockedFoo.getBar()); 125 | fail(); 126 | } catch (e) { 127 | expect(e.message).toContain("to be called after"); 128 | expect(e.message).toContain("but none of them has been called"); 129 | } 130 | }); 131 | }); 132 | }); 133 | 134 | it("should reset calls of all passed mocks", () => { 135 | if (typeof Proxy === "undefined") { 136 | pending("Testing browser doesn't support Proxy."); 137 | } 138 | 139 | // given 140 | const firstMock = mock(); 141 | const secondMock = mock(); 142 | 143 | // when 144 | resetCalls({__tsmockitoMocker: instance(firstMock)}, {__tsmockitoMocker: instance(secondMock)}); 145 | 146 | // then 147 | verify(firstMock.resetCalls()).once(); 148 | verify(secondMock.resetCalls()).once(); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /test/reseting.spec.ts: -------------------------------------------------------------------------------- 1 | import { Mocker } from "../src/Mock"; 2 | import {anything, instance, mock, reset, verify, when} from "../src/ts-mockito"; 3 | import {Foo} from "./utils/Foo"; 4 | 5 | describe("resetting mocked object", () => { 6 | let mockedFoo: Foo; 7 | let foo: Foo; 8 | 9 | beforeEach(() => { 10 | mockedFoo = mock(Foo); 11 | foo = instance(mockedFoo); 12 | }); 13 | 14 | describe("when method has been called once", () => { 15 | describe("but later stub has been reset", () => { 16 | it("shows that never been called", () => { 17 | // given 18 | foo.getBar(); 19 | verify(mockedFoo.getBar()).once(); 20 | 21 | // when 22 | reset(mockedFoo); 23 | 24 | // then 25 | verify(mockedFoo.getBar()).never(); 26 | }); 27 | }); 28 | }); 29 | 30 | describe("when method has been called thrice", () => { 31 | describe("but later stub has been reset", () => { 32 | it("shows that never been called", () => { 33 | // given 34 | foo.getBar(); 35 | foo.getBar(); 36 | foo.getBar(); 37 | verify(mockedFoo.getBar()).thrice(); 38 | 39 | // when 40 | reset(mockedFoo); 41 | 42 | // then 43 | verify(mockedFoo.getBar()).never(); 44 | }); 45 | }); 46 | }); 47 | 48 | describe("when method has been called with arguments twice", () => { 49 | describe("but later stub has been reset", () => { 50 | it("shows that never been called", () => { 51 | // given 52 | foo.sumTwoNumbers(2, 3); 53 | foo.sumTwoNumbers(2, 3); 54 | verify(mockedFoo.sumTwoNumbers(2, 3)).twice(); 55 | 56 | // when 57 | reset(mockedFoo); 58 | 59 | // then 60 | verify(mockedFoo.sumTwoNumbers(2, 3)).never(); 61 | verify(mockedFoo.sumTwoNumbers(anything(), anything())).never(); 62 | }); 63 | }); 64 | }); 65 | 66 | describe("when two different methods has been called twice", () => { 67 | describe("but later stub has been reset", () => { 68 | it("shows that never been called", () => { 69 | // given 70 | foo.getBar(); 71 | foo.getBar(); 72 | foo.sumTwoNumbers(2, 3); 73 | foo.sumTwoNumbers(2, 3); 74 | verify(mockedFoo.getBar()).twice(); 75 | verify(mockedFoo.sumTwoNumbers(2, 3)).twice(); 76 | 77 | // when 78 | reset(mockedFoo); 79 | 80 | // then 81 | verify(mockedFoo.getBar()).never(); 82 | verify(mockedFoo.sumTwoNumbers(2, 3)).never(); 83 | verify(mockedFoo.sumTwoNumbers(anything(), anything())).never(); 84 | }); 85 | }); 86 | }); 87 | 88 | describe("when two different methods has been called", () => { 89 | describe("but later stub has been reset", () => { 90 | it("throws exception with information that methods has not been called", () => { 91 | // given 92 | foo.getBar(); 93 | foo.sumTwoNumbers(2, 3); 94 | verify(mockedFoo.getBar()).calledBefore(mockedFoo.sumTwoNumbers(2, 3)); 95 | 96 | // when 97 | reset(mockedFoo); 98 | 99 | // then 100 | try { 101 | verify(mockedFoo.getBar()).calledBefore(mockedFoo.sumTwoNumbers(2, 3)); 102 | fail(); 103 | } catch (e) { 104 | expect(e.message).toContain("to be called before"); 105 | expect(e.message).toContain("but none of them has been called"); 106 | } 107 | }); 108 | }); 109 | }); 110 | 111 | describe("when two different methods has been after", () => { 112 | describe("but later stub has been reset", () => { 113 | it("throws exception with information that methods has not been called", () => { 114 | // given 115 | foo.getBar(); 116 | foo.sumTwoNumbers(2, 3); 117 | verify(mockedFoo.sumTwoNumbers(2, 3)).calledAfter(mockedFoo.getBar()); 118 | 119 | // when 120 | reset(mockedFoo); 121 | 122 | // then 123 | try { 124 | verify(mockedFoo.sumTwoNumbers(2, 3)).calledAfter(mockedFoo.getBar()); 125 | fail(); 126 | } catch (e) { 127 | expect(e.message).toContain("to be called after"); 128 | expect(e.message).toContain("but none of them has been called"); 129 | } 130 | }); 131 | }); 132 | }); 133 | 134 | describe("when object has been stubbed", () => { 135 | describe("but later stub has been reset", () => { 136 | it("should reset configured stubs", () => { 137 | // given 138 | when(mockedFoo.convertNumberToString(3)).thenReturn("three"); 139 | reset(mockedFoo); 140 | 141 | // when 142 | const result: string = foo.convertNumberToString(3); 143 | 144 | // then 145 | expect(result).toBeNull(); 146 | }); 147 | }); 148 | }); 149 | 150 | describe("when object has been stubbed", () => { 151 | describe("but later stub has been reset", () => { 152 | it("should be able to setup new stubs", () => { 153 | // given 154 | when(mockedFoo.convertNumberToString(3)).thenReturn("three"); 155 | reset(mockedFoo); 156 | when(mockedFoo.convertNumberToString(3)).thenReturn("newStubbedValue"); 157 | 158 | // when 159 | const result: string = foo.convertNumberToString(3); 160 | 161 | // then 162 | expect(result).toEqual("newStubbedValue"); 163 | }); 164 | }); 165 | }); 166 | 167 | it("should reset all passed mocks", () => { 168 | if (typeof Proxy === "undefined") { 169 | pending("Testing browser doesn't support Proxy."); 170 | } 171 | 172 | // given 173 | const firstMock = mock(); 174 | const secondMock = mock(); 175 | 176 | // when 177 | reset({__tsmockitoMocker: instance(firstMock)}, {__tsmockitoMocker: instance(secondMock)}); 178 | 179 | // then 180 | verify(firstMock.reset()).once(); 181 | verify(secondMock.reset()).once(); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /test/spy.spec.ts: -------------------------------------------------------------------------------- 1 | import {capture, reset, spy, verify, when} from "../src/ts-mockito"; 2 | 3 | describe("spying on a real object", () => { 4 | class Real { 5 | public dynamicMethod: Function; 6 | public dynamicMethodInFunction: Function; 7 | public b = 11; 8 | 9 | constructor() { 10 | this.dynamicMethod = () => "dynamicMethod"; 11 | } 12 | 13 | get baz() { 14 | return 3; 15 | } 16 | 17 | public getBar(): string { 18 | this.dynamicMethodInFunction = () => "dynamicMethodInFunction"; 19 | return "bar"; 20 | } 21 | public foo(a: number) { 22 | return a; 23 | } 24 | 25 | public bar() { 26 | return 2; 27 | } 28 | } 29 | 30 | function RealFn() { 31 | 32 | } 33 | 34 | describe("calling a mocked method", () => { 35 | it("delegates a call to the mock", () => { 36 | // given 37 | const foo = new Real(); 38 | const spiedFoo = spy(foo); 39 | 40 | // when 41 | when(spiedFoo.bar()).thenReturn(3); 42 | 43 | // then 44 | expect(foo.bar()).toBe(3); 45 | }); 46 | 47 | it("executes the real method if arguments don't match", () => { 48 | // given 49 | const foo = new Real(); 50 | const spiedFoo = spy(foo); 51 | 52 | // when 53 | when(spiedFoo.foo(1)).thenReturn(42); 54 | 55 | // then 56 | expect(foo.foo(2)).toBe(2); 57 | }); 58 | }); 59 | 60 | describe("calling a real method", () => { 61 | it("executes the instance method", () => { 62 | // given 63 | const foo = new Real(); 64 | 65 | // when 66 | spy(foo); 67 | 68 | // then 69 | expect(foo.bar()).toBe(2); 70 | expect(foo.getBar()).toBe("bar"); 71 | expect(foo.dynamicMethod()).toBe("dynamicMethod"); 72 | expect(foo.dynamicMethodInFunction()).toBe("dynamicMethodInFunction"); 73 | 74 | }); 75 | }); 76 | 77 | describe("calling an object's own method", () => { 78 | it("delegates a call to the mock", () => { 79 | // given 80 | const foo = { 81 | bar: () => 3, 82 | }; 83 | const spiedFoo = spy(foo); 84 | 85 | // when 86 | when(spiedFoo.bar()).thenReturn(42); 87 | 88 | // then 89 | expect(foo.bar()).toBe(42); 90 | }); 91 | }); 92 | 93 | describe("spying functions", () => { 94 | it("should not mock function.prototype methods", () => { 95 | // when 96 | spy(RealFn); 97 | 98 | expect(RealFn.bind).toBe(Function.prototype.bind); 99 | expect(RealFn.apply).toBe(Function.prototype.apply); 100 | }); 101 | }); 102 | 103 | describe("access to a real object property", () => { 104 | it("get instance property", () => { 105 | // given 106 | const foo = new Real(); 107 | 108 | // when 109 | spy(foo); 110 | 111 | // then 112 | expect(foo.b).toBe(11); 113 | }); 114 | }); 115 | 116 | describe("capturing", () => { 117 | it("captures a call to the real method", () => { 118 | // given 119 | const foo = new Real(); 120 | const spiedFoo = spy(foo); 121 | 122 | // when 123 | foo.bar(); 124 | 125 | // then 126 | expect(capture(spiedFoo.bar).last()).toBeDefined(); 127 | }); 128 | 129 | it("captures the call arguments", () => { 130 | // given 131 | const foo = new Real(); 132 | const spiedFoo = spy(foo); 133 | 134 | // when 135 | foo.foo(42); 136 | 137 | // then 138 | expect(capture(spiedFoo.foo).last()).toEqual([42]); 139 | }); 140 | 141 | it("captures a call to the own property", () => { 142 | // given 143 | const foo = { 144 | bar: a => a, 145 | }; 146 | const spiedFoo = spy(foo); 147 | 148 | // when 149 | foo.bar(42); 150 | 151 | // then 152 | expect(capture(spiedFoo.bar).last()).toEqual([42]); 153 | }); 154 | }); 155 | 156 | describe("verifying calls", () => { 157 | it("throws an error if number of calls doesn't match", () => { 158 | // given 159 | const foo = new Real(); 160 | const spiedFoo = spy(foo); 161 | 162 | // when 163 | foo.bar(); 164 | foo.bar(); 165 | 166 | // then 167 | expect(() => verify(spiedFoo.bar()).once()).toThrow(); 168 | }); 169 | 170 | describe("when foo() is called before bar()", () => { 171 | it("throws an error if expected foo() to have been called after bar()", () => { 172 | // given 173 | const foo = new Real(); 174 | const spiedFoo = spy(foo); 175 | 176 | // when 177 | foo.foo(1); 178 | foo.bar(); 179 | 180 | // then 181 | expect(() => verify(spiedFoo.foo(1)).calledAfter(spiedFoo.bar())).toThrow(); 182 | }); 183 | 184 | it("passes if expected foo() to have been before after bar()", () => { 185 | // given 186 | const foo = new Real(); 187 | const spiedFoo = spy(foo); 188 | 189 | // when 190 | foo.foo(1); 191 | foo.bar(); 192 | 193 | // then 194 | expect(() => verify(spiedFoo.foo(1)).calledBefore(spiedFoo.bar())).not.toThrow(); 195 | }); 196 | }); 197 | }); 198 | 199 | describe("resetting", () => { 200 | it("restores a call to the real method", () => { 201 | // given 202 | const foo = new Real(); 203 | const spiedFoo = spy(foo); 204 | 205 | // when 206 | when(spiedFoo.bar()).thenReturn(3); 207 | reset(spiedFoo); 208 | 209 | // then 210 | expect(foo.bar()).toBe(2); 211 | }); 212 | 213 | it("cleans up not owned property descriptors", () => { 214 | // given 215 | const foo = new Real(); 216 | const spiedFoo = spy(foo); 217 | 218 | // when 219 | when(spiedFoo.baz).thenReturn(42); 220 | reset(spiedFoo); 221 | 222 | // then 223 | expect(Object.getOwnPropertyDescriptor(foo, "baz")).not.toBeDefined(); 224 | }); 225 | 226 | it("restores getter properties", () => { 227 | // given 228 | const foo = new Real(); 229 | const spiedFoo = spy(foo); 230 | 231 | // when 232 | when(spiedFoo.baz).thenReturn(42); 233 | reset(spiedFoo); 234 | 235 | // then 236 | expect(foo.baz).toBe(3); 237 | }); 238 | }); 239 | 240 | describe("spying on object which doesn't inherit from anything", () => { 241 | let bareObject; 242 | 243 | beforeEach(() => { 244 | bareObject = Object.create(null, { 245 | someMethod: { 246 | writable: true, 247 | configurable: true, 248 | value: () => 1, 249 | }, 250 | otherMethod: { 251 | writable: true, 252 | configurable: true, 253 | value: () => 2, 254 | }, 255 | }); 256 | }); 257 | 258 | it("can be spied (doesn't throw an exception)", () => { 259 | // given 260 | 261 | // when 262 | spy(bareObject); 263 | 264 | // then 265 | }); 266 | 267 | it("executes the instance method", () => { 268 | // given 269 | 270 | // when 271 | spy(bareObject); 272 | 273 | // then 274 | expect(bareObject.otherMethod()).toBe(2); 275 | }); 276 | 277 | it("delegates a call to the mock", () => { 278 | // given 279 | const spiedObject = spy(bareObject); 280 | 281 | // when 282 | when(spiedObject.someMethod()).thenReturn(2); 283 | 284 | // then 285 | expect(bareObject.someMethod()).toBe(2); 286 | }); 287 | 288 | it("delegates a call to the mock for dynamically created function", () => { 289 | // given 290 | bareObject.newMethod = () => true; 291 | const spiedObject = spy(bareObject); 292 | 293 | // when 294 | when(spiedObject.newMethod()).thenReturn(false); 295 | 296 | // then 297 | expect(bareObject.newMethod()).toBeFalsy(); 298 | }); 299 | }); 300 | }); 301 | -------------------------------------------------------------------------------- /test/stubbing.method.spec.ts: -------------------------------------------------------------------------------- 1 | import {anything, instance, mock, when} from "../src/ts-mockito"; 2 | import {Foo} from "./utils/Foo"; 3 | 4 | describe("mocking", () => { 5 | let mockedFoo: Foo; 6 | let foo: Foo; 7 | 8 | beforeEach(() => { 9 | mockedFoo = mock(Foo); 10 | foo = instance(mockedFoo); 11 | }); 12 | 13 | describe("calling method", () => { 14 | describe("with stubbed return value", () => { 15 | describe("without params", () => { 16 | it("returns stubbed value", () => { 17 | // given 18 | const expectedResult = "fake result"; 19 | when(mockedFoo.getBar()).thenReturn(expectedResult); 20 | 21 | // when 22 | const result = foo.getBar(); 23 | 24 | // then 25 | expect(result).toEqual(expectedResult); 26 | }); 27 | }); 28 | 29 | describe("with single param", () => { 30 | it("returns stubbed value", () => { 31 | // given 32 | const expectedResult = "sampleResult"; 33 | const sampleNumber = 10; 34 | when(mockedFoo.convertNumberToString(sampleNumber)).thenReturn(expectedResult); 35 | 36 | // when 37 | const result = foo.convertNumberToString(sampleNumber); 38 | 39 | // then 40 | expect(result).toEqual(expectedResult); 41 | }); 42 | }); 43 | 44 | describe("with two params", () => { 45 | it("returns stubbed value if two params matches", () => { 46 | // given 47 | const expectedResult = 999; 48 | const firstNumber = 20; 49 | const secondNumber = 30; 50 | when(mockedFoo.sumTwoNumbers(firstNumber, secondNumber)).thenReturn(expectedResult); 51 | 52 | // when 53 | const result = foo.sumTwoNumbers(firstNumber, secondNumber); 54 | 55 | // then 56 | expect(result).toEqual(expectedResult); 57 | }); 58 | 59 | it("returns null if first param doesn't match", () => { 60 | // given 61 | const expectedResult = 999; 62 | const firstNumber = 20; 63 | const secondNumber = 30; 64 | when(mockedFoo.sumTwoNumbers(firstNumber, secondNumber)).thenReturn(expectedResult); 65 | 66 | // when 67 | const result = foo.sumTwoNumbers(123, secondNumber); 68 | 69 | // then 70 | expect(result).toBeNull(); 71 | }); 72 | 73 | it("returns null if second param doesn't match", () => { 74 | // given 75 | const expectedResult = 999; 76 | const firstNumber = 20; 77 | const secondNumber = 30; 78 | when(mockedFoo.sumTwoNumbers(firstNumber, secondNumber)).thenReturn(expectedResult); 79 | 80 | // when 81 | const result = foo.sumTwoNumbers(firstNumber, 123); 82 | 83 | // then 84 | expect(result).toBeNull(); 85 | }); 86 | 87 | it("returns null if both params doesn't match", () => { 88 | // given 89 | const expectedResult = 999; 90 | const firstNumber = 20; 91 | const secondNumber = 30; 92 | when(mockedFoo.sumTwoNumbers(firstNumber, secondNumber)).thenReturn(expectedResult); 93 | 94 | // when 95 | const result = foo.sumTwoNumbers(123, 321); 96 | 97 | // then 98 | expect(result).toBeNull(); 99 | }); 100 | }); 101 | 102 | describe("with optional argument", () => { 103 | describe("and optional argument is provided", () => { 104 | it("returns stubbed value", () => { 105 | // given 106 | const expectedResult = 999; 107 | const firstNumber = 2; 108 | const secondNumber = 3; 109 | when(mockedFoo.sampleMethodWithOptionalArgument(firstNumber, secondNumber)).thenReturn(expectedResult); 110 | 111 | // when 112 | const result = foo.sampleMethodWithOptionalArgument(firstNumber, secondNumber); 113 | 114 | // then 115 | expect(expectedResult).toEqual(result); 116 | }); 117 | }); 118 | 119 | describe("and optional argument is not provided", () => { 120 | it("returns stubbed value", () => { 121 | // given 122 | const firstExpectedResult = 999; 123 | const secondExpectedResult = 333; 124 | const firstNumber = 2; 125 | const secondNumber = 3; 126 | when(mockedFoo.sampleMethodWithOptionalArgument(firstNumber)).thenReturn(firstExpectedResult); 127 | when(mockedFoo.sampleMethodWithOptionalArgument(firstNumber, secondNumber)).thenReturn(secondExpectedResult); 128 | 129 | // when 130 | const firstResult = foo.sampleMethodWithOptionalArgument(firstNumber); 131 | const secondResult = foo.sampleMethodWithOptionalArgument(firstNumber, secondNumber); 132 | 133 | // then 134 | expect(firstExpectedResult).toEqual(firstResult); 135 | expect(secondExpectedResult).toEqual(secondResult); 136 | }); 137 | }); 138 | }); 139 | }); 140 | 141 | describe("with stubbed error", () => { 142 | it("throws given error", () => { 143 | // given 144 | const sampleValue = 123; 145 | const sampleError = new Error("sampleError"); 146 | when(mockedFoo.convertNumberToString(sampleValue)).thenThrow(sampleError); 147 | 148 | // when 149 | let error:Error | null = null; 150 | try { 151 | foo.convertNumberToString(sampleValue); 152 | } catch (e) { 153 | error = e; 154 | } 155 | 156 | // then 157 | expect(error?.message).toEqual("sampleError"); 158 | }); 159 | }); 160 | 161 | describe("with stubbed promise resolve", () => { 162 | it("resolves with given value", done => { 163 | // given 164 | const sampleValue = "abc"; 165 | const expectedResult = "def"; 166 | when(mockedFoo.sampleMethodReturningPromise(sampleValue)).thenResolve(expectedResult); 167 | 168 | // when 169 | foo.sampleMethodReturningPromise(sampleValue) 170 | .then(value => { 171 | // then 172 | expect(value).toEqual(expectedResult); 173 | done(); 174 | }) 175 | .catch(err => done.fail(err)); 176 | }); 177 | 178 | it("resolves with multiple values", done => { 179 | when(mockedFoo.sampleMethodReturningPromise("abc")).thenResolve("one", "two", "three"); 180 | 181 | foo.sampleMethodReturningPromise("abc") 182 | .then(value => { 183 | expect(value).toEqual("one"); 184 | return foo.sampleMethodReturningPromise("abc"); 185 | }) 186 | .then(value => { 187 | expect(value).toEqual("two"); 188 | return foo.sampleMethodReturningPromise("abc"); 189 | }) 190 | .then(value => { 191 | expect(value).toEqual("three"); 192 | done(); 193 | }) 194 | .catch(err => done.fail(err)); 195 | }); 196 | 197 | it("resolves void promise", done => { 198 | when(mockedFoo.sampleMethodReturningVoidPromise("abc")).thenResolve(undefined); 199 | 200 | foo.sampleMethodReturningVoidPromise("abc") 201 | .then(() => { 202 | done(); 203 | }) 204 | .catch(err => done.fail(err)); 205 | }); 206 | 207 | it("resolves void promise without arguments", done => { 208 | when(mockedFoo.sampleMethodReturningVoidPromise("abc")).thenResolve(); 209 | 210 | foo.sampleMethodReturningVoidPromise("abc") 211 | .then(() => { 212 | done(); 213 | }) 214 | .catch(err => done.fail(err)); 215 | }); 216 | }); 217 | 218 | describe("with stubbed promise rejection", () => { 219 | it("rejects with given error", done => { 220 | // given 221 | const sampleValue = "abc"; 222 | const sampleError = new Error("sampleError"); 223 | when(mockedFoo.sampleMethodReturningPromise(sampleValue)).thenReject(sampleError); 224 | 225 | // when 226 | foo.sampleMethodReturningPromise(sampleValue) 227 | .then(value => done.fail()) 228 | .catch(err => { 229 | // then 230 | expect(err.message).toEqual("sampleError"); 231 | done(); 232 | }); 233 | }); 234 | 235 | describe("But without defined error", () => { 236 | it("rejects with default error", done => { 237 | // given 238 | when(mockedFoo.sampleMethodReturningVoidPromiseWithoutParams()).thenReject(); 239 | 240 | // when 241 | foo.sampleMethodReturningVoidPromiseWithoutParams() 242 | .then(value => done.fail()) 243 | .catch(err => { 244 | // then 245 | expect(err.message).toEqual("mocked 'sampleMethodReturningVoidPromiseWithoutParams' rejected"); 246 | done(); 247 | }); 248 | }); 249 | }); 250 | 251 | }); 252 | 253 | describe("with stubbed function call", () => { 254 | it("calls given function", () => { 255 | // given 256 | const sampleValue = 123; 257 | let called = false; 258 | when(mockedFoo.convertNumberToString(sampleValue)).thenCall(() => { 259 | called = true; 260 | }); 261 | 262 | // when 263 | foo.convertNumberToString(sampleValue); 264 | 265 | // then 266 | expect(called).toBeTruthy(); 267 | }); 268 | }); 269 | 270 | describe("with stubbed function call", () => { 271 | describe("if mocked method is called with different argument", () => { 272 | it("dont call given function", () => { 273 | // given 274 | const sampleValue = 123; 275 | let called = false; 276 | when(mockedFoo.convertNumberToString(sampleValue)).thenCall(() => { 277 | called = true; 278 | }); 279 | 280 | // when 281 | foo.convertNumberToString(999); 282 | 283 | // then 284 | expect(called).toBeFalsy(); 285 | }); 286 | }); 287 | }); 288 | 289 | describe("with stubbed function call", () => { 290 | it("returns value returned by given function", () => { 291 | // given 292 | const sampleValue = 123; 293 | const expectedResult = "valueFromFunction"; 294 | when(mockedFoo.convertNumberToString(sampleValue)).thenCall(() => { 295 | return expectedResult; 296 | }); 297 | 298 | // when 299 | const result = foo.convertNumberToString(sampleValue); 300 | 301 | // then 302 | expect(result).toEqual(expectedResult); 303 | }); 304 | }); 305 | 306 | describe("with stubbed function call", () => { 307 | it("pass arguments to given function", () => { 308 | // given 309 | const firstNumber = 5; 310 | const secondNumber = 10; 311 | const expectedResult = 50; 312 | when(mockedFoo.sumTwoNumbers(firstNumber, secondNumber)).thenCall((arg1: number, arg2: number) => { 313 | return arg1 * arg2; 314 | }); 315 | 316 | // when 317 | const result = foo.sumTwoNumbers(firstNumber, secondNumber); 318 | 319 | // then 320 | expect(result).toEqual(expectedResult); 321 | }); 322 | }); 323 | 324 | describe("that was found in the constructor code", () => { 325 | it("returns mocked value", () => { 326 | // given 327 | const expectedResult = "fakeValue"; 328 | when(mockedFoo.dynamicMethod(anything())).thenReturn(expectedResult); 329 | 330 | // when 331 | const result = foo.dynamicMethod("sample matching anything() matcher"); 332 | 333 | // then 334 | expect(result).toEqual(expectedResult); 335 | }); 336 | }); 337 | 338 | describe("that was found in the function code", () => { 339 | it("returns mocked value", () => { 340 | // given 341 | const expectedResult = "fakeValue"; 342 | when(mockedFoo.dynamicMethodInFunction(anything())).thenReturn(expectedResult); 343 | 344 | // when 345 | const result = foo.dynamicMethodInFunction("sample matching anything() matcher"); 346 | 347 | // then 348 | expect(result).toEqual(expectedResult); 349 | }); 350 | }); 351 | }); 352 | 353 | describe("calling method", () => { 354 | describe("that does not exists", () => { 355 | it("throws error", () => { 356 | // given 357 | 358 | // when 359 | let error = null; 360 | try { 361 | foo["notExistingMethod"](); 362 | } catch (e) { 363 | error = e; 364 | } 365 | 366 | // then 367 | expect(error).not.toBeNull(); 368 | }); 369 | }); 370 | }); 371 | }); 372 | -------------------------------------------------------------------------------- /test/utils/Bar.ts: -------------------------------------------------------------------------------- 1 | export class Bar { 2 | constructor(public sampleValue: string = "asd") { 3 | 4 | } 5 | 6 | public differentConvertNumberToString(value: number): string { 7 | return value.toString(); 8 | } 9 | 10 | public sumTwoNumbers(a: number, b: number): number { 11 | return a + b; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/utils/Foo.ts: -------------------------------------------------------------------------------- 1 | export class Foo { 2 | public dynamicMethod: Function; 3 | public dynamicMethodInFunction: Function; 4 | 5 | constructor() { 6 | this.dynamicMethod = () => "dynamicMethod"; 7 | } 8 | 9 | public getBar(): string { 10 | this.dynamicMethodInFunction = () => "dynamicMethodInFunction"; 11 | return "bar"; 12 | } 13 | 14 | public concatStringWithNumber(sampleString: string, sampleNumber: number): string { 15 | return sampleString + sampleNumber; 16 | } 17 | 18 | public convertNumberToString(value: number): string { 19 | return value.toString(); 20 | } 21 | 22 | public getStringById(value: number): string { 23 | return value.toString(); 24 | } 25 | 26 | public sumTwoNumbers(a: number, b: number): number { 27 | return a + b; 28 | } 29 | 30 | public sampleMethodWithOptionalArgument(a: number, b?: number): number { 31 | return a + (b ?? 0); 32 | } 33 | 34 | public sampleMethodWithTwoOptionalArguments(a?: number, b?: number): number { 35 | return (a ?? 0) + (b ?? 0); 36 | } 37 | 38 | public sampleMethodReturningPromise(value: string): Promise { 39 | return Promise.resolve(value); 40 | } 41 | 42 | public sampleMethodReturningVoidPromise(value: string): Promise { 43 | return Promise.resolve(); 44 | } 45 | 46 | public async sampleMethodReturningVoidPromiseWithoutParams(): Promise { 47 | return Promise.resolve(); 48 | } 49 | 50 | public async sampleMethodWithObjectArguments(obj: object): Promise { 51 | return Promise.resolve(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/utils/FooInterface.ts: -------------------------------------------------------------------------------- 1 | export interface FooInterface { 2 | sumByInterface(a: number, b: number): number; 3 | sampleFunctionWithoutArgs(): number; 4 | } 5 | -------------------------------------------------------------------------------- /test/utils/MockableFunctionsFinder.spec.ts: -------------------------------------------------------------------------------- 1 | import {MockableFunctionsFinder} from "../../src/utils/MockableFunctionsFinder"; 2 | 3 | describe("MockableFunctionsFinder", () => { 4 | describe("searching for method names in code", () => { 5 | it("returns all called and defined functions", () => { 6 | // given 7 | const code = getSampleCode(); 8 | 9 | // when 10 | const result = new MockableFunctionsFinder().find(code); 11 | 12 | // then 13 | expect(result).toContain("anonymousMethod"); 14 | expect(result).toContain("convertNumberToString"); 15 | }); 16 | 17 | it("should not find hasOwnProperty as it should not be mocked (because its used by mockito to evaluate properties)", () => { 18 | // given 19 | const code = getSampleCode(); 20 | 21 | // when 22 | const result = new MockableFunctionsFinder().find(code); 23 | 24 | // then 25 | expect(result["hasOwnProperty"] instanceof Function).toBeTruthy(); 26 | }); 27 | }); 28 | 29 | describe("searching for method names in complex class code", () => { 30 | const mockableFunctionsFinder = new MockableFunctionsFinder(); 31 | let mockableMethods: string[]; 32 | 33 | beforeEach(() => { 34 | // tslint:disable-next-line:no-eval 35 | const object = getSampleComplexClassCode(); 36 | mockableMethods = mockableFunctionsFinder.find(object); 37 | }); 38 | 39 | it("should find existing property method", () => { 40 | expect(mockableMethods).toContain('testMethod'); 41 | expect(mockableMethods).toContain('testMethod2'); 42 | expect(mockableMethods).toContain('testMethod3'); 43 | }); 44 | 45 | it("should find existing existing property accessors", () => { 46 | expect(mockableMethods).toContain('someValue'); 47 | }); 48 | 49 | it("should not find non existent property", () => { 50 | expect(mockableMethods).not.toContain("nonExistentProperty"); 51 | }); 52 | }); 53 | }); 54 | 55 | function getSampleCode(): string { 56 | // tslint:disable-next-line:no-eval 57 | return eval(` 58 | class Foo { 59 | constructor (temp) { 60 | this.anonymousMethod = function(arg) { 61 | console.log(arg); 62 | temp.hasOwnProperty("fakeProperty"); 63 | } 64 | } 65 | 66 | convertNumberToString(value) { 67 | return value.toString(); 68 | } 69 | } 70 | 71 | Foo; 72 | `); 73 | } 74 | 75 | function getSampleComplexClassCode() { 76 | // tslint:disable-next-line:no-eval 77 | return eval(` 78 | class InheritedTest { 79 | undefinedProperty = undefined; 80 | nullProperty = null; 81 | nanProperty = NaN; 82 | stringProperty = "stringProperty"; 83 | booleanProperty = true; 84 | testMethod = () => true; 85 | testMethod2 = function () { return true }; 86 | 87 | get someValue() { 88 | return "someValue"; 89 | } 90 | 91 | set someValue(newValue) { 92 | console.info("someValue set"); 93 | } 94 | } 95 | 96 | class Test extends InheritedTest { 97 | testMethod3() { 98 | return 'barbaz'; 99 | } 100 | } 101 | 102 | Test; 103 | `); 104 | } -------------------------------------------------------------------------------- /test/utils/ObjectInspector.spec.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import {ObjectInspector} from "../../src/utils/ObjectInspector"; 3 | 4 | describe("ObjectInspector", () => { 5 | describe("Providing prototype chain", () => { 6 | it("provides all objects from prototype chain as an array", () => { 7 | // given 8 | let methodNames: string[] = []; 9 | 10 | // when 11 | ObjectInspector.getObjectPrototypes(ChildClass.prototype).forEach((obj: any) => { 12 | methodNames = _.union(methodNames, Object.getOwnPropertyNames(obj)); 13 | }); 14 | 15 | // then 16 | expect(methodNames).toContain("sampleNumber"); 17 | expect(methodNames).toContain("sampleString"); 18 | expect(methodNames).toContain("sampleBoolean"); 19 | }); 20 | 21 | it("provides an empty array for non object passed (doesn't throw an exception)", () => { 22 | // given 23 | let called = false; 24 | 25 | // when 26 | ObjectInspector.getObjectPrototypes(null).forEach((obj: any) => { 27 | called = true; 28 | }); 29 | 30 | // then 31 | expect(called).toBeFalsy(); 32 | }); 33 | }); 34 | 35 | describe("Providing object own properties", () => { 36 | it("provides all object properties as an array", () => { 37 | // given 38 | let propertyNames: string[] = []; 39 | 40 | // when 41 | ObjectInspector.getObjectOwnPropertyNames(ParentClass.prototype).forEach((property: any) => { 42 | propertyNames = _.union(propertyNames, [property]); 43 | }); 44 | 45 | // then 46 | expect(propertyNames).toContain("sampleString"); 47 | expect(propertyNames).toContain("sampleBoolean"); 48 | }); 49 | 50 | it("provides an empty array for non object passed (doesn't throw en exception)", () => { 51 | // given 52 | let called = false; 53 | 54 | // when 55 | ObjectInspector.getObjectOwnPropertyNames(null).forEach((obj: any) => { 56 | called = true; 57 | }); 58 | 59 | // then 60 | expect(called).toBeFalsy(); 61 | }); 62 | }); 63 | }); 64 | 65 | class ParentClass { 66 | public sampleString(): string { 67 | return "sampleString"; 68 | } 69 | 70 | public sampleBoolean(): boolean { 71 | return true; 72 | } 73 | } 74 | 75 | class ChildClass extends ParentClass { 76 | public sampleNumber(): number { 77 | return 4; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/utils/Thenable.ts: -------------------------------------------------------------------------------- 1 | export abstract class ThenableClass { 2 | public then(): string { 3 | return "bob"; 4 | } 5 | 6 | public catch(): string { 7 | return "bob"; 8 | } 9 | } 10 | 11 | export interface ThenableInterface { 12 | then(): string; 13 | 14 | catch(): string; 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "outDir": "./lib", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "target": "es5", 8 | "removeComments": true, 9 | "sourceMap": true, 10 | "declaration": true, 11 | "strictNullChecks": true, 12 | "lib": [ 13 | "es5", 14 | "es6", 15 | "dom" 16 | ], 17 | "types": [ 18 | "node", 19 | "lodash", 20 | "jasmine" 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "array-type": false, 9 | "arrow-parens": [true, "ban-single-arg-parens"], 10 | "interface-name": false, 11 | "max-line-length": false, 12 | "member-ordering": [true, {"order": "fields-first"}], 13 | "no-console": [true, "log"], 14 | "no-empty": false, 15 | "no-empty-interface": false, 16 | "no-bitwise": false, 17 | "object-literal-sort-keys": false, 18 | "prefer-template": true, 19 | "semicolon": [true, "always", "ignore-bound-class-methods"], 20 | "ban-types": false, 21 | "no-string-literal": false, 22 | "max-classes-per-file": false 23 | }, 24 | "rulesDirectory": [] 25 | } 26 | --------------------------------------------------------------------------------