├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── index.ts ├── package-lock.json ├── package.json ├── rollup.js ├── test.ts └── tsconfig.json /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | node-version: [12.x, 14.x] 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: Use Node.js ${{ matrix.node-version }} 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: ${{ matrix.node-version }} 15 | - name: npm install, build, and test 16 | run: | 17 | npm install 18 | npm run build --if-present 19 | npm test 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .idea 107 | 108 | index.js 109 | index.cjs 110 | index.d.ts 111 | test.js 112 | test.d.ts 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 RENARD Laurent 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 | # sbuts 2 | 3 | micro stubbing library for javascript environments 4 | 5 | > I like big sbuts and I cannot lie. Mc Hammer 6 | 7 | > A method stub or simply stub in software development is a piece of code used to stand in for some other programming functionality. Wikipedia 8 | 9 | ## Install 10 | 11 | ``npm install --save-dev sbuts`` (or from CDN etc) 12 | 13 | The module has a single export 14 | 15 | ```Javascript 16 | import stub from 'sbuts'; 17 | // etc 18 | ``` 19 | 20 | ## Usage 21 | 22 | The low level API always come back to a generator to get the best flexibility, but higher level syntax sugar is also available 23 | 24 | 1. Stub produces a value 25 | 26 | ```Javascript 27 | let fn = stub(function *(){ 28 | yield 42; 29 | }); 30 | 31 | // or 32 | 33 | fn = stub(42); 34 | 35 | // or 36 | 37 | fn = stub().return(42); 38 | 39 | fn('whatever') === 42; // > true 40 | ``` 41 | 2. Stub produces a list of values when called multiple times 42 | 43 | ```Javascript 44 | fn = stub(function *(){ 45 | yield 42; 46 | yield 'woot'; 47 | }); 48 | 49 | // or 50 | 51 | fn = stub(42,'woot'); 52 | 53 | // or 54 | 55 | fn = stub() 56 | .return(42) 57 | .return('woot'); // can be called later 58 | 59 | fn('foo') === 42; // > true 60 | fn(66) === 'woot'; // > true 61 | ``` 62 | 63 | 3. Stub throws an error 64 | 65 | ```Javascript 66 | fn = stub(function *(){ 67 | throw new Error('some error'); 68 | }); 69 | 70 | or 71 | 72 | fn = stub().throw(new Error('some error')); 73 | 74 | try{ 75 | fn(); 76 | } catch(e){ 77 | e.message === 'some error'; // > true 78 | } 79 | ``` 80 | 81 | 4. stub async function 82 | 83 | ```Javascript 84 | fn = stub(async function *(){ 85 | yield 42; 86 | }); 87 | 88 | // or 89 | 90 | fn = stub(function * (){ 91 | yield Promise.resolve(42); 92 | }); 93 | 94 | // or 95 | 96 | fn = stub(Promise.resolve(42)); 97 | 98 | // or 99 | 100 | fn = stub().resolve(42); 101 | 102 | // or ... probably other ways but you get the idea 103 | 104 | val === await fn() // > true 105 | ``` 106 | 107 | 5. stub async error 108 | 109 | ```Javascript 110 | fn = stub(async function *(){ 111 | throw new Error('some error'); 112 | }); 113 | 114 | // or 115 | 116 | fn = stub(function * (){ 117 | yield Promise.reject(new Error('some error')); 118 | }); 119 | 120 | // or 121 | 122 | fn = stub(Promise.reject(new Error('some message'))); 123 | 124 | // or 125 | 126 | fn = stub().reject(new Error('some message')); 127 | 128 | // or ... probably other ways but you get the idea 129 | 130 | try { 131 | await fn(`woot bim`) 132 | } catch (e) { 133 | e.message === `some message` // > true 134 | } 135 | ``` 136 | 137 | ## Assert on arguments 138 | 139 | Every call captures the arguments, so you can make some assertions on how the stub was called: 140 | 141 | ```Javascript 142 | const fn = stub(function *(){ 143 | let i = 0; 144 | while(true){ 145 | yield ++i; 146 | } 147 | }); 148 | 149 | fn(null, 42); 150 | fn({foo:'bar'}); 151 | 152 | fn.calls; // > [ [null, 42], [ {foo:'bar'} ] ] 153 | ``` 154 | 155 | 156 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | const STUB_EXHAUSTED_ERROR = `stub exhausted, call not expected`; 2 | 3 | interface CallList { 4 | calls: T[], 5 | callCount: number; 6 | called: boolean; 7 | } 8 | 9 | type StubbedFn = (...args: unknown[]) => unknown; 10 | type Stub = T & CallList> 11 | type LazyExtension = { 12 | return>(value: K): LazyStub; 13 | resolve(value: K): LazyStub; 14 | reject(reason: K): LazyStub; 15 | throw(val: K): LazyStub; 16 | }; 17 | type LazyStub = Stub & LazyExtension 18 | 19 | const descriptor = (calls: unknown[]) => ({ 20 | calls: { 21 | value: calls 22 | }, 23 | callCount: { 24 | get() { 25 | return calls.length; 26 | } 27 | }, 28 | called: { 29 | get() { 30 | return calls.length > 0; 31 | } 32 | } 33 | }); 34 | 35 | const fromAsyncGenerator = Promise>(generator: AsyncGenerator): Stub => { 36 | const calls: Parameters[] = []; 37 | return Object.defineProperties(fn, descriptor(calls)); 38 | 39 | async function fn(...args: Parameters) { 40 | calls.push(args); 41 | const {value, done} = await generator.next(); 42 | if (done) { 43 | throw new Error(STUB_EXHAUSTED_ERROR); 44 | } 45 | return value; 46 | } 47 | }; 48 | 49 | const fromGenerator = (generator: Generator): Stub => { 50 | const calls: Parameters[] = []; 51 | return Object.defineProperties(fn, descriptor(calls)); 52 | 53 | function fn(...args: Parameters): ReturnType { 54 | calls.push(args); 55 | const {value, done} = generator.next(); 56 | if (done) { 57 | throw new Error(STUB_EXHAUSTED_ERROR); 58 | } 59 | return value; 60 | } 61 | }; 62 | 63 | enum CallType { 64 | ERROR = 'error', 65 | VALUE = 'value' 66 | } 67 | 68 | interface CallDefinition { 69 | type: CallType, 70 | value: T 71 | } 72 | 73 | const fromVoid = (): LazyStub => { 74 | const queue: CallDefinition[] = []; 75 | const generator = (function* () { 76 | while (true) { 77 | const next = queue.shift(); 78 | if (!next) { 79 | break; 80 | } 81 | const {type, value} = next; 82 | if (type === CallType.ERROR) { 83 | throw value; 84 | } 85 | yield value; 86 | } 87 | })(); 88 | 89 | const fn = fromGenerator(generator); 90 | const queueCall = (value: K | Error, type = CallType.VALUE) => { 91 | queue.push({value, type}); 92 | return fn as LazyStub; 93 | }; 94 | 95 | const extension: LazyExtension = { 96 | return: queueCall, 97 | resolve: (val: K) => queueCall>(Promise.resolve(val)), 98 | reject: (val: K) => queueCall>(Promise.reject(val)), 99 | throw: (val: K) => queueCall(val, CallType.ERROR) 100 | }; 101 | 102 | return Object.assign(fn, extension); 103 | }; 104 | 105 | function stub(generator: Generator): Stub; 106 | function stub(generator: AsyncGenerator): Stub; 107 | function stub(): LazyStub; 108 | function stub(...args:unknown[]): Stub; 109 | function stub unknown>(...args: unknown[]) { 110 | const [input] = args; 111 | 112 | if (typeof input === 'function') { 113 | const generatorObject = input(); 114 | return generatorObject[Symbol.asyncIterator] ? 115 | fromAsyncGenerator(generatorObject) : 116 | fromGenerator(generatorObject); 117 | } 118 | 119 | return args.length === 0 ? 120 | fromVoid() : 121 | fromGenerator(function* () { 122 | yield* args; 123 | }()); 124 | } 125 | 126 | export default stub 127 | 128 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sbuts", 3 | "version": "0.4.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@nodelib/fs.scandir": { 8 | "version": "2.1.4", 9 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", 10 | "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", 11 | "dev": true, 12 | "requires": { 13 | "@nodelib/fs.stat": "2.0.4", 14 | "run-parallel": "^1.1.9" 15 | } 16 | }, 17 | "@nodelib/fs.stat": { 18 | "version": "2.0.4", 19 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", 20 | "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", 21 | "dev": true 22 | }, 23 | "@nodelib/fs.walk": { 24 | "version": "1.2.6", 25 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", 26 | "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", 27 | "dev": true, 28 | "requires": { 29 | "@nodelib/fs.scandir": "2.1.4", 30 | "fastq": "^1.6.0" 31 | } 32 | }, 33 | "arg": { 34 | "version": "5.0.0", 35 | "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.0.tgz", 36 | "integrity": "sha512-4P8Zm2H+BRS+c/xX1LrHw0qKpEhdlZjLCgWy+d78T9vqa2Z2SiD2wMrYuWIAFy5IZUD7nnNXroRttz+0RzlrzQ==", 37 | "dev": true 38 | }, 39 | "braces": { 40 | "version": "3.0.2", 41 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 42 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 43 | "dev": true, 44 | "requires": { 45 | "fill-range": "^7.0.1" 46 | } 47 | }, 48 | "buffer-from": { 49 | "version": "1.1.1", 50 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 51 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 52 | "dev": true 53 | }, 54 | "create-require": { 55 | "version": "1.1.1", 56 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", 57 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", 58 | "dev": true 59 | }, 60 | "diff": { 61 | "version": "5.0.0", 62 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", 63 | "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", 64 | "dev": true 65 | }, 66 | "fast-glob": { 67 | "version": "3.2.5", 68 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", 69 | "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", 70 | "dev": true, 71 | "requires": { 72 | "@nodelib/fs.stat": "^2.0.2", 73 | "@nodelib/fs.walk": "^1.2.3", 74 | "glob-parent": "^5.1.0", 75 | "merge2": "^1.3.0", 76 | "micromatch": "^4.0.2", 77 | "picomatch": "^2.2.1" 78 | } 79 | }, 80 | "fastq": { 81 | "version": "1.11.0", 82 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", 83 | "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", 84 | "dev": true, 85 | "requires": { 86 | "reusify": "^1.0.4" 87 | } 88 | }, 89 | "fill-range": { 90 | "version": "7.0.1", 91 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 92 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 93 | "dev": true, 94 | "requires": { 95 | "to-regex-range": "^5.0.1" 96 | } 97 | }, 98 | "fsevents": { 99 | "version": "2.3.2", 100 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 101 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 102 | "dev": true, 103 | "optional": true 104 | }, 105 | "glob-parent": { 106 | "version": "5.1.2", 107 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 108 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 109 | "dev": true, 110 | "requires": { 111 | "is-glob": "^4.0.1" 112 | } 113 | }, 114 | "is-extglob": { 115 | "version": "2.1.1", 116 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 117 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 118 | "dev": true 119 | }, 120 | "is-glob": { 121 | "version": "4.0.1", 122 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", 123 | "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", 124 | "dev": true, 125 | "requires": { 126 | "is-extglob": "^2.1.1" 127 | } 128 | }, 129 | "is-number": { 130 | "version": "7.0.0", 131 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 132 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 133 | "dev": true 134 | }, 135 | "kleur": { 136 | "version": "4.1.4", 137 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz", 138 | "integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==", 139 | "dev": true 140 | }, 141 | "make-error": { 142 | "version": "1.3.6", 143 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 144 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 145 | "dev": true 146 | }, 147 | "merge2": { 148 | "version": "1.4.1", 149 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 150 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 151 | "dev": true 152 | }, 153 | "micromatch": { 154 | "version": "4.0.4", 155 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", 156 | "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", 157 | "dev": true, 158 | "requires": { 159 | "braces": "^3.0.1", 160 | "picomatch": "^2.2.3" 161 | } 162 | }, 163 | "picomatch": { 164 | "version": "2.2.3", 165 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", 166 | "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", 167 | "dev": true 168 | }, 169 | "pta": { 170 | "version": "0.2.2", 171 | "resolved": "https://registry.npmjs.org/pta/-/pta-0.2.2.tgz", 172 | "integrity": "sha512-wxuf1R00DoB3+ejXrRJRbHU3ixUBojJH1Afc2CTflsLUX/XUkVTY9b6NL/MyB1GEIxy4hmvEhx+IHZuIBXVimA==", 173 | "dev": true, 174 | "requires": { 175 | "arg": "~5.0.0", 176 | "diff": "~5.0.0", 177 | "fast-glob": "~3.2.5", 178 | "kleur": "~4.1.4", 179 | "zora": "~4.0.2" 180 | } 181 | }, 182 | "queue-microtask": { 183 | "version": "1.2.3", 184 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 185 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 186 | "dev": true 187 | }, 188 | "reusify": { 189 | "version": "1.0.4", 190 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 191 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 192 | "dev": true 193 | }, 194 | "rollup": { 195 | "version": "2.45.1", 196 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.45.1.tgz", 197 | "integrity": "sha512-vPD+JoDj3CY8k6m1bLcAFttXMe78P4CMxoau0iLVS60+S9kLsv2379xaGy4NgYWu+h2WTlucpoLPAoUoixFBag==", 198 | "dev": true, 199 | "requires": { 200 | "fsevents": "~2.3.1" 201 | } 202 | }, 203 | "run-parallel": { 204 | "version": "1.2.0", 205 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 206 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 207 | "dev": true, 208 | "requires": { 209 | "queue-microtask": "^1.2.2" 210 | } 211 | }, 212 | "source-map": { 213 | "version": "0.6.1", 214 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 215 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 216 | "dev": true 217 | }, 218 | "source-map-support": { 219 | "version": "0.5.19", 220 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 221 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 222 | "dev": true, 223 | "requires": { 224 | "buffer-from": "^1.0.0", 225 | "source-map": "^0.6.0" 226 | } 227 | }, 228 | "to-regex-range": { 229 | "version": "5.0.1", 230 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 231 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 232 | "dev": true, 233 | "requires": { 234 | "is-number": "^7.0.0" 235 | } 236 | }, 237 | "ts-node": { 238 | "version": "9.1.1", 239 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", 240 | "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", 241 | "dev": true, 242 | "requires": { 243 | "arg": "^4.1.0", 244 | "create-require": "^1.1.0", 245 | "diff": "^4.0.1", 246 | "make-error": "^1.1.1", 247 | "source-map-support": "^0.5.17", 248 | "yn": "3.1.1" 249 | }, 250 | "dependencies": { 251 | "arg": { 252 | "version": "4.1.3", 253 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 254 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 255 | "dev": true 256 | }, 257 | "diff": { 258 | "version": "4.0.2", 259 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 260 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 261 | "dev": true 262 | } 263 | } 264 | }, 265 | "typescript": { 266 | "version": "4.2.4", 267 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", 268 | "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", 269 | "dev": true 270 | }, 271 | "yn": { 272 | "version": "3.1.1", 273 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 274 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 275 | "dev": true 276 | }, 277 | "zora": { 278 | "version": "4.0.2", 279 | "resolved": "https://registry.npmjs.org/zora/-/zora-4.0.2.tgz", 280 | "integrity": "sha512-dSJlPNzWOVJxLcgItXSAPPszatQ7SJctQQthuFfkRxyrhciMzH5GIdTYJYwpw/vx9DMbwRKocVJgoyj8UX2NUw==", 281 | "dev": true 282 | } 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sbuts", 3 | "version": "0.4.1", 4 | "description": "tiny stub library for nodejs and the browser", 5 | "main": "./dist/index.cjs", 6 | "module": "./dist/index.js", 7 | "types": "./index.ts", 8 | "type": "module", 9 | "exports": { 10 | "import": "./dist/index.js", 11 | "require": "./dist/index.cjs" 12 | }, 13 | "scripts": { 14 | "test": "ts-node ./node_modules/.bin/pta --module-loader=cjs test.ts", 15 | "build:tsc": "tsc", 16 | "build": "npm run build:tsc && rollup -c rollup.js", 17 | "prepublish": "npm run build", 18 | "dev": "tsc -w" 19 | }, 20 | "files": [ 21 | "dist/index.js", 22 | "dist/index.cjs", 23 | "index.ts" 24 | ], 25 | "keywords": [ 26 | "stub", 27 | "test", 28 | "testing", 29 | "mock", 30 | "zora", 31 | "tdd" 32 | ], 33 | "author": "Laurent RENARD", 34 | "license": "MIT", 35 | "devDependencies": { 36 | "pta": "~0.2.2", 37 | "rollup": "~2.45.1", 38 | "ts-node": "~9.1.1", 39 | "typescript": "~4.2.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /rollup.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: 'dist/index.js', 3 | output: [{ 4 | file: 'dist/index.cjs', 5 | format: 'cjs' 6 | }], 7 | }; 8 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import stub from './index'; 2 | import {Assert} from 'zora'; 3 | 4 | export default (t: Assert) => { 5 | t.test(`create stub with generator`, t => { 6 | 7 | t.test('when a value is yielded the stub should return that value', t => { 8 | const fn = stub(function* () { 9 | yield 'woot'; 10 | }); 11 | 12 | t.eq(fn('hello world'), 'woot'); 13 | t.eq(fn.calls, [['hello world']]); 14 | }); 15 | 16 | t.test(`when several values are yielded, the stub should sequentially return the values`, t => { 17 | const fn = stub(function* () { 18 | yield 'woot'; 19 | yield 5; 20 | yield {foo: 'bar'}; 21 | }); 22 | 23 | t.eq(fn('first', 'arg'), 'woot'); 24 | t.eq(fn(2), 5); 25 | t.eq(fn(null, 'arg'), {foo: 'bar'}); 26 | t.eq(fn.calls, [ 27 | ['first', 'arg'], 28 | [2], 29 | [null, 'arg'] 30 | ]); 31 | }); 32 | 33 | t.test(`when an error is thrown, the stub should throw`, t => { 34 | const fn = stub(function* () { 35 | throw new Error('message'); 36 | }); 37 | 38 | try { 39 | fn('abc'); 40 | t.fail('should not get here'); 41 | } catch (e) { 42 | t.eq(e.message, 'message'); 43 | t.eq(fn.calls, [ 44 | ['abc'] 45 | ]); 46 | } 47 | }); 48 | 49 | t.test(`when a a value is yielded in an async generator, the stub should behave as an async function`, async t => { 50 | const fn = stub(async function* () { 51 | yield [1, 2, 3]; 52 | }); 53 | 54 | const val = await fn(42); 55 | 56 | t.eq(val, [1, 2, 3]); 57 | t.eq(fn.calls, [[42]]); 58 | }); 59 | 60 | t.test(`when a promise is yielded, the stub should behave as an async function`, async t => { 61 | const fn = stub(function* () { 62 | yield Promise.resolve([1, 2, 3]); 63 | }); 64 | 65 | const val = await fn(42); 66 | 67 | t.eq(val, [1, 2, 3]); 68 | t.eq(fn.calls, [[42]]); 69 | }); 70 | 71 | t.test(`when a promise rejection is yielded, the stub should behave as an async function`, async t => { 72 | const fn = stub(function* () { 73 | yield Promise.reject(new Error('some message')); 74 | }); 75 | 76 | try { 77 | // @ts-ignore 78 | const val = await fn(42); 79 | t.fail(`should not get here`); 80 | } catch (e) { 81 | t.eq(e.message, 'some message'); 82 | t.eq(fn.calls, [[42]]); 83 | } 84 | }); 85 | 86 | t.test(`when an error is thrown in a async generator, the stub should behave as an async function`, async t => { 87 | const fn = stub(async function* () { 88 | throw new Error('some message'); 89 | }); 90 | try { 91 | await fn('a', 2); 92 | t.fail(`should not get here`); 93 | } catch (e) { 94 | t.eq(e.message, 'some message'); 95 | t.eq(fn.calls, [ 96 | ['a', 2] 97 | ]); 98 | } 99 | }); 100 | 101 | t.test(`stub should throw if a it is called more than expected`, t => { 102 | const fn = stub(function* () { 103 | yield 'bim'; 104 | }); 105 | 106 | const val = fn('arg'); 107 | t.eq(val, 'bim'); 108 | try { 109 | fn('out of bound'); 110 | t.fail(`should not get here`); 111 | } catch (e) { 112 | t.eq(e.message, `stub exhausted, call not expected`); 113 | t.eq(fn.calls, [ 114 | ['arg'], 115 | ['out of bound'] 116 | ]); 117 | } 118 | }); 119 | 120 | }); 121 | 122 | t.test(`create stub with provided value`, t => { 123 | 124 | t.test('The stub should return that value', t => { 125 | const fn = stub('woot'); 126 | 127 | t.eq(fn('hello world'), 'woot'); 128 | t.eq(fn.calls, [ 129 | ['hello world'] 130 | ]); 131 | }); 132 | 133 | t.test(`when several values are provided, the stub should sequentially return the values`, t => { 134 | const fn = stub('woot', 5, {foo: 'bar'}); 135 | 136 | t.eq(fn('first', 'arg'), 'woot'); 137 | t.eq(fn(2), 5); 138 | t.eq(fn(null, 'arg'), {foo: 'bar'}); 139 | t.eq(fn.calls, [ 140 | ['first', 'arg'], 141 | [2], 142 | [null, 'arg'] 143 | ]); 144 | }); 145 | 146 | t.test(`When the value is a Promise, the stub should behave as an async function`, async t => { 147 | const fn = stub(Promise.resolve([1, 2, 3])); 148 | 149 | const val = await fn(42); 150 | 151 | t.eq(val, [1, 2, 3]); 152 | t.eq(fn.calls, [[42]]); 153 | }); 154 | 155 | t.test(`when a promise rejection is passed, the stub should behave as an async function`, async t => { 156 | const fn = stub(Promise.reject(new Error(`some message`))); 157 | try { 158 | await fn('a', 2); 159 | t.fail(`should not get here`); 160 | } catch (e) { 161 | t.eq(e.message, 'some message'); 162 | t.eq(fn.calls, [ 163 | ['a', 2] 164 | ]); 165 | } 166 | }); 167 | 168 | }); 169 | 170 | t.test(`create stub with no parameters`, t => { 171 | 172 | t.test(`Return(value) should make the stub return that value`, t => { 173 | const fn = stub().return(42); 174 | t.eq(fn(1), 42); 175 | 176 | fn.return('woot'); 177 | 178 | t.eq(fn({woot: true}), 'woot'); 179 | t.eq(fn.calls, [ 180 | [1], 181 | [{woot: true}] 182 | ]); 183 | }); 184 | 185 | t.test(`resolve(value) should make the stub return that value as if it was an asynchronous function`, async t => { 186 | const fn = stub().resolve(42); 187 | const val = await fn(1, 2, 3); 188 | t.eq(val, 42); 189 | t.eq(fn.calls, [ 190 | [1, 2, 3] 191 | ]); 192 | }); 193 | 194 | t.test(`reject(reason) should stub an asynchronous function throwing an error`, async t => { 195 | const fn = stub().reject(new Error(`message`)); 196 | try { 197 | await fn('foo'); 198 | } catch (e) { 199 | t.eq(e.message, 'message'); 200 | t.eq(fn.calls, [['foo']]); 201 | } 202 | }); 203 | 204 | t.test(`throw(error) should make stub throw an exception on next call`, t => { 205 | const fn = stub().throw(new Error(`some error`)); 206 | try { 207 | fn('blah'); 208 | t.fail(`should not get here`); 209 | } catch (e) { 210 | t.eq(e.message, 'some error'); 211 | } 212 | }); 213 | }); 214 | }; 215 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "noImplicitThis": true, 5 | "noUnusedLocals": true, 6 | "target": "ESNext", 7 | "moduleResolution": "Node", 8 | "noImplicitReturns": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "allowUnreachableCode": false, 11 | "allowSyntheticDefaultImports": true, 12 | "esModuleInterop": true, 13 | "preserveConstEnums": false, 14 | "lib": [ 15 | "ESNext", 16 | "DOM" 17 | ], 18 | "alwaysStrict": true, 19 | "outDir": "dist" 20 | }, 21 | "include": [ 22 | "*.ts" 23 | ] 24 | } 25 | --------------------------------------------------------------------------------