├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── default.yml │ └── publish.yml ├── .gitignore ├── .mocharc.json ├── .editorconfig ├── .prettierrc ├── .prettierrc.json ├── eslint.config.js ├── tsconfig.json ├── LICENSE ├── package.json ├── README.md └── src ├── main.ts └── test └── main_test.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 43081j 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /lib/ 3 | *.swp 4 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "bail": false, 3 | "recursive": true 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_size = 2 6 | indent_style = space 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | --- 2 | bracketSpacing: false 3 | printWidth: 80 4 | semi: true 5 | singleQuote: true 6 | tabWidth: 2 7 | trailingComma: none 8 | useTabs: false 9 | arrowParens: always -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "printWidth": 80 4 | "semi": true 5 | "singleQuote": true 6 | "tabWidth": 2 7 | "trailingComma": "none" 8 | "useTabs": false 9 | "arrowParens": "always" 10 | "quoteProps": "consistent" 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 10 8 | versioning-strategy: increase 9 | groups: 10 | production-dependencies: 11 | dependency-type: "production" 12 | development-dependencies: 13 | dependency-type: "development" 14 | - package-ecosystem: 'github-actions' 15 | directory: '/' 16 | schedule: 17 | interval: weekly 18 | -------------------------------------------------------------------------------- /.github/workflows/default.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x, 20.x, 24.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v6 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v6 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm ci 24 | - run: npm run build 25 | - run: npm run lint 26 | - run: npm test 27 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslintjs from '@eslint/js'; 2 | import {configs as tseslintConfigs} from 'typescript-eslint'; 3 | 4 | const {configs: eslintConfigs} = eslintjs; 5 | 6 | export default [ 7 | { 8 | ...eslintConfigs.recommended, 9 | files: ['src/**/*.ts'] 10 | }, 11 | ...tseslintConfigs.strict, 12 | { 13 | rules: { 14 | '@typescript-eslint/no-unused-vars': [ 15 | 'error', 16 | {varsIgnorePattern: '^[A-Z_]'} 17 | ] 18 | } 19 | }, 20 | { 21 | files: ['src/test/**/*.ts'], 22 | rules: { 23 | '@typescript-eslint/no-non-null-assertion': 'off' 24 | } 25 | } 26 | ]; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "esnext", 5 | "allowJs": false, 6 | "declaration": true, 7 | "outDir": "./lib", 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "strictNullChecks": true, 11 | "strictFunctionTypes": true, 12 | "strictBindCallApply": true, 13 | "strictPropertyInitialization": true, 14 | "noImplicitThis": true, 15 | "alwaysStrict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "moduleResolution": "node", 21 | "forceConsistentCasingInFileNames": true 22 | }, 23 | "include": ["src/**/*.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020 James Garbutt 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hanbi", 3 | "version": "0.0.0-dev", 4 | "description": "A small javascript library for stubbing and spying on methods/functions.", 5 | "type": "module", 6 | "types": "./lib/main.d.ts", 7 | "main": "./lib/main.js", 8 | "exports": { 9 | "default": "./lib/main.js" 10 | }, 11 | "files": [ 12 | "lib", 13 | "!lib/test" 14 | ], 15 | "scripts": { 16 | "clean": "premove ./lib", 17 | "lint": "eslint \"src/**/*.ts\"", 18 | "prebuild": "npm run clean", 19 | "build": "tsc", 20 | "test": "mocha lib", 21 | "format": "prettier --write \"src/**/*.ts\"", 22 | "prepare": "npm run build", 23 | "prepublishOnly": "npm run lint && npm run test" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/43081j/hanbi.git" 28 | }, 29 | "author": "James Garbutt (https://github.com/43081j)", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/43081j/hanbi/issues" 33 | }, 34 | "homepage": "https://github.com/43081j/hanbi#readme", 35 | "devDependencies": { 36 | "@eslint/js": "^9.39.1", 37 | "@types/chai": "^5.2.2", 38 | "@types/mocha": "^10.0.6", 39 | "chai": "^6.2.1", 40 | "esbuild": "^0.27.0", 41 | "eslint": "^9.39.1", 42 | "mocha": "^11.7.5", 43 | "premove": "^4.0.0", 44 | "prettier": "^3.7.3", 45 | "typescript": "^5.9.3", 46 | "typescript-eslint": "^8.48.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release (npm) 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v6 13 | - name: Setup Node 14 | uses: actions/setup-node@v6 15 | with: 16 | node-version: 20 17 | - name: Install Dependencies 18 | run: npm ci 19 | - name: Lint 20 | run: npm run lint 21 | - name: Build 22 | run: npm run build 23 | - name: Test 24 | run: npm test 25 | 26 | publish-npm: 27 | needs: build 28 | runs-on: ubuntu-latest 29 | permissions: 30 | id-token: write 31 | env: 32 | NODE_AUTH_TOKEN: ${{ secrets.npm_token }} 33 | steps: 34 | - uses: actions/checkout@v6 35 | - uses: actions/setup-node@v6 36 | with: 37 | node-version: 22.x 38 | registry-url: 'https://registry.npmjs.org' 39 | cache: 'npm' 40 | - run: npm ci 41 | - name: Build 42 | run: npm run build 43 | - run: npm version ${TAG_NAME} --git-tag-version=false 44 | env: 45 | TAG_NAME: ${{ github.ref_name }} 46 | - run: npm publish --provenance --access public --tag next 47 | if: "github.event.release.prerelease" 48 | - run: npm publish --provenance --access public 49 | if: "!github.event.release.prerelease" 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/v/hanbi)](https://www.npmjs.com/package/hanbi) 2 | 3 | # hanbi 4 | 5 | hanbi is a rather small and simple library for stubbing and spying on methods 6 | and functions in JavaScript tests. 7 | 8 | # Install 9 | 10 | ``` 11 | $ npm i -D hanbi 12 | ``` 13 | 14 | # Usage 15 | 16 | ## `spy()` 17 | 18 | Creates a single "spy" function to be used as input into some other 19 | function. 20 | 21 | ```ts 22 | const spy = hanbi.spy(); 23 | window.addEventListener('load', spy.handler); 24 | spy.called; // true once the event fires 25 | ``` 26 | 27 | ## `stub(fn)` 28 | 29 | Creates a wrapped version of a given function which tracks any calls. 30 | 31 | ```ts 32 | const fn = () => 5; 33 | const stub = hanbi.stub(fn); 34 | stub.handler(); // undefined 35 | stub.called; // true 36 | ``` 37 | 38 | ## `stubMethod(obj, method)` 39 | 40 | Replaces a given method on an object with a wrapped (stubbed) version of it. 41 | 42 | ```ts 43 | class Foo { 44 | myMethod() { 45 | return 5; 46 | } 47 | } 48 | const instance = new Foo(); 49 | const stub = hanbi.stubMethod(instance, 'myMethod'); 50 | instance.myMethod(); // undefined 51 | stub.called; // true 52 | ``` 53 | 54 | ## `restore()` 55 | 56 | Restores all stubs/spies to their original functions. 57 | 58 | ```ts 59 | class Foo { 60 | myMethod() { 61 | return 5; 62 | } 63 | } 64 | const instance = new Foo(); 65 | const stub = hanbi.stubMethod(instance, 'myMethod'); 66 | instance.myMethod(); // undefined 67 | restore(); 68 | instance.myMethod(); // 5 69 | ``` 70 | 71 | # Stub API 72 | 73 | Each of the above mentioned entry points returns a `Stub` which has 74 | several useful methods. 75 | 76 | ```ts 77 | class Stub { 78 | /** 79 | * Wrapped function 80 | */ 81 | handler; 82 | 83 | /** 84 | * Function to be called when stub is restored 85 | */ 86 | restoreCallback; 87 | 88 | /** 89 | * Original function 90 | */ 91 | original; 92 | 93 | /** 94 | * Whether or not this stub has been called 95 | */ 96 | called; 97 | 98 | /** 99 | * List of all calls this stub has received 100 | */ 101 | calls; 102 | 103 | /** 104 | * Retrieves an individual call 105 | * @param index Index of the call to retrieve 106 | * @return Call at the specified index 107 | */ 108 | getCall(index); 109 | 110 | /** 111 | * Retrieves the first call 112 | * @return Call object 113 | */ 114 | firstCall; 115 | 116 | /** 117 | * Retrieves the last call 118 | * @return Call object 119 | */ 120 | lastCall; 121 | 122 | /** 123 | * Number of times this stub has been called 124 | */ 125 | callCount; 126 | 127 | /** 128 | * Specifies the value this stub should return 129 | * @param val Value to return 130 | */ 131 | returns(val); 132 | 133 | /** 134 | * Specifies a function to call to retrieve the return value of this 135 | * stub 136 | * @param fn Function to call 137 | */ 138 | callsFake(fn); 139 | 140 | /** 141 | * Enables pass-through, in that the original function is called when 142 | * this stub is called. 143 | */ 144 | passThrough(); 145 | 146 | /** 147 | * Resets call state (e.g. call count, calls, etc.) 148 | */ 149 | reset(); 150 | 151 | /** 152 | * Restores this stub. 153 | * This behaviour differs depending on what created the stub. 154 | */ 155 | restore(); 156 | 157 | /** 158 | * Asserts that the stub was called with a set of arguments 159 | * @param args Arguments to assert for 160 | * @return Whether they were passed or not 161 | */ 162 | calledWith(...args); 163 | 164 | /** 165 | * Asserts that the stub returned a given value 166 | * @param val Value to check for 167 | * @return Whether the value was ever returned or not 168 | */ 169 | returned(val); 170 | } 171 | ``` 172 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | export type FunctionLike = (...args: any[]) => any; 3 | 4 | export interface StubCall { 5 | readonly args: TArgs; 6 | readonly returnValue?: TReturn; 7 | readonly thisValue: unknown; 8 | } 9 | 10 | /** 11 | * Represents a single stubbed function 12 | */ 13 | export class Stub { 14 | /** 15 | * Wrapped function 16 | */ 17 | public handler: T; 18 | 19 | /** 20 | * Function to be called when stub is restored 21 | */ 22 | public restoreCallback?: () => void; 23 | 24 | /** 25 | * Original function 26 | */ 27 | public original: T; 28 | 29 | protected _calls: Set, ReturnType>> = new Set(); 30 | protected _returnValue?: ReturnType; 31 | protected _returnFunction?: T; 32 | 33 | /** 34 | * Whether or not this stub has been called 35 | */ 36 | public get called(): boolean { 37 | return this._calls.size > 0; 38 | } 39 | 40 | /** 41 | * List of all calls this stub has received 42 | */ 43 | public get calls(): ReadonlySet, ReturnType>> { 44 | return this._calls; 45 | } 46 | 47 | /** 48 | * Retrieves an individual call 49 | * @param index Index of the call to retrieve 50 | * @return Call at the specified index 51 | */ 52 | public getCall(index: number): StubCall, ReturnType> { 53 | return [...this._calls][index]; 54 | } 55 | 56 | /** 57 | * Retrieves the first call 58 | * @return Call object 59 | */ 60 | public get firstCall(): StubCall, ReturnType> | undefined { 61 | return this.getCall(0); 62 | } 63 | 64 | /** 65 | * Retrieves the last call 66 | * @return Call object 67 | */ 68 | public get lastCall(): StubCall, ReturnType> | undefined { 69 | return this.getCall(this.callCount - 1); 70 | } 71 | 72 | /** 73 | * Number of times this stub has been called 74 | */ 75 | public get callCount(): number { 76 | return this._calls.size; 77 | } 78 | 79 | /** 80 | * Constructor 81 | * @param fn Function being stubbed 82 | */ 83 | public constructor(fn: T) { 84 | this.original = fn; 85 | // eslint-disable-next-line @typescript-eslint/no-this-alias 86 | const self = this; 87 | this.handler = function handler( 88 | this: unknown, 89 | ...args: Parameters 90 | ): ReturnType | undefined { 91 | // eslint-disable-next-line no-invalid-this 92 | return self._handleCall.call(self, this, args); 93 | } as T; 94 | } 95 | 96 | /** 97 | * Processes an individual call to this stub 98 | * @param thisValue Context of the call (`this`) 99 | * @param args Arguments passed when being called 100 | * @return Return value of this call 101 | */ 102 | protected _handleCall( 103 | thisValue: unknown, 104 | args: Parameters 105 | ): ReturnType | undefined { 106 | const returnValue = this._returnFunction 107 | ? this._returnFunction.apply(thisValue, args) 108 | : this._returnValue; 109 | this._calls.add({ 110 | args: args, 111 | thisValue, 112 | returnValue 113 | }); 114 | return returnValue; 115 | } 116 | 117 | /** 118 | * Specifies the value this stub should return 119 | * @param val Value to return 120 | * @return {this} 121 | */ 122 | public returns(val: ReturnType): this { 123 | this._returnFunction = undefined; 124 | this._returnValue = val; 125 | return this; 126 | } 127 | 128 | /** 129 | * Specifies a function to call to retrieve the return value of this 130 | * stub 131 | * @param fn Function to call 132 | * @return {this} 133 | */ 134 | public callsFake(fn: (...args: Parameters) => ReturnType): this { 135 | this._returnValue = undefined; 136 | this._returnFunction = fn as T; 137 | return this; 138 | } 139 | 140 | /** 141 | * Enables pass-through, in that the original function is called when 142 | * this stub is called. 143 | * @return {this} 144 | */ 145 | public passThrough(): this { 146 | this.callsFake(this.original); 147 | return this; 148 | } 149 | 150 | /** 151 | * Resets call state (e.g. call count, calls, etc.) 152 | */ 153 | public reset(): void { 154 | this._calls.clear(); 155 | } 156 | 157 | /** 158 | * Restores this stub. 159 | * This behaviour differs depending on what created the stub. 160 | */ 161 | public restore(): void { 162 | if (this.restoreCallback) { 163 | this.restoreCallback(); 164 | } 165 | } 166 | 167 | /** 168 | * Asserts that the stub was called with a set of arguments 169 | * @param args Arguments to assert for 170 | * @return Whether they were passed or not 171 | */ 172 | public calledWith(...args: Parameters): boolean { 173 | return [...this.calls].some( 174 | (call) => 175 | call.args.length === args.length && 176 | call.args.every((arg, idx) => args[idx] === arg) 177 | ); 178 | } 179 | 180 | /** 181 | * Asserts that the stub returned a given value 182 | * @param val Value to check for 183 | * @return Whether the value was ever returned or not 184 | */ 185 | public returned(val: ReturnType): boolean { 186 | return [...this.calls].some((call) => call.returnValue === val); 187 | } 188 | } 189 | 190 | export type StubbedFunction = T extends FunctionLike ? T : FunctionLike; 191 | 192 | const stubbedMethods = new Set<{restore(): void}>(); 193 | 194 | /** 195 | * Stubs a method of a given object. 196 | * @param obj Object the method belongs to 197 | * @param method Method name to stub 198 | * @return Stubbed method 199 | */ 200 | export function stubMethod( 201 | obj: TObj, 202 | method: TKey 203 | ): Stub> { 204 | const instance = new Stub>( 205 | obj[method] as StubbedFunction 206 | ); 207 | obj[method] = instance.handler as TObj[TKey]; 208 | instance.restoreCallback = (): void => { 209 | obj[method] = instance.original as TObj[TKey]; 210 | stubbedMethods.delete(instance); 211 | }; 212 | stubbedMethods.add(instance); 213 | return instance; 214 | } 215 | 216 | /** 217 | * Spies a method of a given object. 218 | * @param obj Object the method belongs to 219 | * @param method Method name to spy 220 | * @return Stubbed method 221 | */ 222 | export function spyMethod( 223 | obj: TObj, 224 | method: TKey 225 | ): Stub> { 226 | return stubMethod(obj, method).passThrough(); 227 | } 228 | 229 | /** 230 | * Stubs a given function. 231 | * @param fn Function to stub 232 | * @return Stubbed function 233 | */ 234 | export function stub(fn: T): Stub { 235 | const result = new Stub(fn); 236 | return result; 237 | } 238 | 239 | /** 240 | * Creates an anonymous spy. 241 | * @return Anonymous stub 242 | */ 243 | export function spy(): Stub { 244 | return new Stub((() => { 245 | return; 246 | }) as T); 247 | } 248 | 249 | /** 250 | * Restores all tracked stubs at once. 251 | */ 252 | export function restore(): void { 253 | for (const stub of stubbedMethods) { 254 | stub.restore(); 255 | } 256 | 257 | stubbedMethods.clear(); 258 | } 259 | -------------------------------------------------------------------------------- /src/test/main_test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import * as lib from '../main.js'; 3 | 4 | describe('Stub', () => { 5 | describe('called', () => { 6 | it('should determine if stub has been called', () => { 7 | const stub = lib.spy(); 8 | expect(stub.called).to.equal(false); 9 | stub.handler(); 10 | expect(stub.called).to.equal(true); 11 | }); 12 | }); 13 | 14 | describe('returned', () => { 15 | it('should determine if stub returned a given value', () => { 16 | const stub = lib.stub((x: number) => x + x); 17 | stub.passThrough(); 18 | stub.handler(5); 19 | expect(stub.returned(10)).to.equal(true); 20 | expect(stub.returned(5)).to.equal(false); 21 | }); 22 | }); 23 | 24 | describe('calledWith', () => { 25 | it('should determine if stub was called with args', () => { 26 | const stub = lib.spy(); 27 | stub.handler(1, 2); 28 | expect(stub.calledWith(1, 2)).to.equal(true); 29 | expect(stub.calledWith(3, 4)).to.equal(false); 30 | }); 31 | 32 | it('should support optional parameters', () => { 33 | const stub = lib.stub((x: number, y?: number) => [x, y]); 34 | 35 | stub.handler(1, 2); 36 | stub.handler(1); 37 | 38 | expect(stub.calledWith(1, 2)).to.equal(true); 39 | expect(stub.calledWith(1)).to.equal(true); 40 | expect(stub.calledWith(1, 3)).to.equal(false); 41 | }); 42 | }); 43 | 44 | describe('firstCall', () => { 45 | it('should retrieve the first call', () => { 46 | const stub = lib.spy(); 47 | stub.handler(1); 48 | stub.handler(2); 49 | expect(stub.firstCall).to.deep.equal({ 50 | args: [1], 51 | thisValue: stub, 52 | returnValue: undefined 53 | }); 54 | }); 55 | }); 56 | 57 | describe('lastCall', () => { 58 | it('should retrieve the last call', () => { 59 | const stub = lib.spy(); 60 | stub.handler(1); 61 | stub.handler(2); 62 | expect(stub.lastCall).to.deep.equal({ 63 | args: [2], 64 | thisValue: stub, 65 | returnValue: undefined 66 | }); 67 | }); 68 | }); 69 | 70 | describe('getCall', () => { 71 | it('should retrieve a specific call', () => { 72 | const stub = lib.spy(); 73 | stub.handler(1); 74 | stub.handler(2); 75 | 76 | expect(stub.getCall(0)).to.deep.equal({ 77 | args: [1], 78 | thisValue: stub, 79 | returnValue: undefined 80 | }); 81 | expect(stub.getCall(1)).to.deep.equal({ 82 | args: [2], 83 | thisValue: stub, 84 | returnValue: undefined 85 | }); 86 | }); 87 | }); 88 | 89 | describe('calls', () => { 90 | it('should return all calls tracked', () => { 91 | const stub = lib.spy(); 92 | expect(stub.calls.size).to.equal(0); 93 | 94 | stub.handler(1, 2, 3); 95 | stub.handler('foo'); 96 | 97 | expect([...stub.calls]).to.deep.equal([ 98 | { 99 | args: [1, 2, 3], 100 | thisValue: stub, 101 | returnValue: undefined 102 | }, 103 | { 104 | args: ['foo'], 105 | thisValue: stub, 106 | returnValue: undefined 107 | } 108 | ]); 109 | }); 110 | 111 | it('should track return values', () => { 112 | const stub = lib.spy(); 113 | stub.returns(1209); 114 | stub.handler(); 115 | expect([...stub.calls]).to.deep.equal([ 116 | { 117 | args: [], 118 | thisValue: stub, 119 | returnValue: 1209 120 | } 121 | ]); 122 | }); 123 | }); 124 | 125 | describe('callCount', () => { 126 | it('should return number of times stub was called', () => { 127 | const stub = lib.spy(); 128 | expect(stub.callCount).to.equal(0); 129 | stub.handler(); 130 | expect(stub.callCount).to.equal(1); 131 | stub.handler(); 132 | expect(stub.callCount).to.equal(2); 133 | }); 134 | }); 135 | 136 | describe('returns', () => { 137 | it('should set return value', () => { 138 | const stub = lib.spy(); 139 | stub.returns(1209); 140 | expect(stub.handler()).to.equal(1209); 141 | stub.returns('foo'); 142 | expect(stub.handler()).to.equal('foo'); 143 | }); 144 | }); 145 | 146 | describe('callsFake', () => { 147 | it('should set return function', () => { 148 | const stub = lib.spy(); 149 | stub.callsFake(() => { 150 | return 1002; 151 | }); 152 | expect(stub.handler()).to.equal(1002); 153 | }); 154 | 155 | it('should be passed args', () => { 156 | const stub = lib.spy(); 157 | stub.callsFake((x, y, z) => { 158 | return [x, y, z]; 159 | }); 160 | expect(stub.handler(1, 2, 3)).to.deep.equal([1, 2, 3]); 161 | }); 162 | }); 163 | 164 | describe('passThrough', () => { 165 | it('should pass through to original function', () => { 166 | const Klass = class { 167 | public someMethod(): number { 168 | return 105; 169 | } 170 | }; 171 | const instance = new Klass(); 172 | const stub = lib.stubMethod(instance, 'someMethod'); 173 | 174 | expect(stub.handler()).to.equal(undefined); 175 | stub.passThrough(); 176 | expect(stub.handler()).to.equal(105); 177 | }); 178 | }); 179 | 180 | describe('reset', () => { 181 | it('should reset call state', () => { 182 | const stub = lib.spy(); 183 | stub.handler(); 184 | 185 | expect(stub.callCount).to.equal(1); 186 | expect(stub.called).to.equal(true); 187 | expect([...stub.calls]).to.deep.equal([ 188 | { 189 | args: [], 190 | thisValue: stub, 191 | returnValue: undefined 192 | } 193 | ]); 194 | 195 | stub.reset(); 196 | 197 | expect(stub.callCount).to.equal(0); 198 | expect(stub.called).to.equal(false); 199 | expect([...stub.calls]).to.deep.equal([]); 200 | }); 201 | }); 202 | 203 | describe('restore', () => { 204 | it('should call restore callback', () => { 205 | const stub = lib.spy(); 206 | let called = false; 207 | stub.restoreCallback = (): void => { 208 | called = true; 209 | }; 210 | 211 | stub.restore(); 212 | expect(called).to.equal(true); 213 | }); 214 | }); 215 | }); 216 | 217 | describe('restore', () => { 218 | it('should restore all stubs', () => { 219 | const Klass = class { 220 | public someMethod(): number { 221 | return 105; 222 | } 223 | }; 224 | const instance1 = new Klass(); 225 | const instance2 = new Klass(); 226 | 227 | const original1 = instance1.someMethod; 228 | const original2 = instance2.someMethod; 229 | 230 | const stub1 = lib.stubMethod(instance1, 'someMethod'); 231 | const stub2 = lib.stubMethod(instance2, 'someMethod'); 232 | 233 | expect(instance1.someMethod).to.equal(stub1.handler); 234 | expect(instance2.someMethod).to.equal(stub2.handler); 235 | 236 | lib.restore(); 237 | 238 | expect(instance1.someMethod).to.equal(original1); 239 | expect(instance2.someMethod).to.equal(original2); 240 | }); 241 | }); 242 | 243 | describe('spy', () => { 244 | it('should create an anonymous stub', () => { 245 | const spy = lib.spy(); 246 | expect(spy.handler(1, 2, 3)).to.equal(undefined); 247 | expect(spy.handler()).to.equal(undefined); 248 | expect([...spy.calls]).to.deep.equal([ 249 | { 250 | args: [1, 2, 3], 251 | thisValue: spy, 252 | returnValue: undefined 253 | }, 254 | { 255 | args: [], 256 | thisValue: spy, 257 | returnValue: undefined 258 | } 259 | ]); 260 | }); 261 | 262 | it('should create a typed stub', () => { 263 | const spy = lib.spy<(x: number) => string>(); 264 | const fn: (x: number) => string = spy.handler; 265 | expect(fn(5)).to.equal(undefined); 266 | }); 267 | }); 268 | 269 | describe('stub', () => { 270 | it('should stub a function', () => { 271 | const fn = (): number => 1500; 272 | const stub = lib.stub(fn); 273 | expect(stub.handler()).to.equal(undefined); 274 | expect(stub.original).to.equal(fn); 275 | expect([...stub.calls]).to.deep.equal([ 276 | { 277 | args: [], 278 | thisValue: stub, 279 | returnValue: undefined 280 | } 281 | ]); 282 | }); 283 | 284 | it('should track thisValue', () => { 285 | const context = {}; 286 | const fn = function fn(this: unknown): unknown { 287 | // eslint-disable-next-line no-invalid-this 288 | return this; 289 | }; 290 | 291 | const stub = lib.stub(fn); 292 | stub.passThrough(); 293 | 294 | const result = stub.handler.call(context); 295 | expect(result).to.equal(context); 296 | expect(stub.lastCall!.thisValue).to.equal(context); 297 | }); 298 | }); 299 | 300 | describe('stubMethod', () => { 301 | it('should stub given method', () => { 302 | const Klass = class { 303 | public someMethod(): number { 304 | return 105; 305 | } 306 | }; 307 | const instance = new Klass(); 308 | const original = instance.someMethod; 309 | const stub = lib.stubMethod(instance, 'someMethod'); 310 | 311 | expect(stub.handler()).to.equal(undefined); 312 | expect(stub.original).to.equal(original); 313 | expect(instance.someMethod).to.equal(stub.handler); 314 | expect([...stub.calls]).to.deep.equal([ 315 | { 316 | args: [], 317 | thisValue: stub, 318 | returnValue: undefined 319 | } 320 | ]); 321 | }); 322 | 323 | it('should track thisValue', () => { 324 | const Klass = class { 325 | public someMethod(): number { 326 | return 105; 327 | } 328 | }; 329 | const instance = new Klass(); 330 | const stub = lib.stubMethod(instance, 'someMethod'); 331 | 332 | instance.someMethod(); 333 | expect([...stub.calls]).to.deep.equal([ 334 | { 335 | args: [], 336 | thisValue: instance, 337 | returnValue: undefined 338 | } 339 | ]); 340 | }); 341 | 342 | describe('restore', () => { 343 | it('should restore original method', () => { 344 | const Klass = class { 345 | public someMethod(): number { 346 | return 105; 347 | } 348 | }; 349 | const instance = new Klass(); 350 | const original = instance.someMethod; 351 | const stub = lib.stubMethod(instance, 'someMethod'); 352 | 353 | expect(instance.someMethod).to.equal(stub.handler); 354 | 355 | stub.restore(); 356 | 357 | expect(instance.someMethod).to.equal(original); 358 | }); 359 | }); 360 | }); 361 | --------------------------------------------------------------------------------