├── .github ├── release_drafter_config.yml └── workflows │ ├── main.yml │ ├── release.yml │ └── release_drafter.yml ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── console ├── build_esm_lib ├── build_esm_lib.ts ├── bumper_ci_service.ts ├── bumper_ci_service_files.ts ├── deps.ts └── pre_check_release.ts ├── egg.json ├── jest.config.ts ├── logo.svg ├── mod.ts ├── package.json ├── src ├── errors.ts ├── fake │ ├── fake_builder.ts │ └── fake_mixin.ts ├── interfaces.ts ├── mock │ ├── mock_builder.ts │ └── mock_mixin.ts ├── pre_programmed_method.ts ├── spy │ ├── spy_builder.ts │ ├── spy_mixin.ts │ └── spy_stub_builder.ts ├── test_double_builder.ts ├── types.ts └── verifiers │ ├── callable_verifier.ts │ ├── function_expression_verifier.ts │ └── method_verifier.ts ├── tests ├── cjs │ ├── jest_assertions.js │ └── unit │ │ └── mod │ │ ├── dummy.test.js │ │ ├── fake.test.js │ │ ├── mock.test.js │ │ ├── spy.test.js │ │ └── stub.test.js ├── deno │ ├── deps.ts │ ├── src │ │ ├── errors_test.ts │ │ └── verifiers │ │ │ ├── function_expression_verifier_test.ts │ │ │ └── method_verifier_test.ts │ └── unit │ │ └── mod │ │ ├── dummy_test.ts │ │ ├── fake_test.ts │ │ ├── mock_test.ts │ │ ├── spy_test.ts │ │ └── stub_test.ts ├── deps.ts └── esm │ ├── jest_assertions.ts │ └── unit │ └── mod │ ├── dummy.test.ts │ ├── fake.test.ts │ ├── mock.test.ts │ ├── spy.test.ts │ └── stub.test.ts ├── tmp └── .gitkeep ├── tsconfig.cjs.json ├── tsconfig.esm.json └── tsconfig.json /.github/release_drafter_config.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | 4 | change-template: '- $TITLE (#$NUMBER)' 5 | 6 | # Only add to the draft release when a PR has one of these labels 7 | include-labels: 8 | - 'Type: Major' 9 | - 'Type: Minor' 10 | - 'Type: Patch' 11 | - 'Type: Chore' 12 | 13 | # Here is how we determine what version the release would be, by using labels. Eg when "minor" is used, the drafter knows to bump up to a new minor version 14 | version-resolver: 15 | major: 16 | labels: 17 | - 'Type: Major' 18 | minor: 19 | labels: 20 | - 'Type: Minor' 21 | patch: 22 | labels: 23 | - 'Type: Patch' 24 | - 'Type: Chore' # allow our chore PR's to just be patches too 25 | default: patch 26 | 27 | # What our release will look like. If no draft has been created, then this will be used, otherwise $CHANGES just gets addedd 28 | template: | 29 | ``` 30 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 31 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 32 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 33 | 34 | Before releasing, make sure the version in package.json matches the version being released here. 35 | 36 | Delete this code block before publishing this release. 37 | 38 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 39 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 40 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 41 | ``` 42 | 43 | ## Compatibility 44 | 45 | * Requires Deno v 46 | * Requires Node v14, v16, or v18 47 | 48 | ## Documentation 49 | 50 | * [Full Documentation](https://drash.land/rhum/v2.x/getting-started/introduction) 51 | 52 | ## Usage 53 | 54 | 1. Create a `deps.ts` file. 55 | 56 | ```typescript 57 | // deps.ts 58 | 59 | export { 60 | Dummy, 61 | Fake, 62 | Mock, 63 | Spy, 64 | Stub 65 | } from "https://deno.land/x/rhum@v$RESOLVED_VERSION/mod.ts"; 66 | ``` 67 | 68 | 2. Import the test doubles from your `deps.ts` file. 69 | 70 | ```typescript 71 | import { 72 | Dummy, 73 | Fake, 74 | Mock, 75 | Spy, 76 | Stub 77 | } from "./deps.ts" 78 | 79 | ... your 80 | ... code 81 | ... here 82 | ``` 83 | 84 | ## Release Summary 85 | 86 | $CHANGES 87 | 88 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | tests_deno: 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest, macos-latest] 16 | runs-on: ${{ matrix.os }} 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Install Deno 22 | uses: denoland/setup-deno@v1 23 | 24 | - name: Unit Tests 25 | run: deno test tests/deno 26 | 27 | tests_node: 28 | strategy: 29 | fail-fast: true 30 | matrix: 31 | os: [ubuntu-latest, windows-latest, macos-latest] 32 | # We only support until EOL 33 | # See https://nodejs.org/en/about/releases/ 34 | node: ['14', '16', '18'] 35 | runs-on: ${{ matrix.os }} 36 | steps: 37 | - uses: actions/checkout@v2 38 | 39 | # We need deno because the "Build CJS and ESM" step runs `deno run` 40 | - name: Install Deno 41 | uses: denoland/setup-deno@v1 42 | 43 | - name: Install Node 44 | uses: actions/setup-node@v2 45 | with: 46 | node-version: ${{ matrix.node }} 47 | 48 | - name: Install deps 49 | run: yarn install 50 | 51 | - name: Build CJS and ESM (*nix) 52 | if: matrix.os != 'windows-latest' 53 | run: | 54 | yarn build 55 | 56 | - name: Build CJS and ESM (windows) 57 | if: matrix.os == 'windows-latest' 58 | run: | 59 | yarn build:windows 60 | 61 | - name: Unit Test 62 | run: yarn test 63 | 64 | linter: 65 | # Only one OS is required since fmt is cross platform 66 | runs-on: ubuntu-latest 67 | 68 | steps: 69 | - uses: actions/checkout@v2 70 | 71 | - name: Install Deno 72 | uses: denoland/setup-deno@v1 73 | 74 | - name: Lint 75 | run: deno lint 76 | 77 | - name: Formatter 78 | run: deno fmt --check 79 | 80 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: [published] 5 | 6 | # In the even this workflow fails, it can be started manually via `workflow_dispatch` 7 | workflow_dispatch: 8 | 9 | jobs: 10 | 11 | npm: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | token: ${{ secrets.CI_USER_PAT }} 17 | 18 | # We need deno because the "Build CJS and ESM" step runs `deno run` 19 | - name: Install Deno 20 | uses: denoland/setup-deno@v1 21 | 22 | - name: Pre-check release version 23 | run: | 24 | deno run --allow-read ./console/pre_check_release.ts ${{ github.event.release.tag_name }} 25 | 26 | # Setup .npmrc file to publish to npm 27 | - name: Install Node 28 | uses: actions/setup-node@v2 29 | with: 30 | registry-url: 'https://registry.npmjs.org' 31 | scope: '@drashland' 32 | 33 | - name: Install deps 34 | run: yarn install 35 | 36 | - name: Build CJS and ESM 37 | run: yarn build 38 | 39 | - name: Publish 40 | run: yarn publish --access public 41 | env: 42 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTOMATION_TOKEN }} 43 | 44 | github: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v2 48 | with: 49 | token: ${{ secrets.CI_USER_PAT }} 50 | 51 | # We need deno because the "Build CJS and ESM" step runs `deno run` 52 | - name: Install Deno 53 | uses: denoland/setup-deno@v1 54 | 55 | # Setup .npmrc file to publish to github 56 | - name: Install Node 57 | uses: actions/setup-node@v2 58 | with: 59 | registry-url: 'https://npm.pkg.github.com' 60 | scope: '@drashland' 61 | 62 | - name: Install deps 63 | run: yarn install 64 | 65 | - name: Build CJS and ESM 66 | run: yarn build 67 | 68 | - name: Publish 69 | run: yarn publish --access public 70 | env: 71 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | -------------------------------------------------------------------------------- /.github/workflows/release_drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - main 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v5 15 | with: 16 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 17 | config-name: release_drafter_config.yml 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.CI_USER_PAT }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # files 2 | .DS_Store 3 | 4 | # directories 5 | .idea 6 | .vscode 7 | lib/ 8 | node_modules/ 9 | tmp*/ 10 | tmp/ 11 | 12 | # exceptions 13 | !.gitkeep 14 | 15 | # locks 16 | yarn.lock 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2022 Drash Land 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rhum 2 | 3 | [![Latest Release](https://img.shields.io/github/release/drashland/rhum.svg?color=bright_green&label=latest)](https://github.com/drashland/rhum/releases/latest) 4 | [![CI](https://img.shields.io/github/actions/workflow/status/drashland/rhum/main.yml?branch=main&label=branch:main)](https://github.com/drashland/rhum/actions/workflows/main.yml) 5 | [![Drash Land Discord](https://img.shields.io/badge/discord-join-blue?logo=discord)](https://discord.gg/RFsCSaHRWK) 6 | 7 | Drash Land - Rhum logo 8 | 9 | Rhum is a test double library that follows 10 | [test double definitions](https://martinfowler.com/bliki/TestDouble.html) from 11 | Gerard Meszaros. 12 | 13 | View the full documentation at https://drash.land/rhum. 14 | 15 | In the event the documentation pages are not accessible, please view the raw 16 | version of the documentation at 17 | https://github.com/drashland/website-v2/tree/main/docs. 18 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { presets: ["@babel/preset-env"] }; 2 | -------------------------------------------------------------------------------- /console/build_esm_lib: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script only exists to run Deno in different OSs. 4 | ( 5 | deno run --allow-read --allow-run --allow-write ./console/build_esm_lib.ts $@ 6 | ) 7 | -------------------------------------------------------------------------------- /console/build_esm_lib.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @TODO(crookse) This can probably use Line. Line wouldn't necessariliy be a 3 | * dependency of the lib. It would be a dependency of the build process. 4 | * 5 | * This script takes TypeScript files that follow Deno's requirements of (e.g., 6 | * `import` statements require `.ts` extensions) and converts them to a portable 7 | * format that other non-Deno processes can use. 8 | * 9 | * For example, running `deno run -A ./console/build_esm_lib.ts ./src ./mod.ts` 10 | * will do the following: 11 | * 12 | * 1. Create a directory to build the portable TypeScript code. The directory 13 | * in this context is called the "workspace directory" and lives at 14 | * `./tmp/conversion_workspace` directory. 15 | * 2. Copy `./src` into the workspace directory. 16 | * 3. Copy `./mod.ts` into the workspace directory. 17 | * 4. Remove all `.ts` extensions from all `import` and `export` statements in 18 | * all files in the workspace directory. 19 | * 20 | * Now that all `.ts` extensions are removed from the `import` and `export` 21 | * statements, the workspace directory can be used by other processes. For 22 | * example, `tsc` can be used on the workspace directory to compile the code 23 | * down to a specific format like CommonJS for Node applications and a plain 24 | * JavaScript module syntax for browser applications. 25 | * 26 | * @param Deno.args A list of directories and files containing TypeScript files 27 | * that this script should convert. 28 | */ 29 | 30 | import { copySync, emptyDirSync, ensureDirSync, walk } from "./deps.ts"; 31 | const decoder = new TextDecoder(); 32 | const encoder = new TextEncoder(); 33 | const args = Deno.args.slice(); 34 | 35 | const debug = optionEnabled("--debug"); 36 | const debugContents = optionEnabled("--debug-contents"); 37 | const workspace = optionValue("--workspace"); 38 | 39 | Promise 40 | .resolve((() => { 41 | createWorkspace(); 42 | logDebug(`Options:`, { debug, debugContents }); 43 | })()) 44 | .then(convertCode) 45 | .then(() => Deno.exit(0)) 46 | .catch((error) => { 47 | logDebug(error); 48 | Deno.exit(1); 49 | }); 50 | 51 | /** 52 | * Convert the code given to this script. 53 | */ 54 | async function convertCode(): Promise { 55 | logDebug("\nStarting .ts extension removal process.\n"); 56 | 57 | for await (const entry of walk(workspace)) { 58 | if (!entry.isFile) { 59 | continue; 60 | } 61 | 62 | logDebug(`Removing .ts extensions from ${entry.path}.`); 63 | removeTsExtensions(entry.path); 64 | logDebug("Moving to next file."); 65 | logDebug(""); 66 | } 67 | 68 | logDebug("Done removing .ts extensions from source files."); 69 | } 70 | 71 | /** 72 | * Create the workspace directory. 73 | */ 74 | function createWorkspace() { 75 | logDebug(`Creating ${workspace}.`); 76 | emptyDirSync("./node_modules"); 77 | emptyDirSync(workspace); 78 | ensureDirSync(workspace); 79 | 80 | for (const item of args) { 81 | const nonDotFilename = item.replace("./", "/"); 82 | logDebug(`Copying ${item} to ${workspace}${nonDotFilename}.`); 83 | copySync(item, workspace + nonDotFilename, { overwrite: true }); 84 | } 85 | } 86 | 87 | /** 88 | * Remove the .ts extensions for runtimes that do not require it. 89 | */ 90 | function removeTsExtensions(filename: string): void { 91 | // Step 1: Read contents 92 | let contents = decoder.decode(Deno.readFileSync(filename)); 93 | 94 | // Step 2: Create an array of import/export statements from the contents 95 | const importStatements = contents.match( 96 | /(import.+\.ts";)|(import.+((\n|\r)\s.+)+(\n|\r).+\.ts";)/g, 97 | ); 98 | const exportStatements = contents.match( 99 | /(export.+\.ts";)|(export.+((\n|\r)\s.+)+(\n|\r).+\.ts";)/g, 100 | ); 101 | 102 | // FIXME(crookse) This is a temporary fix for removing .ts extensions from the 103 | // the `interfaces.ts` import statement in `spy_stub_builder.ts`. For some 104 | // reason, Windows (in the CI) is not removing the `.ts` extension from the 105 | // `interfaces.ts` import statement. 106 | if (filename.includes("spy_stub_builder.ts")) { 107 | contents = contents.replace("../interfaces.ts", "../interfaces"); 108 | } 109 | 110 | // Step 3: Remove all .ts extensions from the import/export statements 111 | const newImportStatements = importStatements?.map((statement: string) => { 112 | return statement.replace(/\.ts";/, `";`); 113 | }); 114 | 115 | const newExportStatements = exportStatements?.map((statement: string) => { 116 | return statement.replace(/\.ts";/, `";`); 117 | }); 118 | 119 | // Step 4: Replace the original contents with the new contents 120 | if (newImportStatements) { 121 | importStatements?.forEach((statement: string, index: number) => { 122 | contents = contents.replace(statement, newImportStatements[index]); 123 | }); 124 | } 125 | if (newExportStatements) { 126 | exportStatements?.forEach((statement: string, index: number) => { 127 | contents = contents.replace(statement, newExportStatements[index]); 128 | }); 129 | } 130 | 131 | if (debugContents) { 132 | logDebug(`New contents (without .ts extensions):`); 133 | logDebug(contents); 134 | } 135 | 136 | // Step 5: Rewrite the original file without .ts extensions 137 | logDebug(`Overwriting ${filename} with new contents.`); 138 | Deno.writeFileSync(filename, encoder.encode(contents)); 139 | logDebug("File written."); 140 | } 141 | 142 | /** 143 | * Log output. 144 | * @param msg The message to log. 145 | */ 146 | function logDebug(...msg: unknown[]): void { 147 | if (!debug) { 148 | return; 149 | } 150 | 151 | console.log(...msg); 152 | } 153 | 154 | /** 155 | * Is the given option enabled? 156 | * @param option The name of the option (e.g., `--debug`). 157 | * @returns True if yes, false if no. 158 | */ 159 | function optionEnabled(option: string): boolean { 160 | const optionIndex = args.indexOf(option); 161 | const enabled = optionIndex !== -1; 162 | 163 | if (enabled) { 164 | args.splice(optionIndex, 1); 165 | } 166 | 167 | return enabled; 168 | } 169 | 170 | /** 171 | * What is the given option value? 172 | * @param option The name of the option (e.g., `--workspace`). 173 | * @returns The value of the option if the option exists. 174 | */ 175 | function optionValue(option: string): boolean { 176 | const extractedOption = args.filter((arg) => arg.includes(option)); 177 | 178 | if (!extractedOption.length) { 179 | return; 180 | } 181 | 182 | args.splice(args.indexOf(extractedOption[0], 1)); 183 | 184 | return extractedOption[0].replace(option + "=", ""); 185 | } 186 | -------------------------------------------------------------------------------- /console/bumper_ci_service.ts: -------------------------------------------------------------------------------- 1 | import { BumperService } from "https://raw.githubusercontent.com/drashland/services/master/ci/bumper_service.ts"; 2 | import { bumperFiles, preReleaseFiles } from "./bumper_ci_service_files.ts"; 3 | 4 | const b = new BumperService("rhum", Deno.args); 5 | 6 | if (b.isForPreRelease()) { 7 | b.bump(preReleaseFiles); 8 | } else { 9 | b.bump(bumperFiles); 10 | } 11 | -------------------------------------------------------------------------------- /console/bumper_ci_service_files.ts: -------------------------------------------------------------------------------- 1 | export const regexes = { 2 | const_statements: /version = ".+"/g, 3 | egg_json: /"version": ".+"/, 4 | import_export_statements: /rhum@v[0-9\.]+[0-9\.]+[0-9\.]/g, 5 | yml_deno: /deno: \[".+"\]/g, 6 | }; 7 | 8 | export const preReleaseFiles = [ 9 | { 10 | filename: "./egg.json", 11 | replaceTheRegex: regexes.egg_json, 12 | replaceWith: `"version": "{{ thisModulesLatestVersion }}"`, 13 | }, 14 | ]; 15 | 16 | export const bumperFiles = []; 17 | -------------------------------------------------------------------------------- /console/deps.ts: -------------------------------------------------------------------------------- 1 | export { 2 | emptyDirSync, 3 | ensureDirSync, 4 | walk, 5 | } from "https://deno.land/std@0.167.0/fs/mod.ts"; 6 | export { copySync } from "https://deno.land/std@0.167.0/fs/copy.ts"; 7 | -------------------------------------------------------------------------------- /console/pre_check_release.ts: -------------------------------------------------------------------------------- 1 | const versionToPublish = Deno.args[0]; 2 | 3 | const packageJsonContents = new TextDecoder().decode( 4 | Deno.readFileSync("./package.json"), 5 | ); 6 | 7 | const packageJson = JSON.parse(packageJsonContents); 8 | const packageJsonVersion = `v${packageJson.version}`; 9 | 10 | console.log("Checking package.json version with GitHub release tag version.\n"); 11 | console.log(`packge.json version: ${packageJsonVersion}`); 12 | console.log(`GitHub release version: ${versionToPublish}\n`); 13 | 14 | if (packageJsonVersion !== versionToPublish) { 15 | console.log("Version mismatch. Stopping Release workflow."); 16 | Deno.exit(1); 17 | } else { 18 | console.log("Versions match. Proceeding with Release workflow."); 19 | } 20 | -------------------------------------------------------------------------------- /egg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rhum", 3 | "description": "A test double module to stub and mock your code.", 4 | "version": "1.1.14", 5 | "stable": true, 6 | "repository": "https://github.com/drashland/rhum", 7 | "files": [ 8 | "./mod.ts", 9 | "./README.md", 10 | "./logo.svg", 11 | "LICENSE" 12 | ], 13 | "check": false, 14 | "entry": "./mod.ts", 15 | "homepage": "https://github.com/drashland/rhum", 16 | "ignore": [], 17 | "unlisted": false 18 | } 19 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | import type { Config } from "@jest/types"; 3 | 4 | const config: Config.InitialOptions = { 5 | preset: "ts-jest", 6 | testEnvironment: "node", 7 | verbose: true, 8 | moduleFileExtensions: ["ts", "js"], 9 | transform: { 10 | "^.+\\.(ts|tsx)?$": "ts-jest", 11 | "^.+\\.(js|jsx)$": "babel-jest", 12 | }, 13 | }; 14 | export default config; 15 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | back 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import type { Callable, Constructor, MethodOf } from "./src/types.ts"; 2 | import { MockBuilder } from "./src/mock/mock_builder.ts"; 3 | import { FakeBuilder } from "./src/fake/fake_builder.ts"; 4 | import { SpyBuilder } from "./src/spy/spy_builder.ts"; 5 | import { SpyStubBuilder } from "./src/spy/spy_stub_builder.ts"; 6 | import * as Interfaces from "./src/interfaces.ts"; 7 | export * as Types from "./src/types.ts"; 8 | export * as Interfaces from "./src/interfaces.ts"; 9 | 10 | //////////////////////////////////////////////////////////////////////////////// 11 | // FILE MARKER - DUMMY ///////////////////////////////////////////////////////// 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | /** 15 | * Create a dummy. 16 | * 17 | * Per Martin Fowler (based on Gerard Meszaros), "Dummy objects are passed 18 | * around but never actually used. Usually they are just used to fill parameter 19 | * lists." 20 | * 21 | * @param constructorFn - The constructor function to use to become the 22 | * prototype of the dummy. Dummy objects should be the same instance as what 23 | * they are standing in for. For example, if a `SomeClass` parameter needs to be 24 | * filled with a dummy because it is out of scope for a test, then the dummy 25 | * should be an instance of `SomeClass`. 26 | * @returns A dummy object being an instance of the given constructor function. 27 | */ 28 | export function Dummy(constructorFn?: Constructor): T { 29 | const dummy = Object.create({}); 30 | Object.setPrototypeOf(dummy, constructorFn ?? Object); 31 | return dummy; 32 | } 33 | 34 | //////////////////////////////////////////////////////////////////////////////// 35 | // FILE MARKER - FAKE ////////////////////////////////////////////////////////// 36 | //////////////////////////////////////////////////////////////////////////////// 37 | 38 | /** 39 | * Get the builder to create fake objects. 40 | * 41 | * Per Martin Fowler (based on Gerard Meszaros), "Fake objects actually have 42 | * working implementations, but usually take some shortcut which makes them not 43 | * suitable for production (an InMemoryTestDatabase is a good example)." 44 | * 45 | * @param constructorFn - The constructor function of the object to fake. 46 | * 47 | * @returns Instance of `FakeBuilder`. 48 | */ 49 | export function Fake(constructorFn: Constructor): FakeBuilder { 50 | return new FakeBuilder(constructorFn); 51 | } 52 | 53 | //////////////////////////////////////////////////////////////////////////////// 54 | // FILE MARKER - MOCK ////////////////////////////////////////////////////////// 55 | //////////////////////////////////////////////////////////////////////////////// 56 | 57 | /** 58 | * Get the builder to create mocked objects. 59 | * 60 | * Per Martin Fowler (based on Gerard Meszaros), "Mocks are pre-programmed with 61 | * expectations which form a specification of the calls they are expected to 62 | * receive. They can throw an exception if they receive a call they don't expect 63 | * and are checked during verification to ensure they got all the calls they 64 | * were expecting." 65 | * 66 | * @param constructorFn - The constructor function of the object to mock. 67 | * 68 | * @returns Instance of `MockBuilder`. 69 | */ 70 | export function Mock(constructorFn: Constructor): MockBuilder { 71 | return new MockBuilder(constructorFn); 72 | } 73 | 74 | //////////////////////////////////////////////////////////////////////////////// 75 | // FILE MARKER - SPY /////////////////////////////////////////////////////////// 76 | //////////////////////////////////////////////////////////////////////////////// 77 | 78 | /** 79 | * Create a spy out of a function expression. 80 | * 81 | * @param functionExpression - The function expression to turn into a spy. 82 | * @param returnValue - (Optional) The value the spy should return when called. 83 | * Defaults to "spy-stubbed". 84 | * 85 | * @returns The original function expression with spying capabilities. 86 | */ 87 | export function Spy< 88 | OriginalFunction extends Callable, 89 | ReturnValue, 90 | >( 91 | functionExpression: OriginalFunction, 92 | returnValue?: ReturnValue, 93 | ): Interfaces.ISpyStubFunctionExpression & OriginalFunction; 94 | 95 | /** 96 | * Create a spy out of an object's method. 97 | * 98 | * @param obj - The object containing the method to spy on. 99 | * @param dataMember - The method to spy on. 100 | * @param returnValue - (Optional) The value the spy should return when called. 101 | * Defaults to "spy-stubbed". 102 | * 103 | * @returns The original method with spying capabilities. 104 | */ 105 | export function Spy( 106 | obj: OriginalObject, 107 | dataMember: MethodOf, 108 | returnValue?: ReturnValue, 109 | ): Interfaces.ISpyStubMethod; 110 | 111 | /** 112 | * Create spy out of a class. 113 | * 114 | * @param constructorFn - The constructor function of the object to spy on. 115 | * 116 | * @returns The original object with spying capabilities. 117 | */ 118 | export function Spy( 119 | constructorFn: Constructor, 120 | ): Interfaces.ISpy & OriginalClass; 121 | 122 | /** 123 | * Create a spy out of a class, class method, or function. 124 | * 125 | * Per Martin Fowler (based on Gerard Meszaros), "Spies are stubs that also 126 | * record some information based on how they were called. One form of this might 127 | * be an email service that records how many messages it was sent." 128 | * 129 | * @param original - The original to turn into a spy. 130 | * @param methodOrReturnValue - (Optional) If creating a spy out of an object's method, then 131 | * this would be the method name. If creating a spy out of a function 132 | * expression, then this would be the return value. 133 | * @param returnValue - (Optional) If creating a spy out of an object's method, then 134 | * this would be the return value. 135 | */ 136 | export function Spy( 137 | original: unknown, 138 | methodOrReturnValue?: unknown, 139 | returnValue?: unknown, 140 | ): unknown { 141 | if (typeof original === "function") { 142 | // If the function has the prototype field, the it's a constructor function. 143 | // 144 | // Examples: 145 | // class Hello { } 146 | // function Hello() { } 147 | // 148 | if ("prototype" in original) { 149 | return new SpyBuilder(original as Constructor).create(); 150 | } 151 | 152 | // Otherwise, it's just a function. 153 | // 154 | // Example: 155 | // const hello = () => "world"; 156 | // 157 | // Not that function declarations (e.g., function hello() { }) will have 158 | // "prototype" and will go through the SpyBuilder() flow above. 159 | return new SpyStubBuilder(original as OriginalObject) 160 | .returnValue(methodOrReturnValue as ReturnValue) 161 | .createForFunctionExpression(); 162 | } 163 | 164 | // If we get here, then we are not spying on a class or function. We must be 165 | // spying on an object's method. 166 | return new SpyStubBuilder(original as OriginalObject) 167 | .method(methodOrReturnValue as MethodOf) 168 | .returnValue(returnValue as ReturnValue) 169 | .createForObjectMethod(); 170 | } 171 | 172 | //////////////////////////////////////////////////////////////////////////////// 173 | // FILE MARKER - STUB ////////////////////////////////////////////////////////// 174 | //////////////////////////////////////////////////////////////////////////////// 175 | 176 | /** 177 | * Create a stub function that returns "stubbed". 178 | * 179 | * @returns A function that returns "stubbed". 180 | */ 181 | export function Stub(): () => "stubbed"; 182 | 183 | /** 184 | * Take the given object and stub its given data member to return the given 185 | * return value. 186 | * 187 | * @param obj - The object receiving the stub. 188 | * @param dataMember - The data member on the object to be stubbed. 189 | * @param returnValue - (optional) What the stub should return. Defaults to 190 | * "stubbed". 191 | */ 192 | export function Stub( 193 | obj: OriginalObject, 194 | dataMember: keyof OriginalObject, 195 | returnValue?: ReturnValue, 196 | ): void; 197 | /** 198 | * Take the given object and stub its given data member to return the given 199 | * return value. 200 | * 201 | * Per Martin Fowler (based on Gerard Meszaros), "Stubs provide canned answers 202 | * to calls made during the test, usually not responding at all to anything 203 | * outside what's programmed in for the test." 204 | * 205 | * @param obj - (Optional) The object receiving the stub. Defaults to a stub 206 | * function. 207 | * @param dataMember - (Optional) The data member on the object to be stubbed. 208 | * Only used if `obj` is an object. 209 | * @param returnValue - (Optional) What the stub should return. Defaults to 210 | * "stubbed" for class properties and a function that returns "stubbed" for 211 | * class methods. Only used if `object` is an object and `dataMember` is a 212 | * member of that object. 213 | */ 214 | export function Stub( 215 | obj?: OriginalObject, 216 | dataMember?: keyof OriginalObject, 217 | returnValue?: ReturnValue, 218 | ): unknown { 219 | if (obj === null) { 220 | throw new Error(`Cannot create a stub using Stub(null)`); 221 | } 222 | 223 | if (obj === undefined) { 224 | return function stubbed() { 225 | return "stubbed"; 226 | }; 227 | } 228 | 229 | // If we get here, then we know for a fact that we are stubbing object 230 | // properties. Also, we do not care if `returnValue` was passed in here. If it 231 | // is not passed in, then `returnValue` defaults to "spy-stubbed". Otherwise, use 232 | // the value of `returnValue`. 233 | if (typeof obj === "object" && dataMember !== undefined) { 234 | // If we are stubbing a method, then make sure the method is still callable 235 | if (typeof obj![dataMember] === "function") { 236 | Object.defineProperty(obj, dataMember, { 237 | value: () => returnValue !== undefined ? returnValue : "stubbed", 238 | writable: true, 239 | }); 240 | } else { 241 | // If we are stubbing a property, then just reassign the property 242 | Object.defineProperty(obj, dataMember, { 243 | value: returnValue !== undefined ? returnValue : "stubbed", 244 | writable: true, 245 | }); 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@drashland/rhum", 3 | "version": "2.2.0", 4 | "description": "A test double library", 5 | "main": "./lib/cjs/mod.js", 6 | "types": "./lib/cjs/mod.d.ts", 7 | "repository": "git@github.com:drashland/rhum.git", 8 | "author": "Drash Land", 9 | "license": "MIT", 10 | "scripts": { 11 | "build": "yarn build:esm-lib && yarn && yarn build:esm && yarn build:cjs", 12 | "build:cjs": "tsc --project tsconfig.cjs.json", 13 | "build:conversion-workspace": "deno run --allow-read --allow-write ./console/build_esm_lib.ts", 14 | "build:esm": "tsc --project tsconfig.esm.json", 15 | "build:esm-lib": "console/build_esm_lib ./src ./mod.ts --workspace=./tmp/conversion_workspace --debug", 16 | "build:windows": "bash console/build_esm_lib ./src ./mod.ts --workspace=./tmp/conversion_workspace --debug && yarn && yarn build:cjs && yarn build:esm", 17 | "release": "yarn publish --access public", 18 | "test": "yarn test:deno && yarn test:cjs && yarn test:esm", 19 | "test:cjs": "yarn jest tests/cjs/", 20 | "test:deno": "deno test -A tests/deno/", 21 | "test:esm": "yarn jest tests/esm/", 22 | "validate:nix": "rm -rf node_modules && rm yarn.lock && yarn build && yarn test" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "7.x", 26 | "@babel/preset-env": "7.x", 27 | "@types/jest": "27.x", 28 | "@types/node": "16.x", 29 | "babel-jest": "27.x", 30 | "jest": "27.x", 31 | "ts-jest": "27.x", 32 | "ts-node": "10.x", 33 | "tsc": "2.x", 34 | "typescript": "4.x" 35 | }, 36 | "files": [ 37 | "./lib" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base class for custom errors in Rhum. 3 | */ 4 | class RhumError extends Error { 5 | public name: string; 6 | 7 | constructor(name: string, message: string) { 8 | super(message); 9 | this.name = name; 10 | } 11 | } 12 | 13 | /** 14 | * Error to throw in relation to fake logic. 15 | */ 16 | export class FakeError extends RhumError { 17 | constructor(message: string) { 18 | super("FakeError", message); 19 | } 20 | } 21 | 22 | /** 23 | * Error to thrown in relation to mock logic. 24 | */ 25 | export class MockError extends RhumError { 26 | constructor(message: string) { 27 | super("MockError", message); 28 | } 29 | } 30 | 31 | /** 32 | * Error to throw in relation to spy logic. 33 | */ 34 | export class SpyError extends RhumError { 35 | constructor(message: string) { 36 | super("SpyError", message); 37 | } 38 | } 39 | 40 | /** 41 | * Error to throw in relation to verification logic. For example, when a method 42 | * or function expression is being verified that it was called once via 43 | * `.verify("someMethod").toBeCalled(1)`. The stack trace shown with this error 44 | * is modified to look like the following example: 45 | * 46 | * @example 47 | * ```text 48 | * VerificationError: Method "someMethod" was called with args when expected to receive no args. 49 | * at file:///path/to/some/file.ts:99:14 50 | * 51 | * Verification Results: 52 | * Actual args -> (2, "hello", {}) 53 | * Expected args -> (no args) 54 | * 55 | * Check the above "file.ts" file at/around line 99 for code like the following to fix this error: 56 | * .verify("someMethod").toBeCalledWithoutArgs() 57 | * ``` 58 | */ 59 | export class VerificationError extends RhumError { 60 | #actual_results: string; 61 | #code_that_threw: string; 62 | #expected_results: string; 63 | 64 | /** 65 | * @param message - The error message (to be shown in the stack trace). 66 | * @param codeThatThrew - An example of the code (or the exact code) that 67 | * caused this error to be thrown. 68 | * @param actualResults - A message stating the actual results (to be show in 69 | * the stack trace). 70 | * @param expectedResults - A message stating the expected results (to be 71 | * shown in the stack trace). 72 | */ 73 | constructor( 74 | message: string, 75 | codeThatThrew: string, 76 | actualResults: string, 77 | expectedResults: string, 78 | ) { 79 | super("VerificationError", message); 80 | this.#code_that_threw = codeThatThrew; 81 | this.#actual_results = actualResults; 82 | this.#expected_results = expectedResults; 83 | this.#makeStackConcise(); 84 | } 85 | 86 | /** 87 | * Shorten the stack trace to show exactly what file threw the error. This 88 | * redefines the stack trace (if there is a stack trace). 89 | */ 90 | #makeStackConcise(): void { 91 | const ignoredLines = [ 92 | "", 93 | "deno:runtime", 94 | "callable_verifier.ts", 95 | "method_verifier.ts", 96 | "function_expression_verifier.ts", 97 | "_mixin.ts", 98 | ".toBeCalled", 99 | ".toBeCalledWithArgs", 100 | ".toBeCalledWithoutArgs", 101 | ]; 102 | 103 | if (!this.stack) { 104 | return; 105 | } 106 | 107 | let conciseStackArray = this.stack.split("\n").filter((line: string) => { 108 | return ignoredLines.filter((ignoredLine: string) => { 109 | return line.includes(ignoredLine); 110 | }).length === 0; 111 | }); 112 | 113 | // Sometimes, the error stack will contain the problematic file twice. We 114 | // only care about showing the problematic file once in this "concise" 115 | // stack. In order to check for this, we check to see if the array contains 116 | // more than 2 values. The first value should be the `VerificationError` 117 | // message. The second value should be the first instance of the problematic 118 | // file. Knowing this, we can slice the array to contain only the error 119 | // message and the first instance of the problematic file. 120 | if (conciseStackArray.length > 2) { 121 | conciseStackArray = conciseStackArray.slice(0, 2); 122 | } 123 | 124 | const conciseStack = conciseStackArray.join("\n"); 125 | 126 | const extractedFilenameWithLineAndColumnNumbers = conciseStack.match( 127 | /\/[a-zA-Z0-9\(\)\[\]_-\d.]+\.ts:\d+:\d+/, 128 | ); 129 | 130 | let [filename, lineNumber] = extractedFilenameWithLineAndColumnNumbers 131 | ? extractedFilenameWithLineAndColumnNumbers[0].split(":") 132 | : ""; 133 | 134 | filename = filename.replace("/", ""); 135 | 136 | let newStack = ` 137 | 138 | ${conciseStack} 139 | 140 | Verification Results: 141 | ${this.#actual_results} 142 | ${this.#expected_results} 143 | `; 144 | 145 | if (lineNumber) { 146 | newStack += ` 147 | Check the above "${filename}" file at/around line ${lineNumber} for code like the following to fix this error: 148 | ${this.#code_that_threw} 149 | 150 | 151 | `; 152 | } 153 | 154 | this.stack = newStack; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/fake/fake_builder.ts: -------------------------------------------------------------------------------- 1 | import type { IFake, ITestDouble } from "../interfaces.ts"; 2 | import type { Constructor } from "../types.ts"; 3 | 4 | import { PreProgrammedMethod } from "../pre_programmed_method.ts"; 5 | import { TestDoubleBuilder } from "../test_double_builder.ts"; 6 | import { createFake } from "./fake_mixin.ts"; 7 | 8 | /** 9 | * Builder to help build a fake object. This does all of the heavy-lifting to 10 | * create a fake object. 11 | */ 12 | export class FakeBuilder extends TestDoubleBuilder { 13 | ////////////////////////////////////////////////////////////////////////////// 14 | // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// 15 | ////////////////////////////////////////////////////////////////////////////// 16 | 17 | /** 18 | * Create the fake object. 19 | * 20 | * @returns The original object with faking capabilities. 21 | */ 22 | public create(): ClassToFake & IFake { 23 | const original = new this.constructor_fn(...this.constructor_args); 24 | 25 | const fake = createFake, ClassToFake>( 26 | this.constructor_fn, 27 | ...this.constructor_args, 28 | ); 29 | 30 | (fake as IFake & ITestDouble).init( 31 | original, 32 | this.getAllFunctionNames(original), 33 | ); 34 | 35 | // Attach all of the original's properties to the fake 36 | this.addOriginalProperties>(original, fake); 37 | 38 | // Attach all of the original's functions to the fake 39 | this.getAllFunctionNames(original).forEach((method: string) => { 40 | this.#addOriginalMethod( 41 | original, 42 | fake, 43 | method, 44 | ); 45 | }); 46 | 47 | return fake as ClassToFake & IFake; 48 | } 49 | 50 | ////////////////////////////////////////////////////////////////////////////// 51 | // FILE MARKER - METHODS - PRIVATE /////////////////////////////////////////// 52 | ////////////////////////////////////////////////////////////////////////////// 53 | 54 | /** 55 | * Add an original object's method to a fake object -- determining whether the 56 | * method should or should not be trackable. 57 | * 58 | * @param original - The original object containing the method to add. 59 | * @param fake - The fake object receiving the method. 60 | * @param method - The name of the method to fake -- callable via 61 | * `fake[method](...)`. 62 | */ 63 | #addOriginalMethod( 64 | original: ClassToFake, 65 | fake: IFake, 66 | method: string, 67 | ): void { 68 | if (this.native_methods.indexOf(method as string) !== -1) { 69 | return this.addOriginalMethodWithoutTracking>( 70 | original, 71 | fake, 72 | method, 73 | ); 74 | } 75 | 76 | this.#addOriginalMethodAsProxy( 77 | original, 78 | fake, 79 | method as keyof ClassToFake, 80 | ); 81 | } 82 | 83 | /** 84 | * Adds the original method as a proxy, which can be configured during tests. 85 | * 86 | * @param original - The original object containing the method to add. 87 | * @param fake - The fake object receiving the method. 88 | * @param method - The name of the method. 89 | */ 90 | #addOriginalMethodAsProxy( 91 | original: ClassToFake, 92 | fake: IFake, 93 | method: keyof ClassToFake, 94 | ): void { 95 | Object.defineProperty(fake, method, { 96 | value: (...args: unknown[]) => { 97 | // Make sure the method calls its original self 98 | const methodToCall = 99 | original[method as keyof ClassToFake] as unknown as ( 100 | ...params: unknown[] 101 | ) => unknown; 102 | 103 | // We need to check if the method was pre-preprogrammed to do something. 104 | // If it was, then we make sure that this method we are currently 105 | // defining returns that pre-programmed expectation. 106 | if (methodToCall instanceof PreProgrammedMethod) { 107 | return methodToCall.run(args); 108 | } 109 | 110 | // When method calls its original self, let the `this` context of the 111 | // original be the fake. Reason being the fake has tracking and the 112 | // original does not. 113 | const bound = methodToCall.bind(fake); 114 | 115 | // Use `return` because the original function could return a value 116 | return bound(...args); 117 | }, 118 | writable: true, 119 | }); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/fake/fake_mixin.ts: -------------------------------------------------------------------------------- 1 | import type { IFake, IMethodChanger } from "../interfaces.ts"; 2 | import type { Constructor, MethodOf } from "../types.ts"; 3 | import { FakeError } from "../errors.ts"; 4 | 5 | import { PreProgrammedMethod } from "../pre_programmed_method.ts"; 6 | 7 | /** 8 | * Create a mock object as an extension of an original object. 9 | * 10 | * @param OriginalClass - The class the mock should extend. 11 | * 12 | * @returns A mock object of the `OriginalClass`. 13 | */ 14 | export function createFake( 15 | OriginalClass: OriginalConstructor, 16 | ...originalConstructorArgs: unknown[] 17 | ): IFake { 18 | const Original = OriginalClass as unknown as Constructor< 19 | // deno-lint-ignore no-explicit-any 20 | (...args: any[]) => any 21 | >; 22 | return new class FakeExtension extends Original { 23 | /** 24 | * Helper property to see that this is a fake object and not the original. 25 | */ 26 | is_fake = true; 27 | 28 | /** 29 | * The original object that this class creates a fake of. 30 | */ 31 | #original!: OriginalObject; 32 | 33 | /** 34 | * Map of pre-programmed methods defined by the user. 35 | */ 36 | #pre_programmed_methods: Map< 37 | MethodOf, 38 | IMethodChanger 39 | > = new Map(); 40 | 41 | //////////////////////////////////////////////////////////////////////////// 42 | // FILE MARKER - CONSTRUCTOR /////////////////////////////////////////////// 43 | //////////////////////////////////////////////////////////////////////////// 44 | 45 | constructor() { 46 | super(...originalConstructorArgs); 47 | } 48 | 49 | //////////////////////////////////////////////////////////////////////////// 50 | // FILE MARKER - METHODS - PUBLIC ////////////////////////////////////////// 51 | //////////////////////////////////////////////////////////////////////////// 52 | 53 | /** 54 | * @param original - The original object to fake. 55 | */ 56 | public init(original: OriginalObject) { 57 | this.#original = original; 58 | } 59 | 60 | /** 61 | * Pre-program a method on the original to return a specific value. 62 | * 63 | * @param methodName The method name on the original. 64 | * @returns A pre-programmed method that will be called instead of original. 65 | */ 66 | public method( 67 | methodName: MethodOf, 68 | ): IMethodChanger { 69 | // If the method was already pre-programmed previously, then return it so 70 | // the user can add more method setups to it 71 | if (this.#pre_programmed_methods.has(methodName)) { 72 | return this.#pre_programmed_methods.get(methodName)!; 73 | } 74 | 75 | const methodConfiguration = new PreProgrammedMethod< 76 | OriginalObject, 77 | ReturnValueType 78 | >( 79 | methodName, 80 | ); 81 | 82 | if ( 83 | !((methodName as string) in (this.#original as Record)) 84 | ) { 85 | const typeSafeMethodName = String(methodName as string); 86 | 87 | throw new FakeError( 88 | `Method "${typeSafeMethodName}" does not exist.`, 89 | ); 90 | } 91 | 92 | Object.defineProperty(this.#original, methodName, { 93 | value: methodConfiguration, 94 | writable: true, 95 | }); 96 | 97 | const methodChanger = methodConfiguration as unknown as IMethodChanger< 98 | ReturnValueType 99 | >; 100 | 101 | this.#pre_programmed_methods.set(methodName, methodChanger); 102 | 103 | return methodChanger; 104 | } 105 | }(); 106 | } 107 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import type { MethodOf, MockMethodCalls } from "./types.ts"; 2 | 3 | //////////////////////////////////////////////////////////////////////////////// 4 | // FILE MARKER - IERROR //////////////////////////////////////////////////////// 5 | //////////////////////////////////////////////////////////////////////////////// 6 | 7 | /** 8 | * Interface that all errors must follow. This is useful when a client wants to 9 | * throw a custom error class via `.willThrow()` for mocks and fakes. 10 | */ 11 | export interface IError { 12 | /** 13 | * The name of the error (shown before the error message when thrown). 14 | * Example: `ErrorName: `. 15 | */ 16 | name: string; 17 | 18 | /** 19 | * The error message. Allows undefined in case there is no message. 20 | */ 21 | message?: string; 22 | } 23 | 24 | //////////////////////////////////////////////////////////////////////////////// 25 | // FILE MARKER - IFAKE ///////////////////////////////////////////////////////// 26 | //////////////////////////////////////////////////////////////////////////////// 27 | 28 | export interface IFake { 29 | /** 30 | * Helper property to show that this object is a fake. 31 | */ 32 | is_fake: boolean; 33 | 34 | /** 35 | * Access the method shortener to make the given method take a shortcut. 36 | * 37 | * @param method - The name of the method to shorten. 38 | */ 39 | method( 40 | method: MethodOf, 41 | ): IMethodChanger; 42 | } 43 | 44 | //////////////////////////////////////////////////////////////////////////////// 45 | // FILE MARKER - IFUNCTIONEXPRESSIONVERIFIER /////////////////////////////////// 46 | //////////////////////////////////////////////////////////////////////////////// 47 | 48 | /** 49 | * Interface of verifier that verifies function expression calls. 50 | */ 51 | export interface IFunctionExpressionVerifier extends IVerifier { 52 | /** 53 | * The args used when called. 54 | */ 55 | args: unknown[]; 56 | 57 | /** 58 | * The number of calls made. 59 | */ 60 | calls: number; 61 | } 62 | 63 | //////////////////////////////////////////////////////////////////////////////// 64 | // FILE MARKER - IMETHODEXPECTATION //////////////////////////////////////////// 65 | //////////////////////////////////////////////////////////////////////////////// 66 | 67 | export interface IMethodExpectation { 68 | /** 69 | * Set an expectation that the given number of expected calls should be made. 70 | * 71 | * @param expectedCalls - (Optional) The number of expected calls. If not 72 | * specified, then verify that there was at least one call. 73 | * 74 | * @returns `this` To allow method chaining. 75 | */ 76 | toBeCalled(expectedCalls?: number): this; 77 | 78 | // TODO(crookse) Future release 79 | /** 80 | * Set an expectation that the given args should be used during a method call. 81 | * 82 | * @param requiredArg - Used to make this method require at least one arg. 83 | * @param restOfArgs - Any other args to check during verification. 84 | * 85 | * @returns `this` To allow method chaining. 86 | */ 87 | // toBeCalledWithArgs(requiredArg: unknown, ...restOfArgs: unknown[]): this; 88 | 89 | // TODO(crookse) Future release 90 | /** 91 | * Set an expectation that a method call should be called without args. 92 | * 93 | * @returns `this` To allow method chaining. 94 | */ 95 | // toBeCalledWithoutArgs(): this; 96 | } 97 | 98 | //////////////////////////////////////////////////////////////////////////////// 99 | // FILE MARKER - IMETHODCHANGER //////////////////////////////////////////////// 100 | //////////////////////////////////////////////////////////////////////////////// 101 | 102 | export interface IMethodChanger { 103 | // TODO(crookse) Think about introducing this as an alias to willReturn(). 104 | // willCall(args: T): void; 105 | 106 | /** 107 | * Pre-program a method to return (and call) the following action. 108 | * @param action The action the method will return and call. 109 | */ 110 | willReturn(action: (...args: unknown[]) => ReturnValue): void; 111 | 112 | /** 113 | * Pre-program this method to return the given value. 114 | * @param returnValue The value that should be returned when this object is 115 | * being used in place of an original method. 116 | */ 117 | willReturn(returnValue: T): void; 118 | 119 | /** 120 | * Pre-program this method to throw the given error. 121 | * @param error - An error which extends the `Error` class or has the same 122 | * interface as the `Error` class. 123 | */ 124 | willThrow(error: IError & T): void; 125 | } 126 | 127 | //////////////////////////////////////////////////////////////////////////////// 128 | // FILE MARKER - IMETHODVERIFIER /////////////////////////////////////////////// 129 | //////////////////////////////////////////////////////////////////////////////// 130 | 131 | /** 132 | * Interface of verifier that verifies method calls. 133 | */ 134 | export interface IMethodVerifier extends IVerifier { 135 | /** 136 | * Property to hold the args used when the method using this verifier was 137 | * called. 138 | */ 139 | args: unknown[]; 140 | 141 | /** 142 | * Property to hold the number of times the method using this verifier was 143 | * called. 144 | */ 145 | calls: number; 146 | } 147 | 148 | //////////////////////////////////////////////////////////////////////////////// 149 | // FILE MARKER - IMOCK ///////////////////////////////////////////////////////// 150 | //////////////////////////////////////////////////////////////////////////////// 151 | 152 | export interface IMock { 153 | /** 154 | * Property to track method calls. 155 | */ 156 | calls: MockMethodCalls; 157 | 158 | /** 159 | * Helper property to see that this is a mock object and not the original. 160 | */ 161 | is_mock: boolean; 162 | 163 | /** 164 | * Access the method expectation creator to create an expectation for the 165 | * given method. 166 | * 167 | * @param method - The name of the method to create an expectation for. 168 | */ 169 | expects( 170 | method: MethodOf, 171 | ): IMethodExpectation; 172 | 173 | /** 174 | * Access the method pre-programmer to change the behavior of the given method. 175 | * 176 | * @param method - The name of the method to pre-program. 177 | */ 178 | method( 179 | method: MethodOf, 180 | ): IMethodChanger; 181 | 182 | /** 183 | * Verify that all expectations from the `.expects()` calls. 184 | */ 185 | verifyExpectations(): void; 186 | } 187 | 188 | //////////////////////////////////////////////////////////////////////////////// 189 | // FILE MARKER - ISPY ////////////////////////////////////////////////////////// 190 | //////////////////////////////////////////////////////////////////////////////// 191 | 192 | export interface ISpy { 193 | /** 194 | * Helper property to see that this is a spy object and not the original. 195 | */ 196 | is_spy: boolean; 197 | 198 | /** 199 | * Property to track all stubbed methods. This property is used when calling 200 | * `.verify("someMethod")`. The `.verify("someMethod")` call will return the 201 | * `ISpyStubMethod` object via `stubbed_methods["someMethod"]`. 202 | */ 203 | stubbed_methods: Record, ISpyStubMethod>; 204 | 205 | /** 206 | * Access the method verifier. 207 | * 208 | * @returns A verifier to verify method calls. 209 | */ 210 | verify( 211 | method: MethodOf, 212 | ): IMethodVerifier; 213 | } 214 | 215 | //////////////////////////////////////////////////////////////////////////////// 216 | // FILE MARKER - ISPYSTUBFUNCTIONEXPRESSION //////////////////////////////////// 217 | //////////////////////////////////////////////////////////////////////////////// 218 | 219 | /** 220 | * Interface for spies on function expressions. 221 | */ 222 | export interface ISpyStubFunctionExpression { 223 | /** 224 | * Access the function expression verifier. 225 | * 226 | * @returns A verifier to verify function expression calls. 227 | */ 228 | verify(): IFunctionExpressionVerifier; 229 | } 230 | 231 | //////////////////////////////////////////////////////////////////////////////// 232 | // FILE MARKER - ISPYSTUBMETHOD //////////////////////////////////////////////// 233 | //////////////////////////////////////////////////////////////////////////////// 234 | 235 | /** 236 | * Interface for spies on object methods. 237 | */ 238 | export interface ISpyStubMethod { 239 | /** 240 | * Access the method verifier. 241 | * 242 | * @returns A verifier to verify method calls. 243 | */ 244 | verify(): IMethodVerifier; 245 | } 246 | 247 | //////////////////////////////////////////////////////////////////////////////// 248 | // FILE MARKER - ITESTDOUBLE /////////////////////////////////////////////////// 249 | //////////////////////////////////////////////////////////////////////////////// 250 | 251 | export interface ITestDouble { 252 | init( 253 | original: OriginalObject, 254 | methodsToTrack: string[], 255 | ): void; 256 | } 257 | 258 | //////////////////////////////////////////////////////////////////////////////// 259 | // FILE MARKER - IVERIFIER ///////////////////////////////////////////////////// 260 | //////////////////////////////////////////////////////////////////////////////// 261 | 262 | /** 263 | * Base interface for verifiers. 264 | */ 265 | export interface IVerifier { 266 | /** 267 | * Verify that calls were made. 268 | * 269 | * @param expectedCalls - (Optional) The number of expected calls. If not 270 | * specified, then verify that there was at least one call. 271 | * 272 | * @returns `this` To allow method chaining. 273 | */ 274 | toBeCalled(expectedCalls?: number): this; 275 | 276 | /** 277 | * Verify that the given args were used. Takes a rest parameter of args to use 278 | * during verification. At least one arg is required to use this method, which 279 | * is the `requiredArg` param. 280 | * 281 | * @param requiredArg - Used to make this method require at least one arg. 282 | * @param restOfArgs - Any other args to check during verification. 283 | * 284 | * @returns `this` To allow method chaining. 285 | */ 286 | toBeCalledWithArgs(requiredArg: unknown, ...restOfArgs: unknown[]): this; 287 | 288 | /** 289 | * Verify that no args were used. 290 | * 291 | * @returns `this` To allow method chaining. 292 | */ 293 | toBeCalledWithoutArgs(): this; 294 | } 295 | -------------------------------------------------------------------------------- /src/mock/mock_builder.ts: -------------------------------------------------------------------------------- 1 | import type { IMock, ITestDouble } from "../interfaces.ts"; 2 | import type { Constructor } from "../types.ts"; 3 | 4 | import { PreProgrammedMethod } from "../pre_programmed_method.ts"; 5 | import { TestDoubleBuilder } from "../test_double_builder.ts"; 6 | import { createMock } from "./mock_mixin.ts"; 7 | 8 | /** 9 | * Builder to help build a mock object. This does all of the heavy-lifting to 10 | * create a mock object. Its `create()` method returns an instance of `Mock`, 11 | * which is basically an original object with added data members for verifying 12 | * behavior. 13 | */ 14 | export class MockBuilder extends TestDoubleBuilder { 15 | ////////////////////////////////////////////////////////////////////////////// 16 | // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// 17 | ////////////////////////////////////////////////////////////////////////////// 18 | 19 | /** 20 | * Create the mock object. 21 | * 22 | * @returns The original object with mocking capabilities. 23 | */ 24 | public create(): ClassToMock & IMock { 25 | const original = new this.constructor_fn(...this.constructor_args); 26 | 27 | const mock = createMock, ClassToMock>( 28 | this.constructor_fn, 29 | ...this.constructor_args, 30 | ); 31 | 32 | (mock as IMock & ITestDouble).init( 33 | original, 34 | this.getAllFunctionNames(original), 35 | ); 36 | 37 | // Attach all of the original's properties to the mock 38 | this.addOriginalProperties>(original, mock); 39 | 40 | // Attach all of the original's functions to the mock 41 | this.getAllFunctionNames(original).forEach((method: string) => { 42 | this.#addOriginalMethod( 43 | original, 44 | mock, 45 | method, 46 | ); 47 | }); 48 | 49 | return mock as ClassToMock & IMock; 50 | } 51 | 52 | ////////////////////////////////////////////////////////////////////////////// 53 | // FILE MARKER - METHODS - PRIVATE /////////////////////////////////////////// 54 | ////////////////////////////////////////////////////////////////////////////// 55 | 56 | /** 57 | * Add an original object's method to a mock object -- determining whether the 58 | * method should or should not be trackable. 59 | * 60 | * @param original - The original object containing the method to add. 61 | * @param mock - The mock object receiving the method. 62 | * @param method - The name of the method to mock -- callable via 63 | * `mock[method](...)`. 64 | */ 65 | #addOriginalMethod( 66 | original: ClassToMock, 67 | mock: IMock, 68 | method: string, 69 | ): void { 70 | // If this is a native method, then do not do anything fancy. Just add it to 71 | // the mock. 72 | if (this.native_methods.indexOf(method as string) !== -1) { 73 | return this.addOriginalMethodWithoutTracking( 74 | original, 75 | mock, 76 | method, 77 | ); 78 | } 79 | 80 | this.#addOriginalMethodAsProxy( 81 | original, 82 | mock, 83 | method as keyof ClassToMock, 84 | ); 85 | } 86 | 87 | /** 88 | * Adds the original method as a proxy, which can be configured during tests. 89 | * 90 | * @param original - The original object containing the method to add. 91 | * @param mock - The mock object receiving the method. 92 | * @param method - The name of the method. 93 | */ 94 | #addOriginalMethodAsProxy( 95 | original: ClassToMock, 96 | mock: IMock, 97 | method: keyof ClassToMock, 98 | ): void { 99 | Object.defineProperty(mock, method, { 100 | value: (...args: unknown[]) => { 101 | // Track that this method was called 102 | mock.calls[method]++; 103 | // TODO: copy spy approach because we need mock.expected_args 104 | 105 | // Make sure the method calls its original self 106 | const methodToCall = 107 | original[method as keyof ClassToMock] as unknown as ( 108 | ...params: unknown[] 109 | ) => unknown; 110 | 111 | // We need to check if the method was pre-preprogrammed to do something. 112 | // If it was, then we make sure that this method we are currently 113 | // defining returns that pre-programmed expectation. 114 | if (methodToCall instanceof PreProgrammedMethod) { 115 | return methodToCall.run(args); 116 | } 117 | 118 | // When method calls its original self, let the `this` context of the 119 | // original be the mock. Reason being the mock has tracking and the 120 | // original does not. 121 | const bound = methodToCall.bind(mock); 122 | 123 | // Use `return` because the original function could return a value 124 | return bound(...args); 125 | }, 126 | writable: true, 127 | }); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/mock/mock_mixin.ts: -------------------------------------------------------------------------------- 1 | import type { IMethodChanger, IMock } from "../interfaces.ts"; 2 | import type { Constructor, MethodOf, MockMethodCalls } from "../types.ts"; 3 | 4 | import { MethodVerifier } from "../verifiers/method_verifier.ts"; 5 | import { MockError } from "../errors.ts"; 6 | import { PreProgrammedMethod } from "../pre_programmed_method.ts"; 7 | 8 | /** 9 | * Class to help mocks create method expectations. 10 | */ 11 | class MethodExpectation { 12 | /** 13 | * Property to hold the number of expected calls this method should receive. 14 | */ 15 | #expected_calls?: number | undefined; 16 | 17 | /** 18 | * Property to hold the expected args this method should use. 19 | */ 20 | #expected_args?: unknown[] | undefined; 21 | 22 | /** 23 | * Property to hold the args this method was called with. 24 | */ 25 | #args?: unknown[] | undefined; 26 | 27 | /** 28 | * See `MethodVerifier#method_name`. 29 | */ 30 | #method_name: MethodOf; 31 | 32 | /** 33 | * The verifier to use when verifying expectations. 34 | */ 35 | #verifier: MethodVerifier; 36 | 37 | ////////////////////////////////////////////////////////////////////////////// 38 | // FILE MARKER - CONSTRUCTOR ///////////////////////////////////////////////// 39 | ////////////////////////////////////////////////////////////////////////////// 40 | 41 | /** 42 | * @param methodName - See `MethodVerifier#method_name`. 43 | */ 44 | constructor(methodName: MethodOf) { 45 | this.#method_name = methodName; 46 | this.#verifier = new MethodVerifier(methodName); 47 | } 48 | 49 | ////////////////////////////////////////////////////////////////////////////// 50 | // FILE MARKER - METHODS - PRIVATE /////////////////////////////////////////// 51 | ////////////////////////////////////////////////////////////////////////////// 52 | 53 | get method_name(): MethodOf { 54 | return this.#method_name; 55 | } 56 | 57 | ////////////////////////////////////////////////////////////////////////////// 58 | // FILE MARKER - METHODS - PRIVATE /////////////////////////////////////////// 59 | ////////////////////////////////////////////////////////////////////////////// 60 | 61 | /** 62 | * See `IMethodExpectation.toBeCalled()`. 63 | */ 64 | public toBeCalled(expectedCalls?: number): this { 65 | this.#expected_calls = expectedCalls; 66 | return this; 67 | } 68 | 69 | /** 70 | * See `IMethodExpectation.toBeCalledWithArgs()`. 71 | */ 72 | public toBeCalledWithArgs(...expectedArgs: unknown[]): this { 73 | this.#expected_args = expectedArgs; 74 | return this; 75 | } 76 | 77 | /** 78 | * See `IMethodExpectation.toBeCalledWithoutArgs()`. 79 | */ 80 | public toBeCalledWithoutArgs(): this { 81 | this.#expected_args = undefined; 82 | return this; 83 | } 84 | 85 | /** 86 | * Verify all expected calls were made. 87 | * 88 | * @param actualCalls - The number of actual calls. 89 | */ 90 | public verifyCalls(actualCalls: number): void { 91 | this.#verifier.toBeCalled(actualCalls, this.#expected_calls); 92 | this.#verifier.toBeCalledWithoutArgs(this.#args ?? []); 93 | } 94 | } 95 | 96 | /** 97 | * Create a mock object as an extension of an original object. 98 | * 99 | * @param OriginalClass - The class the mock should extend. 100 | * 101 | * @returns A mock object of the `OriginalClass`. 102 | */ 103 | export function createMock( 104 | OriginalClass: OriginalConstructor, 105 | ...originalConstructorArgs: unknown[] 106 | ): IMock { 107 | const Original = OriginalClass as unknown as Constructor< 108 | // deno-lint-ignore no-explicit-any 109 | (...args: any[]) => any 110 | >; 111 | return new class MockExtension extends Original { 112 | /** 113 | * See `IMock#is_mock`. 114 | */ 115 | is_mock = true; 116 | 117 | /** 118 | * See `IMock#calls`. 119 | */ 120 | #calls!: MockMethodCalls; 121 | 122 | /** 123 | * An array of expectations to verify (if any). 124 | */ 125 | #expectations: MethodExpectation[] = []; 126 | 127 | /** 128 | * The original object that this class creates a mock of. 129 | */ 130 | #original!: OriginalObject; 131 | 132 | /** 133 | * Map of pre-programmed methods defined by the user. 134 | */ 135 | #pre_programmed_methods: Map< 136 | MethodOf, 137 | IMethodChanger 138 | > = new Map(); 139 | 140 | //////////////////////////////////////////////////////////////////////////// 141 | // FILE MARKER - CONSTRUCTOR /////////////////////////////////////////////// 142 | //////////////////////////////////////////////////////////////////////////// 143 | 144 | constructor() { 145 | super(...originalConstructorArgs); 146 | } 147 | 148 | //////////////////////////////////////////////////////////////////////////// 149 | // FILE MARKER - GETTERS / SETTERS ///////////////////////////////////////// 150 | //////////////////////////////////////////////////////////////////////////// 151 | 152 | get calls(): MockMethodCalls { 153 | return this.#calls; 154 | } 155 | 156 | //////////////////////////////////////////////////////////////////////////// 157 | // FILE MARKER - METHODS - PUBLIC ////////////////////////////////////////// 158 | //////////////////////////////////////////////////////////////////////////// 159 | 160 | /** 161 | * @param original - The original object to mock. 162 | * @param methodsToTrack - The original object's method to make trackable. 163 | */ 164 | public init(original: OriginalObject, methodsToTrack: string[]) { 165 | this.#original = original; 166 | this.#calls = this.#constructCallsProperty(methodsToTrack); 167 | } 168 | 169 | /** 170 | * See `IMock.expects()`. 171 | */ 172 | public expects( 173 | method: MethodOf, 174 | ): MethodExpectation { 175 | const expectation = new MethodExpectation(method); 176 | this.#expectations.push(expectation); 177 | return expectation; 178 | } 179 | 180 | /** 181 | * See `IMock.method()`. 182 | */ 183 | public method( 184 | methodName: MethodOf, 185 | ): IMethodChanger { 186 | // If the method was already pre-programmed previously, then return it so 187 | // the user can add more method setups to it 188 | if (this.#pre_programmed_methods.has(methodName)) { 189 | return this.#pre_programmed_methods.get(methodName)!; 190 | } 191 | 192 | const methodConfiguration = new PreProgrammedMethod< 193 | OriginalObject, 194 | ReturnValueType 195 | >( 196 | methodName, 197 | ); 198 | 199 | if ( 200 | !((methodName as string) in (this.#original as Record)) 201 | ) { 202 | const typeSafeMethodName = String(methodName); 203 | throw new MockError( 204 | `Method "${typeSafeMethodName}" does not exist.`, 205 | ); 206 | } 207 | 208 | Object.defineProperty(this.#original, methodName, { 209 | value: methodConfiguration, 210 | writable: true, 211 | }); 212 | 213 | const methodChanger = methodConfiguration as unknown as IMethodChanger< 214 | ReturnValueType 215 | >; 216 | 217 | this.#pre_programmed_methods.set(methodName, methodChanger); 218 | 219 | return methodChanger; 220 | } 221 | 222 | /** 223 | * See `IMock.verifyExpectations()`. 224 | */ 225 | public verifyExpectations(): void { 226 | this.#expectations.forEach((e: MethodExpectation) => { 227 | e.verifyCalls(this.#calls[e.method_name]); 228 | }); 229 | } 230 | 231 | //////////////////////////////////////////////////////////////////////////// 232 | // FILE MARKER - METHODS - PRIVATE ///////////////////////////////////////// 233 | //////////////////////////////////////////////////////////////////////////// 234 | 235 | /** 236 | * Construct the calls property. Only construct it, do not set it. The 237 | * constructor will set it. 238 | * 239 | * @param methodsToTrack - All of the methods on the original object to make 240 | * trackable. 241 | * 242 | * @returns - Key-value object where the key is the method name and the value 243 | * is the number of calls. All calls start at 0. 244 | */ 245 | #constructCallsProperty( 246 | methodsToTrack: string[], 247 | ): Record { 248 | const calls: Partial> = {}; 249 | 250 | methodsToTrack.map((key: string) => { 251 | calls[key as keyof OriginalObject] = 0; 252 | }); 253 | 254 | return calls as Record; 255 | } 256 | }(); 257 | } 258 | -------------------------------------------------------------------------------- /src/pre_programmed_method.ts: -------------------------------------------------------------------------------- 1 | import type { IError, IMethodChanger } from "./interfaces.ts"; 2 | import type { MethodOf } from "./types.ts"; 3 | 4 | type ReturnValueFunction = (...args: unknown[]) => ReturnValue; 5 | 6 | /** 7 | * Class that allows to be a "stand-in" for a method. For example, when used in 8 | * a mock object, the mock object can replace methods with pre-programmed 9 | * methods (using this class), and have a system under test use the 10 | * pre-programmed methods. 11 | */ 12 | export class PreProgrammedMethod 13 | implements IMethodChanger { 14 | /** 15 | * The original name of the method being pre-programmed. 16 | */ 17 | #method_name: MethodOf; 18 | 19 | /** 20 | * The object containing the pre-programmed setup details. This object is what 21 | * runs under the hood to provide the pre-programmed expectation. 22 | */ 23 | #method_setup?: MethodSetup; 24 | 25 | ////////////////////////////////////////////////////////////////////////////// 26 | // FILE MARKER - CONSTRUCTOR ///////////////////////////////////////////////// 27 | ////////////////////////////////////////////////////////////////////////////// 28 | 29 | /** 30 | * @param methodName - The name of the method to program. Must be a method of 31 | * the original object in question. 32 | */ 33 | constructor(methodName: MethodOf) { 34 | this.#method_name = methodName; 35 | } 36 | 37 | ////////////////////////////////////////////////////////////////////////////// 38 | // FILE MARKER - METHODS - PUBLIC /////////////////////////////////////////// 39 | ////////////////////////////////////////////////////////////////////////////// 40 | 41 | // public willCall(action: (...args: unknown[]) => ReturnValue): void { 42 | // this.#method_setup = new MethodSetupCallsCallback( 43 | // this.#method_name, 44 | // action, 45 | // ); 46 | // } 47 | 48 | // public willReturn(action: (...args: unknown[]) => ReturnValue): void; 49 | 50 | public willReturn( 51 | returnValue: ReturnValue | ReturnValueFunction, 52 | ): void { 53 | if (typeof returnValue === "function") { 54 | this.#method_setup = new MethodSetupCallsCallback( 55 | this.#method_name, 56 | returnValue as ReturnValueFunction, 57 | ); 58 | return; 59 | } 60 | 61 | this.#method_setup = new MethodSetupReturnsStaticValue( 62 | this.#method_name, 63 | returnValue, 64 | ); 65 | } 66 | 67 | public willThrow(error: IError): void { 68 | this.#method_setup = new MethodSetupThrowsError( 69 | this.#method_name, 70 | error, 71 | ); 72 | } 73 | 74 | /** 75 | * Run this method. 76 | * @param args 77 | * @returns 78 | */ 79 | public run(args?: unknown[]): unknown { 80 | if (!this.#method_setup) { 81 | throw new Error( 82 | `Pre-programmed method "${ 83 | String(this.#method_name) 84 | }" does not have a return value.`, 85 | ); 86 | } 87 | return this.#method_setup?.run(args); 88 | } 89 | } 90 | 91 | enum MethodSetupExpectation { 92 | ExecuteCallback, 93 | ReturnStaticValue, 94 | ThrowError, 95 | } 96 | 97 | /** 98 | * Class to hold information about a specific pre-programmed method setup. 99 | */ 100 | abstract class MethodSetup { 101 | readonly id: string; 102 | protected expectation: MethodSetupExpectation; 103 | protected method_name: string; 104 | 105 | constructor( 106 | methodName: MethodOf, 107 | expectation: MethodSetupExpectation, 108 | ) { 109 | this.id = this.run.toString(); 110 | this.method_name = String(methodName); // Make the method name type-safe 111 | this.expectation = expectation; 112 | } 113 | 114 | abstract run(args?: unknown): unknown; 115 | } 116 | 117 | class MethodSetupThrowsError 118 | extends MethodSetup { 119 | #error: IError; 120 | 121 | constructor( 122 | methodName: MethodOf, 123 | returnValue: IError, 124 | ) { 125 | super(methodName, MethodSetupExpectation.ThrowError); 126 | this.#error = returnValue; 127 | } 128 | 129 | run(): void { 130 | throw this.#error; 131 | } 132 | } 133 | 134 | class MethodSetupReturnsStaticValue 135 | extends MethodSetup { 136 | #return_value: ReturnValue; 137 | 138 | constructor( 139 | methodName: MethodOf, 140 | returnValue: ReturnValue, 141 | ) { 142 | super(methodName, MethodSetupExpectation.ReturnStaticValue); 143 | this.#return_value = returnValue; 144 | } 145 | 146 | run(): ReturnValue { 147 | return this.#return_value; 148 | } 149 | } 150 | 151 | class MethodSetupCallsCallback< 152 | OriginalObject, 153 | Callback extends ((...args: unknown[]) => ReturnType), 154 | > extends MethodSetup { 155 | #callback: Callback; 156 | 157 | constructor( 158 | methodName: MethodOf, 159 | callback: Callback, 160 | ) { 161 | super(methodName, MethodSetupExpectation.ReturnStaticValue); 162 | this.#callback = callback; 163 | } 164 | 165 | run(args: unknown[]): ReturnType { 166 | return this.#callback(...args); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/spy/spy_builder.ts: -------------------------------------------------------------------------------- 1 | import type { Constructor } from "../types.ts"; 2 | import type { ISpy, ITestDouble } from "../interfaces.ts"; 3 | import { createSpy } from "./spy_mixin.ts"; 4 | import { TestDoubleBuilder } from "../test_double_builder.ts"; 5 | 6 | /** 7 | * Builder to help build a spy object. This does all of the heavy-lifting to 8 | * create a spy object. Its `create()` method returns an instance of `Spy`, 9 | * which is basically an original object with stubbed data members. 10 | * 11 | * This builder differs from the `SpyStub` because it stubs out the entire 12 | * class, whereas the `SpyStub` stubs specific data members. 13 | * 14 | * Under the hood, this builder uses `SpyStub` to stub the data members in the 15 | * class. 16 | */ 17 | export class SpyBuilder extends TestDoubleBuilder { 18 | ////////////////////////////////////////////////////////////////////////////// 19 | // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// 20 | ////////////////////////////////////////////////////////////////////////////// 21 | 22 | /** 23 | * Create the spy object. 24 | * 25 | * @returns The original object with capabilities from the Spy class. 26 | */ 27 | public create(): ClassToSpy { 28 | const original = new this.constructor_fn(...this.constructor_args); 29 | 30 | const spy = createSpy, ClassToSpy>( 31 | this.constructor_fn, 32 | ...this.constructor_args, 33 | ); 34 | 35 | (spy as ISpy & ITestDouble).init( 36 | original, 37 | this.getAllFunctionNames(original), 38 | ); 39 | 40 | // Attach all of the original's properties to the spy 41 | this.addOriginalProperties>(original, spy); 42 | 43 | // Attach all of the original's native methods to the spy 44 | this.#addNativeMethods(original, spy); 45 | 46 | return spy as ClassToSpy & ISpy; 47 | } 48 | 49 | ////////////////////////////////////////////////////////////////////////////// 50 | // FILE MARKER - METHODS - PRIVATE /////////////////////////////////////////// 51 | ////////////////////////////////////////////////////////////////////////////// 52 | 53 | /** 54 | * Add an original object's method to a spy object -- determining whether the 55 | * method should or should not be trackable. 56 | * 57 | * @param original - The original object containing the method to add. 58 | * @param spy - The spy object receiving the method. 59 | */ 60 | #addNativeMethods( 61 | original: ClassToSpy, 62 | spy: ISpy, 63 | ): void { 64 | this.getAllFunctionNames(original).forEach((method: string) => { 65 | // If this is not a native method, then we do not care to add it because all 66 | // methods should be stubbed in `SpyExtension#init()`. The `init()` method 67 | // sets `#stubbed_methods` to `SpyStub` objects with a return value of 68 | // "stubbed". 69 | if (this.native_methods.indexOf(method as string) === -1) { 70 | return; 71 | } 72 | 73 | return this.addOriginalMethodWithoutTracking( 74 | original, 75 | spy, 76 | method, 77 | ); 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/spy/spy_mixin.ts: -------------------------------------------------------------------------------- 1 | import type { Constructor, MethodOf } from "../types.ts"; 2 | import type { IMethodVerifier, ISpy, ISpyStubMethod } from "../interfaces.ts"; 3 | import { SpyStubBuilder } from "./spy_stub_builder.ts"; 4 | 5 | /** 6 | * Create a spy as an extension of an original object. 7 | */ 8 | export function createSpy( 9 | OriginalClass: OriginalConstructor, 10 | ...originalConstructorArgs: unknown[] 11 | ): ISpy { 12 | const Original = OriginalClass as unknown as Constructor< 13 | // deno-lint-ignore no-explicit-any 14 | (...args: any[]) => any 15 | >; 16 | return new class SpyExtension extends Original { 17 | /** 18 | * See `ISpy#is_spy`. 19 | */ 20 | is_spy = true; 21 | 22 | /** 23 | * See `ISpy#stubbed_methods`. 24 | */ 25 | #stubbed_methods!: Record, ISpyStubMethod>; 26 | 27 | /** 28 | * The original object that this class creates a spy out of. 29 | */ 30 | #original!: OriginalObject; 31 | 32 | //////////////////////////////////////////////////////////////////////////// 33 | // FILE MARKER - CONSTRUCTOR /////////////////////////////////////////////// 34 | //////////////////////////////////////////////////////////////////////////// 35 | 36 | constructor() { 37 | super(...originalConstructorArgs); 38 | } 39 | 40 | //////////////////////////////////////////////////////////////////////////// 41 | // FILE MARKER - GETTERS / SETTERS ///////////////////////////////////////// 42 | //////////////////////////////////////////////////////////////////////////// 43 | 44 | get stubbed_methods(): Record, ISpyStubMethod> { 45 | return this.#stubbed_methods; 46 | } 47 | 48 | //////////////////////////////////////////////////////////////////////////// 49 | // FILE MARKER - METHODS - PUBLIC ////////////////////////////////////////// 50 | //////////////////////////////////////////////////////////////////////////// 51 | 52 | /** 53 | * @param original - The original object to create a spy out of. 54 | * @param methodsToTrack - The original object's method to make trackable. 55 | */ 56 | public init(original: OriginalObject, methodsToTrack: string[]) { 57 | this.#original = original; 58 | this.#stubbed_methods = this.#constructStubbedMethodsProperty( 59 | methodsToTrack, 60 | ); 61 | } 62 | 63 | /** 64 | * Get the verifier for the given method to do actual verification. 65 | */ 66 | public verify( 67 | methodName: MethodOf, 68 | ): IMethodVerifier { 69 | return this.#stubbed_methods[methodName].verify(); 70 | } 71 | 72 | //////////////////////////////////////////////////////////////////////////// 73 | // FILE MARKER - METHODS - PRIVATE ///////////////////////////////////////// 74 | //////////////////////////////////////////////////////////////////////////// 75 | 76 | /** 77 | * Construct the calls property. Only construct it, do not set it. The 78 | * constructor will set it. 79 | * 80 | * @param methodsToTrack - All of the methods on the original object to make 81 | * trackable. 82 | * 83 | * @returns - Key-value object where the key is the method name and the 84 | * value is the number of calls. All calls start at 0. 85 | */ 86 | #constructStubbedMethodsProperty( 87 | methodsToTrack: string[], 88 | ): Record, ISpyStubMethod> { 89 | const stubbedMethods: Partial< 90 | Record, ISpyStubMethod> 91 | > = {}; 92 | 93 | methodsToTrack.forEach((method: string) => { 94 | const spyMethod = new SpyStubBuilder(this) 95 | .method(method as MethodOf) 96 | .returnValue("spy-stubbed") 97 | .createForObjectMethod(); 98 | stubbedMethods[method as MethodOf] = spyMethod; 99 | }); 100 | 101 | return stubbedMethods as Record< 102 | MethodOf, 103 | ISpyStubMethod 104 | >; 105 | } 106 | }(); 107 | } 108 | -------------------------------------------------------------------------------- /src/spy/spy_stub_builder.ts: -------------------------------------------------------------------------------- 1 | import type { MethodOf } from "../types.ts"; 2 | import type { 3 | IFunctionExpressionVerifier, 4 | IMethodVerifier, 5 | ISpyStubFunctionExpression, 6 | ISpyStubMethod, 7 | } from "../interfaces.ts"; 8 | import { MethodVerifier } from "../verifiers/method_verifier.ts"; 9 | import { FunctionExpressionVerifier } from "../verifiers/function_expression_verifier.ts"; 10 | 11 | /** 12 | * This class helps verifying function expression calls. It's a wrapper for 13 | * `FunctionExpressionVerifier` only to make the verification methods (e.g., 14 | * `toBeCalled()`) have a shorter syntax -- allowing the user of this library to 15 | * only pass in expected calls as opposed to expected calls and actual calls. 16 | */ 17 | class SpyStubFunctionExpressionVerifier extends FunctionExpressionVerifier { 18 | /** 19 | * See `IFunctionExpressionVerifier.args`. 20 | */ 21 | #args: unknown[]; 22 | 23 | /** 24 | * See `IFunctionExpressionVerifier.calls`. 25 | */ 26 | #calls: number; 27 | 28 | ////////////////////////////////////////////////////////////////////////////// 29 | // FILE MARKER - CONSTRUCTOR ///////////////////////////////////////////////// 30 | ////////////////////////////////////////////////////////////////////////////// 31 | 32 | /** 33 | * @param name - See `FunctionExpressionVerifier.#name`. 34 | * @param calls - See `IFunctionExpressionVerifier.calls`. 35 | * @param args - See `IFunctionExpressionVerifier.args`. 36 | */ 37 | constructor( 38 | name: string, 39 | calls: number, 40 | args: unknown[], 41 | ) { 42 | super(name); 43 | this.#calls = calls; 44 | this.#args = args; 45 | } 46 | 47 | ////////////////////////////////////////////////////////////////////////////// 48 | // FILE MARKER - METHODS - GETTERS / SETTERS ///////////////////////////////// 49 | ////////////////////////////////////////////////////////////////////////////// 50 | 51 | get args(): unknown[] { 52 | return this.#args; 53 | } 54 | 55 | get calls(): number { 56 | return this.#calls; 57 | } 58 | 59 | ////////////////////////////////////////////////////////////////////////////// 60 | // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// 61 | ////////////////////////////////////////////////////////////////////////////// 62 | 63 | /** 64 | * See `IVerifier.toBeCalled()`. 65 | */ 66 | public toBeCalled(expectedCalls?: number): this { 67 | return super.toBeCalled( 68 | this.#calls, 69 | expectedCalls, 70 | ); 71 | } 72 | 73 | /** 74 | * See `IVerifier.toBeCalledWithArgs()`. 75 | */ 76 | public toBeCalledWithArgs(...expectedArgs: unknown[]): this { 77 | return super.toBeCalledWithArgs( 78 | this.#args, 79 | expectedArgs, 80 | ); 81 | } 82 | 83 | /** 84 | * See `IVerifier.toBeCalledWithoutArgs()`. 85 | */ 86 | public toBeCalledWithoutArgs() { 87 | super.toBeCalledWithoutArgs( 88 | this.#args, 89 | ); 90 | return this; 91 | } 92 | } 93 | 94 | /** 95 | * This class helps verifying object method calls. It's a wrapper for 96 | * `MethodVerifier` only to make the verification methods (e.g., `toBeCalled()`) 97 | * have a shorter syntax -- allowing the user of this library to only pass in 98 | * expected calls as opposed to expected calls and actual calls. 99 | */ 100 | class SpyStubMethodVerifier 101 | extends MethodVerifier { 102 | /** 103 | * See `IMethodVerifier.args`. 104 | */ 105 | #args: unknown[]; 106 | 107 | /** 108 | * See `IMethodVerifier.calls`. 109 | */ 110 | #calls: number; 111 | 112 | ////////////////////////////////////////////////////////////////////////////// 113 | // FILE MARKER - CONSTRUCTOR ///////////////////////////////////////////////// 114 | ////////////////////////////////////////////////////////////////////////////// 115 | 116 | /** 117 | * @param methodName - See `MethodVerifier.method_name`. 118 | * @param calls - See `IMethodVerifier.calls`. 119 | * @param args - See `IMethodVerifier.args`. 120 | */ 121 | constructor( 122 | methodName: MethodOf, 123 | calls: number, 124 | args: unknown[], 125 | ) { 126 | super(methodName); 127 | this.#calls = calls; 128 | this.#args = args; 129 | } 130 | 131 | ////////////////////////////////////////////////////////////////////////////// 132 | // FILE MARKER - METHODS - GETTERS / SETTERS ///////////////////////////////// 133 | ////////////////////////////////////////////////////////////////////////////// 134 | 135 | get args(): unknown[] { 136 | return this.#args; 137 | } 138 | 139 | get calls(): number { 140 | return this.#calls; 141 | } 142 | 143 | ////////////////////////////////////////////////////////////////////////////// 144 | // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// 145 | ////////////////////////////////////////////////////////////////////////////// 146 | 147 | /** 148 | * See `IVerifier.toBeCalled()`. 149 | */ 150 | public toBeCalled(expectedCalls?: number): this { 151 | const calls = expectedCalls ?? ""; 152 | 153 | return super.toBeCalled( 154 | this.calls, 155 | expectedCalls, 156 | `.verify().toBeCalled(${calls})`, 157 | ); 158 | } 159 | 160 | /** 161 | * See `IVerifier.toBeCalledWithArgs()`. 162 | */ 163 | public toBeCalledWithArgs(...expectedArgs: unknown[]): this { 164 | const expectedArgsAsString = this.argsAsString(expectedArgs); 165 | 166 | return super.toBeCalledWithArgs( 167 | this.args, 168 | expectedArgs, 169 | `.verify().toBeCalledWithArgs(${expectedArgsAsString})`, 170 | ); 171 | } 172 | 173 | /** 174 | * See `IVerifier.toBeCalledWithoutArgs()`. 175 | */ 176 | public toBeCalledWithoutArgs(): this { 177 | return super.toBeCalledWithoutArgs( 178 | this.args, 179 | `.verify().toBeCalledWithoutArgs()`, 180 | ); 181 | } 182 | } 183 | 184 | /** 185 | * Builder to help build the following: 186 | * 187 | * - Spies for object methods (e.g., `someObject.someMethod()`). 188 | * - Spies for function expressions (e.g., `const hello = () => "world"`). 189 | */ 190 | export class SpyStubBuilder { 191 | /** 192 | * Property to hold the number of time this method was called. 193 | */ 194 | #calls = 0; 195 | 196 | /** 197 | * Property to hold the args this method was last called with. 198 | */ 199 | #last_called_with_args: unknown[] = []; 200 | 201 | /** 202 | * The name of the method this spy is replacing. 203 | */ 204 | #method?: MethodOf; 205 | 206 | /** 207 | * The original object. 208 | */ 209 | #original: OriginalObject; 210 | 211 | /** 212 | * (Optional) The return value to return when the method this spy replaces is 213 | * called. 214 | */ 215 | #return_value?: ReturnValue; 216 | 217 | ////////////////////////////////////////////////////////////////////////////// 218 | // FILE MARKER - CONSTRUCTOR ///////////////////////////////////////////////// 219 | ////////////////////////////////////////////////////////////////////////////// 220 | 221 | /** 222 | * @param original - The original object containing the method to spy on. 223 | * @param method - The method to spy on. 224 | * @param returnValue (Optional) Specify the return value of the method. 225 | * Defaults to "stubbed". 226 | */ 227 | constructor(original: OriginalObject) { 228 | this.#original = original; 229 | } 230 | 231 | ////////////////////////////////////////////////////////////////////////////// 232 | // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// 233 | ////////////////////////////////////////////////////////////////////////////// 234 | 235 | /** 236 | * Set the name of the method this spy stub is stubbing. 237 | * 238 | * @param method - The name of the method. This must be a method of the 239 | * original object. 240 | * 241 | * @returns `this` To allow method chaining. 242 | */ 243 | public method(method: MethodOf): this { 244 | this.#method = method; 245 | return this; 246 | } 247 | 248 | /** 249 | * Set the return value of this spy stub. 250 | * 251 | * @param returnValue - The value to return when this spy stub is called. 252 | * 253 | * @returns `this` To allow method chaining. 254 | */ 255 | public returnValue(returnValue: ReturnValue): this { 256 | this.#return_value = returnValue; 257 | return this; 258 | } 259 | 260 | /** 261 | * Create this spy stub for an object's method. 262 | * 263 | * @returns `this` behind the `ISpyStubMethod` interface so that only 264 | * `ISpyStubMethod` data members can be seen/called. 265 | */ 266 | public createForObjectMethod(): ISpyStubMethod { 267 | this.#stubOriginalMethodWithTracking(); 268 | 269 | Object.defineProperty(this, "verify", { 270 | value: () => 271 | new SpyStubMethodVerifier( 272 | this.#method!, 273 | this.#calls, 274 | this.#last_called_with_args, 275 | ), 276 | }); 277 | 278 | return this as unknown as ISpyStubMethod & { verify: IMethodVerifier }; 279 | } 280 | 281 | /** 282 | * Create this spy stub for a function expression. 283 | * 284 | * @returns `this` behind the `ISpyStubFunctionExpression` interface so that 285 | * only `ISpyStubFunctionExpression` data members can be seen/called. 286 | */ 287 | public createForFunctionExpression(): ISpyStubFunctionExpression { 288 | const ret = (...args: unknown[]) => { 289 | this.#calls++; 290 | this.#last_called_with_args = args; 291 | return this.#return_value ?? "spy-stubbed"; 292 | }; 293 | 294 | ret.verify = (): IFunctionExpressionVerifier => 295 | new SpyStubFunctionExpressionVerifier( 296 | (this.#original as unknown as { name: string }).name, 297 | this.#calls, 298 | this.#last_called_with_args, 299 | ); 300 | 301 | return ret; 302 | } 303 | 304 | ////////////////////////////////////////////////////////////////////////////// 305 | // FILE MARKER - METHODS - PRIVATE /////////////////////////////////////////// 306 | ////////////////////////////////////////////////////////////////////////////// 307 | 308 | /** 309 | * Stub the original to have tracking. 310 | */ 311 | #stubOriginalMethodWithTracking(): void { 312 | if (!this.#method) { 313 | throw new Error( 314 | `Cannot create a spy stub for an object's method without providing the object and method name.`, 315 | ); 316 | } 317 | 318 | Object.defineProperty(this.#original, this.#method, { 319 | value: (...args: unknown[]) => { 320 | this.#calls++; 321 | this.#last_called_with_args = args; 322 | 323 | return this.#return_value !== undefined 324 | ? this.#return_value 325 | : "spy-stubbed"; 326 | }, 327 | writable: true, 328 | }); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/test_double_builder.ts: -------------------------------------------------------------------------------- 1 | import type { Constructor } from "./types.ts"; 2 | 3 | /** 4 | * This class contains the methods to help build most of what a test double 5 | * needs. 6 | */ 7 | export class TestDoubleBuilder { 8 | /** 9 | * The constructor function of the objet to create a test double out of. 10 | */ 11 | protected constructor_fn: Constructor; 12 | 13 | /** 14 | * A list of args the class constructor takes. This is used to instantiate the 15 | * original with args (if needed). 16 | */ 17 | protected constructor_args: unknown[] = []; 18 | 19 | /** 20 | * A list of native methods on an original object that should not be modified. 21 | * When adding an original's methods to a test double, the copying process 22 | * uses this array to skip adding these native methods with tracking. There is 23 | * no reason to track these methods. 24 | */ 25 | protected native_methods = [ 26 | "__defineGetter__", 27 | "__defineSetter__", 28 | "__lookupGetter__", 29 | "__lookupSetter__", 30 | "constructor", 31 | "hasOwnProperty", 32 | "isPrototypeOf", 33 | "propertyIsEnumerable", 34 | "toLocaleString", 35 | "toString", 36 | "valueOf", 37 | ]; 38 | 39 | ////////////////////////////////////////////////////////////////////////////// 40 | // FILE MARKER - METHODS - CONSTRUCTOR /////////////////////////////////////// 41 | ////////////////////////////////////////////////////////////////////////////// 42 | 43 | /** 44 | * Construct an object of this class. 45 | * 46 | * @param constructorFn - See `this.constructor_fn`. 47 | */ 48 | constructor(constructorFn: Constructor) { 49 | this.constructor_fn = constructorFn; 50 | } 51 | 52 | ////////////////////////////////////////////////////////////////////////////// 53 | // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// 54 | ////////////////////////////////////////////////////////////////////////////// 55 | 56 | /** 57 | * Before constructing the fake object, track any constructor function args 58 | * that need to be passed in when constructing the fake object. 59 | * 60 | * @param args - A rest parameter of args that will get passed in to the 61 | * constructor function of the object being faked. 62 | * 63 | * @returns `this` so that methods in this class can be chained. 64 | */ 65 | public withConstructorArgs(...args: unknown[]): this { 66 | this.constructor_args = args; 67 | return this; 68 | } 69 | 70 | ////////////////////////////////////////////////////////////////////////////// 71 | // FILE MARKER - METHODS - PROTECTED ///////////////////////////////////////// 72 | ////////////////////////////////////////////////////////////////////////////// 73 | 74 | /** 75 | * Get all functions from the original so they can be added to the test 76 | * double. 77 | * 78 | * @param obj - The object that will be a test double. 79 | * 80 | * @returns An array of the object's functions. 81 | */ 82 | protected getAllFunctionNames(obj: OriginalClass): string[] { 83 | let functions: string[] = []; 84 | let clone = obj; 85 | 86 | // This do-while loop iterates over all of the parent classes of the 87 | // original object (if any). It gets all of the functions from the parent 88 | // class currently being iterated over, adds them to the `functions` array 89 | // variable, and repeats this process until it reaches the top-level parent. 90 | // An example is below: 91 | // 92 | // class A {} 93 | // class B extends A {} 94 | // class C extends B {} 95 | // class D extends C {} 96 | // 97 | // let clone = new D(); 98 | // 99 | // do { 100 | // functions = functions.concat(Object.getOwnPropertyNames(clone)); 101 | // // Iteration 1 ---> D {} // Adds all functions 102 | // // Iteration 2 ---> D {} // Adds all functions 103 | // // Iteration 3 ---> C {} // Adds all functions 104 | // // Iteration 4 ---> B {} // Adds all functions 105 | // // Iteration 5 ---> A {} // Adds all functions 106 | // // Iteration 6 ---> {} // Adss all functions and stops here 107 | // } while ((clone = Object.getPrototypeOf(clone))); 108 | // 109 | do { 110 | functions = functions.concat(Object.getOwnPropertyNames(clone)); 111 | } while ((clone = Object.getPrototypeOf(clone))); 112 | 113 | return functions.sort().filter( 114 | function (e: string, i: number, arr: unknown[]) { 115 | if ( 116 | e != arr[i + 1] && typeof obj[e as keyof OriginalClass] == "function" 117 | ) { 118 | return true; 119 | } 120 | }, 121 | ); 122 | } 123 | 124 | /** 125 | * Get all properties from the original so they can be added to the test 126 | * double. 127 | * 128 | * @param obj - The object that will be a test double. 129 | * 130 | * @returns An array of the object's properties. 131 | */ 132 | protected getAllPropertyNames(obj: OriginalClass): string[] { 133 | let properties: string[] = []; 134 | let clone = obj; 135 | 136 | // This do-while loop iterates over all of the parent classes of the 137 | // original object (if any). It gets all of the properties from the parent 138 | // class currently being iterated over, adds them to the `properties` array 139 | // variable, and repeats this process until it reaches the top-level parent. 140 | // An example is below: 141 | // 142 | // class A {} 143 | // class B extends A {} 144 | // class C extends B {} 145 | // class D extends C {} 146 | // 147 | // let clone = new D(); 148 | // 149 | // do { 150 | // properties = properties.concat(Object.getOwnPropertyNames(clone)); 151 | // // Iteration 1 ---> D {} // Adds all properties 152 | // // Iteration 2 ---> D {} // Adds all properties 153 | // // Iteration 3 ---> C {} // Adds all properties 154 | // // Iteration 4 ---> B {} // Adds all properties 155 | // // Iteration 5 ---> A {} // Adds all properties 156 | // // Iteration 6 ---> {} // Adss all properties and stops here 157 | // } while ((clone = Object.getPrototypeOf(clone))); 158 | // 159 | do { 160 | properties = properties.concat(Object.getOwnPropertyNames(clone)); 161 | } while ((clone = Object.getPrototypeOf(clone))); 162 | 163 | return properties.sort().filter( 164 | function (e: string, i: number, arr: unknown[]) { 165 | if ( 166 | e != arr[i + 1] && typeof obj[e as keyof OriginalClass] != "function" 167 | ) { 168 | return true; 169 | } 170 | }, 171 | ); 172 | } 173 | 174 | /** 175 | * Add an original object's method to a a test double without doing anything 176 | * else. 177 | * 178 | * @param original - The original object containing the method. 179 | * @param testDouble - The test double object receiving the method. 180 | * @param method - The name of the method to fake -- callable via 181 | * `testDouble[method](...)`. 182 | */ 183 | protected addOriginalMethodWithoutTracking( 184 | original: OriginalClass, 185 | testDouble: TestDoubleInterface, 186 | method: string, 187 | ): void { 188 | Object.defineProperty(testDouble, method, { 189 | value: original[method as keyof OriginalClass], 190 | }); 191 | } 192 | 193 | /** 194 | * Add an original object's properties to the test double. 195 | * 196 | * @param original The original object containing the property. 197 | * @param testDouble The test double object receiving the property. 198 | */ 199 | protected addOriginalProperties( 200 | original: OriginalClass, 201 | testDouble: TestDoubleInterface, 202 | ): void { 203 | this.getAllPropertyNames(original).forEach((property: string) => { 204 | this.addOriginalProperty( 205 | original, 206 | testDouble, 207 | property, 208 | ); 209 | }); 210 | } 211 | 212 | /** 213 | * Add an original object's property to the test double. 214 | * 215 | * @param original The original object containing the property. 216 | * @param testDouble The test double object receiving the property. 217 | * @param property The name of the property -- retrievable via 218 | * `testDouble[property]`. 219 | */ 220 | protected addOriginalProperty( 221 | original: OriginalClass, 222 | testDouble: TestDoubleInterface, 223 | property: string, 224 | ): void { 225 | const desc = Object.getOwnPropertyDescriptor(original, property) ?? 226 | Object.getOwnPropertyDescriptor( 227 | this.constructor_fn.prototype, 228 | property, 229 | ); 230 | 231 | // If we do not have a desc, then we have no idea what the value should be. 232 | // Also, we have no idea what we are copying, so we should just not do it. 233 | if (!desc) { 234 | return; 235 | } 236 | 237 | // Basic property (e.g., public test = "hello"). We do not handle get() and 238 | // set() because those are handled by the test double mixin. 239 | if (("value" in desc)) { 240 | Object.defineProperty(testDouble, property, { 241 | value: desc.value, 242 | writable: true, 243 | }); 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Describes the type as something that is callable. 3 | */ 4 | // deno-lint-ignore no-explicit-any 5 | export type Callable = (...args: any[]) => ReturnValue; 6 | 7 | /** 8 | * Describes the type as a constructable object using the `new` keyword. 9 | */ 10 | // deno-lint-ignore no-explicit-any 11 | export type Constructor = new (...args: any[]) => Class; 12 | 13 | /** 14 | * Describes the `MockExtension#calls` property. 15 | * 16 | * This is a record where the key is the method that was called and the value is 17 | * the number of times the method was called. 18 | */ 19 | export type MockMethodCalls = Record; 20 | 21 | /** 22 | * Describes the type as a method of the given generic `Object`. 23 | * 24 | * This is used for type-hinting in places like `.verify("someMethod")`. 25 | */ 26 | export type MethodOf = { 27 | // deno-lint-ignore no-explicit-any 28 | [K in keyof Object]: Object[K] extends (...args: any[]) => unknown ? K 29 | : never; 30 | }[keyof Object]; 31 | -------------------------------------------------------------------------------- /src/verifiers/callable_verifier.ts: -------------------------------------------------------------------------------- 1 | import { VerificationError } from "../errors.ts"; 2 | 3 | export class CallableVerifier { 4 | ////////////////////////////////////////////////////////////////////////////// 5 | // FILE MARKER - METHODS - PROTECTED ///////////////////////////////////////// 6 | ////////////////////////////////////////////////////////////////////////////// 7 | 8 | /** 9 | * Make a user friendly version of the args. This is for display in the 10 | * `VerificationError` stack trace. For example, the original args ... 11 | * 12 | * [true, false, "hello"] 13 | * 14 | * ... becomes ... 15 | * 16 | * true, false, "hello" 17 | * 18 | * The above will ultimately end up in stack trace messages like: 19 | * 20 | * .toBeCalledWith(true, false, "hello") 21 | * 22 | * If we do not do this, then the following stack trace message will show, 23 | * which is not really clear because the args are in an array and the 24 | * "hello" string has its quotes missing: 25 | * 26 | * .toBeCalledWith([true, false, hello]) 27 | * 28 | * @param args - The args to convert to a string. 29 | * 30 | * @returns The args as a string. 31 | */ 32 | protected argsAsString(args: unknown[]): string { 33 | return JSON.stringify(args) 34 | .slice(1, -1) 35 | .replace(/,/g, ", "); 36 | } 37 | 38 | /** 39 | * Same as `this.argsAsString()`, but add typings to the args. For example: 40 | * 41 | * [true, false, "hello"] 42 | * 43 | * ... becomes ... 44 | * 45 | * true, false, "hello" 46 | * 47 | * @param args - The args to convert to a string. 48 | * 49 | * @returns The args as a string with typings. 50 | */ 51 | protected argsAsStringWithTypes(args: unknown[]): string { 52 | return args.map((arg: unknown) => { 53 | return `${JSON.stringify(arg)}${this.getArgType(arg)}`; 54 | }).join(", "); 55 | } 56 | 57 | /** 58 | * Get the arg type in string format for the given arg. 59 | * 60 | * @param arg - The arg to evaluate. 61 | * 62 | * @returns The arg type surrounded by brackets (e.g., ). 63 | */ 64 | protected getArgType(arg: unknown): string { 65 | if (arg && typeof arg === "object") { 66 | if ("prototype" in arg) { 67 | return "<" + Object.getPrototypeOf(arg) + ">"; 68 | } 69 | return ""; 70 | } 71 | 72 | return "<" + typeof arg + ">"; 73 | } 74 | 75 | /** 76 | * Verify that the number of actual calls matches the number of expected 77 | * calls. 78 | * 79 | * @param actualCalls - The actual number of calls. 80 | * @param expectedCalls - The expected number of calls. 81 | * @param errorMessage - The error message to show in the stack trace. 82 | * @param codeThatThrew - The code using this verification. 83 | * 84 | * @returns `this` To allow method chaining. 85 | */ 86 | protected verifyToBeCalled( 87 | actualCalls: number, 88 | expectedCalls: number | undefined, 89 | errorMessage: string, 90 | codeThatThrew: string, 91 | ): void { 92 | // If expected calls were not specified, then just check that the method was 93 | // called at least once 94 | if (!expectedCalls) { 95 | if (actualCalls > 0) { 96 | return; 97 | } 98 | 99 | throw new VerificationError( 100 | errorMessage, 101 | codeThatThrew, 102 | `Actual calls -> 0`, 103 | `Expected calls -> 1 (or more)`, 104 | ); 105 | } 106 | 107 | // If we get here, then we gucci. No need to process further. 108 | if (actualCalls === expectedCalls) { 109 | return; 110 | } 111 | 112 | // If we get here, then the actual number of calls do not match the expected 113 | // number of calls, so we should throw an error 114 | throw new VerificationError( 115 | errorMessage, 116 | codeThatThrew, 117 | `Actual calls -> ${actualCalls}`, 118 | `Expected calls -> ${expectedCalls}`, 119 | ); 120 | } 121 | 122 | /** 123 | * Verify that the number of expected args is not more than the actual args. 124 | * 125 | * @param actualArgs - The actual args. 126 | * @param expectedArgs - The expected args. 127 | * @param errorMessage - The error message to show in the stack trace. 128 | * @param codeThatThrew - The code using this verification. 129 | */ 130 | protected verifyToBeCalledWithArgsTooManyArgs( 131 | actualArgs: unknown[], 132 | expectedArgs: unknown[], 133 | errorMessage: string, 134 | codeThatThrew: string, 135 | ): void { 136 | if (expectedArgs.length > actualArgs.length) { 137 | throw new VerificationError( 138 | errorMessage, 139 | codeThatThrew, 140 | `Actual args -> ${ 141 | actualArgs.length > 0 142 | ? `(${this.argsAsStringWithTypes(actualArgs)})` 143 | : "(no args)" 144 | }`, 145 | `Expected args -> ${this.argsAsStringWithTypes(expectedArgs)}`, 146 | ); 147 | } 148 | } 149 | 150 | /** 151 | * Verify that the number of expected args is not less than the actual args. 152 | * 153 | * @param actualArgs - The actual args. 154 | * @param expectedArgs - The expected args. 155 | * @param errorMessage - The error message to show in the stack trace. 156 | * @param codeThatThrew - The code using this verification. 157 | */ 158 | protected verifyToBeCalledWithArgsTooFewArgs( 159 | actualArgs: unknown[], 160 | expectedArgs: unknown[], 161 | errorMessage: string, 162 | codeThatThrew: string, 163 | ): void { 164 | if (expectedArgs.length < actualArgs.length) { 165 | throw new VerificationError( 166 | errorMessage, 167 | codeThatThrew, 168 | `Actual call -> (${this.argsAsStringWithTypes(actualArgs)})`, 169 | `Expected call -> (${this.argsAsStringWithTypes(expectedArgs)})`, 170 | ); 171 | } 172 | } 173 | 174 | /** 175 | * Verify that the expected args match the actual args by value and type. 176 | * 177 | * @param actualArgs - The actual args. 178 | * @param expectedArgs - The expected args. 179 | * @param errorMessage - The error message to show in the stack trace. 180 | * @param codeThatThrew - The code using this verification. 181 | */ 182 | protected verifyToBeCalledWithArgsUnexpectedValues( 183 | actualArgs: unknown[], 184 | expectedArgs: unknown[], 185 | errorMessage: string, 186 | codeThatThrew: string, 187 | ): void { 188 | expectedArgs.forEach((arg: unknown, index: number) => { 189 | const parameterPosition = index + 1; 190 | 191 | // Args match? We gucci. 192 | if (actualArgs[index] === arg) { 193 | return; 194 | } 195 | 196 | // Args do not match? Check if we are comparing arrays and see if they 197 | // match 198 | if (this.#comparingArrays(actualArgs[index], arg)) { 199 | const match = this.#compareArrays( 200 | actualArgs[index] as unknown[], 201 | arg as unknown[], 202 | ); 203 | // Arrays match? We gucci. 204 | if (match) { 205 | return; 206 | } 207 | } 208 | 209 | // Alright, we have an unexpected arg, so throw an error 210 | const unexpectedArg = `\`${actualArgs[index]}${ 211 | this.getArgType(actualArgs[index]) 212 | }\``; 213 | 214 | throw new VerificationError( 215 | errorMessage 216 | .replace("{{ unexpected_arg }}", unexpectedArg) 217 | .replace("{{ parameter_position }}", parameterPosition.toString()), 218 | codeThatThrew, 219 | `Actual call -> (${this.argsAsStringWithTypes(actualArgs)})`, 220 | `Expected call -> (${this.argsAsStringWithTypes(expectedArgs)})`, 221 | ); 222 | }); 223 | } 224 | 225 | /** 226 | * Verify that the no args were used. 227 | * 228 | * @param actualArgs - The actual args (if any). 229 | * @param errorMessage - The error message to show in the stack trace. 230 | * @param codeThatThrew - The code using this verification. 231 | */ 232 | protected verifyToBeCalledWithoutArgs( 233 | actualArgs: unknown[], 234 | errorMessage: string, 235 | codeThatThrew: string, 236 | ): this { 237 | const actualArgsAsString = JSON.stringify(actualArgs) 238 | .slice(1, -1) 239 | .replace(/,/g, ", "); 240 | 241 | if (actualArgs.length === 0) { 242 | return this; 243 | } 244 | 245 | throw new VerificationError( 246 | errorMessage, 247 | codeThatThrew, 248 | `Actual args -> (${actualArgsAsString})`, 249 | `Expected args -> (no args)`, 250 | ); 251 | } 252 | 253 | ////////////////////////////////////////////////////////////////////////////// 254 | // FILE MARKER - METHODS - PRIVATE /////////////////////////////////////////// 255 | ////////////////////////////////////////////////////////////////////////////// 256 | 257 | /** 258 | * Check that the given arrays are exactly equal. 259 | * 260 | * @param a - The first array. 261 | * @param b - The second array (which should match the first array). 262 | * 263 | * @returns True if the arrays match, false if not. 264 | */ 265 | #compareArrays(a: unknown[], b: unknown[]): boolean { 266 | return a.length === b.length && a.every((val, index) => val === b[index]); 267 | } 268 | 269 | /** 270 | * Are we comparing arrays? 271 | * 272 | * @param obj1 - Object to evaluate if it is an array. 273 | * @param obj2 - Object to evaluate if it is an array. 274 | * 275 | * @returns True if yes, false if no. 276 | */ 277 | #comparingArrays(obj1: unknown, obj2: unknown): boolean { 278 | return Array.isArray(obj1) && Array.isArray(obj2); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/verifiers/function_expression_verifier.ts: -------------------------------------------------------------------------------- 1 | import { CallableVerifier } from "./callable_verifier.ts"; 2 | 3 | /** 4 | * Test doubles use this class to verify that their methods were called, were 5 | * called with a number of args, were called with specific types of args, and so 6 | * on. 7 | */ 8 | export class FunctionExpressionVerifier extends CallableVerifier { 9 | /** 10 | * The name of this function. 11 | */ 12 | #name: string; 13 | 14 | ////////////////////////////////////////////////////////////////////////////// 15 | // FILE MARKER - CONSTRUCTOR ///////////////////////////////////////////////// 16 | ////////////////////////////////////////////////////////////////////////////// 17 | 18 | /** 19 | * @param name - See `this.#name`. 20 | */ 21 | constructor(name: string) { 22 | super(); 23 | this.#name = name ?? null; 24 | } 25 | 26 | ////////////////////////////////////////////////////////////////////////////// 27 | // FILE MARKER - GETTERS / SETTERS /////////////////////////////////////////// 28 | ////////////////////////////////////////////////////////////////////////////// 29 | 30 | get name(): string { 31 | return this.#name; 32 | } 33 | 34 | ////////////////////////////////////////////////////////////////////////////// 35 | // FILE MARKER - METHODS - PROTECTED ///////////////////////////////////////// 36 | ////////////////////////////////////////////////////////////////////////////// 37 | 38 | /** 39 | * Verify that the actual calls match the expected calls. 40 | * 41 | * @param actualCalls - The number of actual calls. 42 | * @param expectedCalls - The number of calls expected. If this is -1, then 43 | * just verify that the method was called without checking how many times it 44 | * was called. 45 | */ 46 | public toBeCalled( 47 | actualCalls: number, 48 | expectedCalls?: number, 49 | ): this { 50 | const calls = expectedCalls ?? ""; 51 | 52 | const errorMessage = expectedCalls 53 | ? `Function "${this.#name}" was not called ${calls} time(s).` 54 | : `Function "${this.#name}" was not called.`; 55 | 56 | this.verifyToBeCalled( 57 | actualCalls, 58 | expectedCalls, 59 | errorMessage, 60 | `.verify().toBeCalled(${calls})`, 61 | ); 62 | 63 | return this; 64 | } 65 | 66 | /** 67 | * Verify that this method was called with the given args. 68 | * 69 | * @param actualArgs - The actual args that this method was called with. 70 | * @param expectedArgs - The args this method is expected to have received. 71 | */ 72 | public toBeCalledWithArgs( 73 | actualArgs: unknown[], 74 | expectedArgs: unknown[], 75 | ): this { 76 | const expectedArgsAsString = this.argsAsString(expectedArgs); 77 | const codeThatThrew = 78 | `.verify().toBeCalledWithArgs(${expectedArgsAsString})`; 79 | 80 | this.verifyToBeCalledWithArgsTooManyArgs( 81 | actualArgs, 82 | expectedArgs, 83 | `Function "${this.#name}" received too many args.`, 84 | codeThatThrew, 85 | ); 86 | 87 | this.verifyToBeCalledWithArgsTooFewArgs( 88 | actualArgs, 89 | expectedArgs, 90 | `Function "${this.#name}" was called with ${actualArgs.length} arg(s) instead of ${expectedArgs.length}.`, 91 | codeThatThrew, 92 | ); 93 | 94 | this.verifyToBeCalledWithArgsUnexpectedValues( 95 | actualArgs, 96 | expectedArgs, 97 | `Function "${this.#name}" received unexpected arg {{ unexpected_arg }} at parameter position {{ parameter_position }}.`, 98 | codeThatThrew, 99 | ); 100 | 101 | return this; 102 | } 103 | 104 | /** 105 | * Verify that this method was called without args. 106 | * 107 | * @param actualArgs - The actual args that this method was called with. This 108 | * method expects it to be an empty array. 109 | */ 110 | public toBeCalledWithoutArgs( 111 | actualArgs: unknown[], 112 | ): this { 113 | return super.verifyToBeCalledWithoutArgs( 114 | actualArgs, 115 | `Function "${this.#name}" was called with args when expected to receive no args.`, 116 | `.verify().toBeCalledWithoutArgs()`, 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/verifiers/method_verifier.ts: -------------------------------------------------------------------------------- 1 | import type { MethodOf } from "../types.ts"; 2 | import { CallableVerifier } from "./callable_verifier.ts"; 3 | 4 | /** 5 | * Test doubles use this class to verify that their methods were called, were 6 | * called with a number of args, were called with specific types of args, and so 7 | * on. 8 | */ 9 | export class MethodVerifier extends CallableVerifier { 10 | /** 11 | * The name of the method using this class. This is only used for display in 12 | * error stack traces if this class throws. 13 | */ 14 | #method_name: MethodOf | null; 15 | 16 | ////////////////////////////////////////////////////////////////////////////// 17 | // FILE MARKER - CONSTRUCTOR ///////////////////////////////////////////////// 18 | ////////////////////////////////////////////////////////////////////////////// 19 | 20 | /** 21 | * @param methodName - See `this.#method_name`. 22 | */ 23 | constructor(methodName?: MethodOf) { 24 | super(); 25 | this.#method_name = methodName ?? null; 26 | } 27 | 28 | ////////////////////////////////////////////////////////////////////////////// 29 | // FILE MARKER - GETTERS / SETTERS /////////////////////////////////////////// 30 | ////////////////////////////////////////////////////////////////////////////// 31 | 32 | get method_name(): MethodOf | null { 33 | return this.#method_name; 34 | } 35 | 36 | ////////////////////////////////////////////////////////////////////////////// 37 | // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// 38 | ////////////////////////////////////////////////////////////////////////////// 39 | 40 | /** 41 | * Verify that the actual calls match the expected calls. 42 | * 43 | * @param actualCalls - The number of actual calls. 44 | * @param expectedCalls - The number of calls expected. If this is -1, then 45 | * just verify that the method was called without checking how many times it 46 | * was called. 47 | * @param codeThatThrew - (Optional) A custom display of code that throws. 48 | * 49 | * @returns `this` To allow method chaining. 50 | */ 51 | public toBeCalled( 52 | actualCalls: number, 53 | expectedCalls?: number, 54 | codeThatThrew?: string, 55 | ): this { 56 | const typeSafeMethod = String(this.#method_name); 57 | 58 | const calls = expectedCalls ?? ""; 59 | codeThatThrew = codeThatThrew ?? 60 | `.verify("${typeSafeMethod}").toBeCalled(${calls})`; 61 | 62 | const errorMessage = expectedCalls 63 | ? `Method "${typeSafeMethod}" was not called ${calls} time(s).` 64 | : `Method "${typeSafeMethod}" was not called.`; 65 | 66 | this.verifyToBeCalled( 67 | actualCalls, 68 | expectedCalls, 69 | errorMessage, 70 | codeThatThrew, 71 | ); 72 | 73 | return this; 74 | } 75 | 76 | /** 77 | * Verify that this method was called with the given args. 78 | * 79 | * @param actualArgs - The actual args that this method was called with. 80 | * @param expectedArgs - The args this method is expected to have received. 81 | * @param codeThatThrew - (Optional) A custom display of code that throws. 82 | * 83 | * @returns `this` To allow method chaining. 84 | */ 85 | public toBeCalledWithArgs( 86 | actualArgs: unknown[], 87 | expectedArgs: unknown[], 88 | codeThatThrew?: string, 89 | ): this { 90 | const typeSafeMethodName = String(this.#method_name); 91 | 92 | const expectedArgsAsString = this.argsAsString(expectedArgs); 93 | codeThatThrew = codeThatThrew ?? 94 | `.verify("${typeSafeMethodName}").toBeCalledWithArgs(${expectedArgsAsString})`; 95 | 96 | this.verifyToBeCalledWithArgsTooManyArgs( 97 | actualArgs, 98 | expectedArgs, 99 | `Method "${typeSafeMethodName}" received too many args.`, 100 | codeThatThrew, 101 | ); 102 | 103 | this.verifyToBeCalledWithArgsTooFewArgs( 104 | actualArgs, 105 | expectedArgs, 106 | `Method "${typeSafeMethodName}" was called with ${actualArgs.length} arg(s) instead of ${expectedArgs.length}.`, 107 | codeThatThrew, 108 | ); 109 | 110 | this.verifyToBeCalledWithArgsUnexpectedValues( 111 | actualArgs, 112 | expectedArgs, 113 | `Method "${typeSafeMethodName}" received unexpected arg {{ unexpected_arg }} at parameter position {{ parameter_position }}.`, 114 | codeThatThrew, 115 | ); 116 | 117 | return this; 118 | } 119 | 120 | /** 121 | * Verify that this method was called without args. 122 | * 123 | * @param actualArgs - The actual args that this method was called with. This 124 | * method expects it to be an empty array. 125 | * @param codeThatThrew - (Optional) A custom display of code that throws. 126 | * 127 | * @returns `this` To allow method chaining. 128 | */ 129 | public toBeCalledWithoutArgs( 130 | actualArgs: unknown[], 131 | codeThatThrew?: string, 132 | ): this { 133 | const typeSafeMethodName = String(this.#method_name); 134 | 135 | codeThatThrew = codeThatThrew ?? 136 | `.verify("${typeSafeMethodName}").toBeCalledWithoutArgs()`; 137 | 138 | this.verifyToBeCalledWithoutArgs( 139 | actualArgs, 140 | `Method "${typeSafeMethodName}" was called with args when expected to receive no args.`, 141 | codeThatThrew, 142 | ); 143 | 144 | return this; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/cjs/jest_assertions.js: -------------------------------------------------------------------------------- 1 | function assertEquals(actual, expected) { 2 | expect(actual).toStrictEqual(expected); 3 | } 4 | 5 | function assertThrows( 6 | actual, 7 | expected, 8 | message, 9 | ) { 10 | expect(actual).toThrow(new expected(message)); 11 | } 12 | 13 | module.exports = { 14 | assertEquals, 15 | assertThrows, 16 | }; 17 | -------------------------------------------------------------------------------- /tests/cjs/unit/mod/dummy.test.js: -------------------------------------------------------------------------------- 1 | const { Dummy, Mock } = require("../../../../lib/cjs/mod"); 2 | 3 | describe("Dummy()", () => { 4 | it("can fill parameter lists", () => { 5 | const mockServiceOne = Mock(ServiceOne).create(); 6 | const dummy3 = Dummy(ServiceThree); 7 | 8 | const resource = new Resource( 9 | mockServiceOne, 10 | Dummy(ServiceTwo), 11 | dummy3, 12 | ); 13 | 14 | resource.callServiceOne(); 15 | expect(mockServiceOne.calls.methodServiceOne).toBe(1); 16 | }); 17 | 18 | it("can be made without specifying constructor args", () => { 19 | const dummy = Dummy(Resource); 20 | expect(Object.getPrototypeOf(dummy)).toBe(Resource); 21 | }); 22 | }); 23 | 24 | class Resource { 25 | constructor( 26 | serviceOne, 27 | serviceTwo, 28 | serviceThree, 29 | ) { 30 | this.service_one = serviceOne; 31 | this.service_two = serviceTwo; 32 | this.service_three = serviceThree; 33 | } 34 | 35 | callServiceOne() { 36 | this.service_one.methodServiceOne(); 37 | } 38 | 39 | callServiceTwo() { 40 | this.service_two.methodServiceTwo(); 41 | } 42 | 43 | callServiceThree() { 44 | this.service_three.methodServiceThree(); 45 | } 46 | } 47 | 48 | class ServiceOne { 49 | methodServiceOne() { 50 | return "Method from ServiceOne was called."; 51 | } 52 | } 53 | 54 | class ServiceTwo { 55 | methodServiceTwo() { 56 | return "Method from ServiceTwo was called."; 57 | } 58 | } 59 | 60 | class ServiceThree { 61 | methodServiceThree() { 62 | return "Method from ServiceThree was called."; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/cjs/unit/mod/fake.test.js: -------------------------------------------------------------------------------- 1 | const { Fake } = require("../../../../lib/cjs/mod"); 2 | const { assertEquals } = require("../../jest_assertions"); 3 | 4 | describe("Fake()", () => { 5 | it("creates fake builder", () => { 6 | const fake = Fake(TestObjectOne); 7 | expect(fake.constructor.name).toBe("FakeBuilder"); 8 | }); 9 | 10 | describe(".create()", () => { 11 | it("creates fake object", () => { 12 | const fake = Fake(TestObjectTwo).create(); 13 | expect(fake.name).toBe(undefined); 14 | expect(fake.is_fake).toBe(true); 15 | }); 16 | }); 17 | 18 | describe(".withConstructorArgs(...)", () => { 19 | it("can take 1 arg", () => { 20 | const fake = Fake(TestObjectTwo) 21 | .withConstructorArgs("some name") 22 | .create(); 23 | expect(fake.name).toBe("some name"); 24 | expect(fake.is_fake).toBe(true); 25 | }); 26 | 27 | it("can take more than 1 arg", () => { 28 | const fake = Fake(TestObjectTwoMore) 29 | .withConstructorArgs("some name", ["hello"]) 30 | .create(); 31 | expect(fake.name).toBe("some name"); 32 | expect(fake.array).toStrictEqual(["hello"]); 33 | expect(fake.is_fake).toBe(true); 34 | }); 35 | }); 36 | 37 | describe(".method(...)", () => { 38 | it("requires .willReturn(...) or .willThrow(...) to be chained", () => { 39 | const fake = Fake(TestObjectThree).create(); 40 | expect(fake.is_fake).toBe(true); 41 | 42 | // Original returns "World" 43 | expect(fake.test()).toBe("World"); 44 | 45 | // Don't fully pre-program the method. This should cause an error during assertions. 46 | fake.method("test"); 47 | 48 | try { 49 | fake.test(); 50 | } catch (error) { 51 | expect(error.message).toBe( 52 | `Pre-programmed method "test" does not have a return value.`, 53 | ); 54 | } 55 | }); 56 | 57 | it(".willReturn(...) does not call original method and returns given value", () => { 58 | // Assert that a fake can make a class take a shortcut 59 | const fakeServiceDoingShortcut = Fake(Repository).create(); 60 | fakeServiceDoingShortcut.method("findAllUsers").willReturn("shortcut"); 61 | const resourceWithShortcut = new Resource( 62 | fakeServiceDoingShortcut, 63 | ); 64 | resourceWithShortcut.getUsers(); 65 | expect(fakeServiceDoingShortcut.anotha_one_called).toBe(false); 66 | expect(fakeServiceDoingShortcut.do_something_called).toBe(false); 67 | expect(fakeServiceDoingShortcut.do_something_else_called).toBe(false); 68 | 69 | // Assert that the fake service can call original implementations 70 | const fakeServiceNotDoingShortcut = Fake(Repository).create(); 71 | const resourceWithoutShortcut = new Resource( 72 | fakeServiceNotDoingShortcut, 73 | ); 74 | resourceWithoutShortcut.getUsers(); 75 | expect(fakeServiceNotDoingShortcut.anotha_one_called).toBe(true); 76 | expect(fakeServiceNotDoingShortcut.do_something_called).toBe(true); 77 | expect(fakeServiceNotDoingShortcut.do_something_else_called).toBe(true); 78 | }); 79 | 80 | it(".willReturn(...) can be performed more than once", () => { 81 | // Assert that a fake can make a class take a shortcut 82 | const fakeServiceDoingShortcut = Fake(Repository).create(); 83 | fakeServiceDoingShortcut.method("findUserById").willReturn("shortcut"); 84 | fakeServiceDoingShortcut.method("findUserById").willReturn("shortcut2"); 85 | const resourceWithShortcut = new Resource( 86 | fakeServiceDoingShortcut, 87 | ); 88 | resourceWithShortcut.getUser(1); 89 | expect(fakeServiceDoingShortcut.anotha_one_called).toBe(false); 90 | expect(fakeServiceDoingShortcut.do_something_called).toBe(false); 91 | expect(fakeServiceDoingShortcut.do_something_else_called).toBe(false); 92 | 93 | // Assert that the fake service can call original implementations 94 | const fakeServiceNotDoingShortcut = Fake(Repository).create(); 95 | const resourceWithoutShortcut = new Resource( 96 | fakeServiceNotDoingShortcut, 97 | ); 98 | resourceWithoutShortcut.getUser(1); 99 | expect(fakeServiceNotDoingShortcut.anotha_one_called).toBe(true); 100 | expect(fakeServiceNotDoingShortcut.do_something_called).toBe(true); 101 | expect(fakeServiceNotDoingShortcut.do_something_else_called).toBe(true); 102 | }); 103 | 104 | it(".willThrow(...) does not call original method and throws error", () => { 105 | // Assert that a fake can make a class take a shortcut 106 | const fakeServiceDoingShortcut = Fake(Repository).create(); 107 | fakeServiceDoingShortcut.method("findAllUsers").willReturn("shortcut"); 108 | const resourceWithShortcut = new Resource( 109 | fakeServiceDoingShortcut, 110 | ); 111 | resourceWithShortcut.getUsers(); 112 | expect(fakeServiceDoingShortcut.anotha_one_called).toBe(false); 113 | expect(fakeServiceDoingShortcut.do_something_called).toBe(false); 114 | expect(fakeServiceDoingShortcut.do_something_else_called).toBe(false); 115 | 116 | // Assert that the fake service can call original implementations 117 | const fakeServiceNotDoingShortcut = Fake(Repository).create(); 118 | const resourceWithoutShortcut = new Resource( 119 | fakeServiceNotDoingShortcut, 120 | ); 121 | resourceWithoutShortcut.getUsers(); 122 | expect(fakeServiceNotDoingShortcut.anotha_one_called).toBe(true); 123 | expect(fakeServiceNotDoingShortcut.do_something_called).toBe(true); 124 | expect(fakeServiceNotDoingShortcut.do_something_else_called).toBe(true); 125 | }); 126 | 127 | it(".willReturn(fake) returns the fake object (basic)", () => { 128 | const fake = Fake(TestObjectFourBuilder).create(); 129 | expect(fake.is_fake).toBe(true); 130 | 131 | fake 132 | .method("someComplexMethod") 133 | .willReturn(fake); 134 | 135 | expect(fake.someComplexMethod()).toBe(fake); 136 | }); 137 | 138 | it(".willReturn(fake) returns the fake object (extra)", () => { 139 | // Assert that the original implementation sets properties 140 | const fake1 = Fake(TestObjectFourBuilder).create(); 141 | expect(fake1.is_fake).toBe(true); 142 | fake1.someComplexMethod(); 143 | expect(fake1.something_one).toBe("one"); 144 | expect(fake1.something_two).toBe("two"); 145 | 146 | // Assert that the fake implementation will not set properties 147 | const fake2 = Fake(TestObjectFourBuilder).create(); 148 | expect(fake2.is_fake).toBe(true); 149 | fake2 150 | .method("someComplexMethod") 151 | .willReturn(fake2); 152 | 153 | expect(fake2.someComplexMethod()).toBe(fake2); 154 | expect(fake2.something_one).toBe(undefined); 155 | expect(fake2.something_two).toBe(undefined); 156 | 157 | // Assert that we can also use setters 158 | const fake3 = Fake(TestObjectFourBuilder).create(); 159 | expect(fake3.is_fake).toBe(true); 160 | fake3.someComplexMethod(); 161 | expect(fake3.something_one).toBe("one"); 162 | expect(fake3.something_two).toBe("two"); 163 | fake3.something_one = "you got changed"; 164 | expect(fake3.something_one).toBe("you got changed"); 165 | }); 166 | 167 | it( 168 | `.willReturn((...) => {...}) returns true|false depending on given args`, 169 | () => { 170 | const fakeFiveService = Fake(TestObjectFiveService) 171 | .create(); 172 | 173 | const fakeFive = Fake(TestObjectFive) 174 | .withConstructorArgs(fakeFiveService) 175 | .create(); 176 | 177 | assertEquals(fakeFive.is_fake, true); 178 | assertEquals(fakeFiveService.is_fake, true); 179 | 180 | fakeFiveService 181 | .method("get") 182 | .willReturn((key, _defaultValue) => { 183 | if (key == "host") { 184 | return "locaaaaaal"; 185 | } 186 | 187 | if (key == "port") { 188 | return 3000; 189 | } 190 | 191 | return undefined; 192 | }); 193 | 194 | // `false` because `fakeFiveService.get("port") == 3000` 195 | assertEquals(fakeFive.send(), false); 196 | 197 | fakeFiveService 198 | .method("get") 199 | .willReturn((key, _defaultValue) => { 200 | if (key == "host") { 201 | return "locaaaaaal"; 202 | } 203 | 204 | if (key == "port") { 205 | return 4000; 206 | } 207 | 208 | return undefined; 209 | }); 210 | 211 | // `true` because `fakeFiveService.get("port") != 3000` 212 | assertEquals(fakeFive.send(), true); 213 | }, 214 | ); 215 | 216 | it( 217 | `.willReturn((...) => {...}) returns true|false depending on given args (multiple args)`, 218 | () => { 219 | const fakeFiveService = Fake(TestObjectFiveServiceMultipleArgs) 220 | .create(); 221 | 222 | const fakeFive = Fake(TestObjectFiveMultipleArgs) 223 | .withConstructorArgs(fakeFiveService) 224 | .create(); 225 | 226 | assertEquals(fakeFive.is_fake, true); 227 | assertEquals(fakeFiveService.is_fake, true); 228 | 229 | fakeFiveService 230 | .method("get") 231 | .willReturn((key, defaultValue) => { 232 | if (key == "host" && defaultValue == "localhost") { 233 | return null; 234 | } 235 | 236 | if (key == "port" && defaultValue == 5000) { 237 | return 4000; 238 | } 239 | 240 | return undefined; 241 | }); 242 | 243 | // `false` because `fakeFiveService.get("port") == 3000` 244 | assertEquals(fakeFive.send(), false); 245 | 246 | fakeFiveService 247 | .method("get") 248 | .willReturn((key, defaultValue) => { 249 | if (key == "host" && defaultValue == "localhost") { 250 | return "locaaaaaal"; 251 | } 252 | 253 | if (key == "port" && defaultValue == 5000) { 254 | return 4000; 255 | } 256 | 257 | return undefined; 258 | }); 259 | 260 | // `true` because `fakeFiveService.get("port") != 3000` 261 | assertEquals(fakeFive.send(), true); 262 | }, 263 | ); 264 | 265 | it(".willThrow() causes throwing RandomError (with constructor)", () => { 266 | const fake = Fake(TestObjectThree).create(); 267 | expect(fake.is_fake).toBe(true); 268 | 269 | // Original returns "World" 270 | expect(fake.test()).toBe("World"); 271 | 272 | // Make the original method throw RandomError 273 | fake 274 | .method("test") 275 | .willThrow(new RandomError("Random error message.")); 276 | 277 | expect(() => fake.test()).toThrow( 278 | new RandomError("Random error message."), 279 | ); 280 | }); 281 | 282 | it(".willThrow() causes throwing RandomError2 (no constructor)", () => { 283 | const fake = Fake(TestObjectThree).create(); 284 | expect(fake.is_fake).toBe(true); 285 | 286 | // Original returns "World" 287 | expect(fake.test()).toBe("World"); 288 | 289 | // Make the original method throw RandomError 290 | fake 291 | .method("test") 292 | .willThrow(new RandomError2()); 293 | 294 | expect(() => fake.test()).toThrow( 295 | new RandomError2("Some message not by the constructor."), 296 | ); 297 | }); 298 | }); 299 | }); 300 | 301 | //////////////////////////////////////////////////////////////////////////////// 302 | // FILE MARKER - DATA ////////////////////////////////////////////////////////// 303 | //////////////////////////////////////////////////////////////////////////////// 304 | 305 | class TestObjectOne { 306 | } 307 | 308 | class TestObjectTwo { 309 | constructor(name) { 310 | this.name = name; 311 | } 312 | } 313 | 314 | class TestObjectTwoMore { 315 | constructor(name, array) { 316 | this.name = name; 317 | this.array = array; 318 | } 319 | } 320 | 321 | class TestObjectThree { 322 | hello() { 323 | return; 324 | } 325 | 326 | test() { 327 | this.hello(); 328 | this.hello(); 329 | return "World"; 330 | } 331 | } 332 | 333 | class Resource { 334 | constructor( 335 | serviceOne, 336 | ) { 337 | this.repository = serviceOne; 338 | } 339 | 340 | getUsers() { 341 | this.repository.findAllUsers(); 342 | } 343 | 344 | getUser(id) { 345 | this.repository.findUserById(id); 346 | } 347 | } 348 | 349 | class TestObjectFourBuilder { 350 | someComplexMethod() { 351 | this.setSomethingOne(); 352 | this.setSomethingTwo(); 353 | return this; 354 | } 355 | 356 | setSomethingOne() { 357 | this.something_one = "one"; 358 | } 359 | 360 | setSomethingTwo() { 361 | this.something_two = "two"; 362 | } 363 | } 364 | 365 | class TestObjectFive { 366 | constructor(service) { 367 | this.service = service; 368 | } 369 | 370 | send() { 371 | const host = this.service.get("host"); 372 | const port = this.service.get("port"); 373 | 374 | if (host == null) { 375 | return false; 376 | } 377 | 378 | if (port === 3000) { 379 | return false; 380 | } 381 | 382 | return true; 383 | } 384 | } 385 | 386 | class TestObjectFiveService { 387 | map = new Map(); 388 | constructor() { 389 | this.map.set("host", "locaaaaaal"); 390 | this.map.set("port", 3000); 391 | } 392 | get(item) { 393 | return this.map.get(item); 394 | } 395 | } 396 | 397 | class TestObjectFiveMultipleArgs { 398 | constructor(service) { 399 | this.service = service; 400 | } 401 | 402 | send() { 403 | const host = this.service.get("host", "localhost"); 404 | const port = this.service.get("port", 5000); 405 | 406 | if (host == null) { 407 | return false; 408 | } 409 | 410 | if (port === 3000) { 411 | return false; 412 | } 413 | 414 | return true; 415 | } 416 | } 417 | 418 | class TestObjectFiveServiceMultipleArgs { 419 | map = new Map(); 420 | get(key, defaultValue) { 421 | return this.map.has(key) ? this.map.get(key) : defaultValue; 422 | } 423 | } 424 | 425 | class Repository { 426 | constructor() { 427 | this.anotha_one_called = false; 428 | this.do_something_called = false; 429 | this.do_something_else_called = false; 430 | } 431 | 432 | findAllUsers() { 433 | this.doSomething(); 434 | this.doSomethingElse(); 435 | this.anothaOne(); 436 | return "Finding all users"; 437 | } 438 | 439 | findUserById(id) { 440 | this.doSomething(); 441 | this.doSomethingElse(); 442 | this.anothaOne(); 443 | return `Finding user by id #${id}`; 444 | } 445 | 446 | anothaOne() { 447 | this.anotha_one_called = true; 448 | } 449 | 450 | doSomething() { 451 | this.do_something_called = true; 452 | } 453 | 454 | doSomethingElse() { 455 | this.do_something_else_called = true; 456 | } 457 | } 458 | 459 | class RandomError extends Error {} 460 | class RandomError2 extends Error { 461 | name = "RandomError2Name"; 462 | message = "Some message not by the constructor."; 463 | } 464 | -------------------------------------------------------------------------------- /tests/cjs/unit/mod/mock.test.js: -------------------------------------------------------------------------------- 1 | const { Mock } = require("../../../../lib/cjs/mod"); 2 | const { assertEquals } = require("../../jest_assertions"); 3 | 4 | class Request { 5 | constructor( 6 | url, 7 | options = {}, 8 | ) { 9 | this.url = url; 10 | this.options = options; 11 | this.headers = this.createHeaders(options.headers); 12 | this.method = options.method ? options.method : "get"; 13 | } 14 | 15 | createHeaders(headers) { 16 | const map = new Map(); 17 | for (const header in headers) { 18 | const value = headers[header]; 19 | map.set(header, value); 20 | } 21 | return map; 22 | } 23 | } 24 | 25 | class TestObjectOne { 26 | } 27 | 28 | class TestObjectTwo { 29 | constructor(name) { 30 | this.name = name; 31 | } 32 | } 33 | 34 | class TestObjectTwoMore { 35 | constructor(name, array) { 36 | this.name = name; 37 | this.array = array; 38 | } 39 | } 40 | 41 | class TestObjectThree { 42 | hello() { 43 | return; 44 | } 45 | 46 | test() { 47 | this.hello(); 48 | this.hello(); 49 | return "World"; 50 | } 51 | } 52 | 53 | class TestObjectFourBuilder { 54 | someComplexMethod() { 55 | this.setSomethingOne(); 56 | this.setSomethingTwo(); 57 | return this; 58 | } 59 | 60 | setSomethingOne() { 61 | this.something_one = "one"; 62 | } 63 | 64 | setSomethingTwo() { 65 | this.something_two = "two"; 66 | } 67 | } 68 | 69 | class TestObjectFive { 70 | constructor(service) { 71 | this.service = service; 72 | } 73 | 74 | send() { 75 | const host = this.service.get("host"); 76 | const port = this.service.get("port"); 77 | 78 | if (host == null) { 79 | return false; 80 | } 81 | 82 | if (port === 3000) { 83 | return false; 84 | } 85 | 86 | return true; 87 | } 88 | } 89 | 90 | class TestObjectFiveService { 91 | map = new Map(); 92 | constructor() { 93 | this.map.set("host", "locaaaaaal"); 94 | this.map.set("port", 3000); 95 | } 96 | get(item) { 97 | return this.map.get(item); 98 | } 99 | } 100 | 101 | class TestObjectFiveMultipleArgs { 102 | constructor(service) { 103 | this.service = service; 104 | } 105 | 106 | send() { 107 | const host = this.service.get("host", "localhost"); 108 | const port = this.service.get("port", 5000); 109 | 110 | if (host == null) { 111 | return false; 112 | } 113 | 114 | if (port === 3000) { 115 | return false; 116 | } 117 | 118 | return true; 119 | } 120 | } 121 | 122 | class TestObjectFiveServiceMultipleArgs { 123 | map = new Map(); 124 | get(key, defaultValue) { 125 | return this.map.has(key) ? this.map.get(key) : defaultValue; 126 | } 127 | } 128 | 129 | class RandomError extends Error {} 130 | class RandomError2 extends Error { 131 | name = "RandomError2Name"; 132 | message = "Some message not by the constructor."; 133 | } 134 | 135 | class MathService { 136 | add( 137 | num1, 138 | num2, 139 | useNestedAdd = false, 140 | ) { 141 | if (useNestedAdd) { 142 | return this.nestedAdd(num1, num2); 143 | } 144 | return num1 + num2; 145 | } 146 | 147 | nestedAdd(num1, num2) { 148 | return num1 + num2; 149 | } 150 | } 151 | 152 | class TestObjectLotsOfDataMembers { 153 | age = 0; 154 | math_service = undefined; 155 | protected_property = "I AM PROTECTED PROPERTY."; 156 | constructor(name, mathService) { 157 | this.math_service = mathService; 158 | this.name = name; 159 | } 160 | sum( 161 | num1, 162 | num2, 163 | useNestedAdd = false, 164 | ) { 165 | const sum = this.math_service.add(num1, num2, useNestedAdd); 166 | return sum; 167 | } 168 | protectedMethod() { 169 | return "I AM A PROTECTED METHOD."; 170 | } 171 | } 172 | 173 | class TestRequestHandler { 174 | handle(request) { 175 | const method = request.method.toLowerCase(); 176 | const contentType = request.headers.get("content-type"); 177 | 178 | if (method !== "post") { 179 | return "Method is not post"; 180 | } 181 | 182 | if (contentType !== "application/json") { 183 | return "Content-Type is incorrect"; 184 | } 185 | 186 | return "posted"; 187 | } 188 | } 189 | 190 | describe("Mock()", () => { 191 | it( 192 | "creates mock builder", 193 | () => { 194 | const mock = Mock(TestObjectOne); 195 | expect(mock.constructor.name).toBe("MockBuilder"); 196 | }, 197 | ); 198 | 199 | describe(".create()", () => { 200 | it( 201 | "creates mock object", 202 | () => { 203 | const mock = Mock(TestObjectTwo).create(); 204 | expect(mock.name).toBe(undefined); 205 | expect(mock.is_mock).toBe(true); 206 | }, 207 | ); 208 | }); 209 | 210 | describe(".withConstructorArgs(...)", () => { 211 | it( 212 | "can take 1 arg", 213 | () => { 214 | const mock = Mock(TestObjectTwo) 215 | .withConstructorArgs("some name") 216 | .create(); 217 | expect(mock.name).toBe("some name"); 218 | expect(mock.is_mock).toBe(true); 219 | }, 220 | ); 221 | 222 | it( 223 | "can take more than 1 arg", 224 | () => { 225 | const mock = Mock(TestObjectTwoMore) 226 | .withConstructorArgs("some name", ["hello"]) 227 | .create(); 228 | expect(mock.name).toBe("some name"); 229 | expect(mock.array).toStrictEqual(["hello"]); 230 | expect(mock.is_mock).toBe(true); 231 | 232 | const mockMathService = Mock(MathService) 233 | .create(); 234 | const mockTestObject = Mock(TestObjectLotsOfDataMembers) 235 | .withConstructorArgs("has mocked math service", mockMathService) 236 | .create(); 237 | expect(mockMathService.calls.add).toBe(0); 238 | mockTestObject.sum(1, 1); 239 | expect(mockMathService.calls.add).toBe(1); 240 | }, 241 | ); 242 | }); 243 | 244 | describe("method(...)", () => { 245 | it( 246 | "requires .willReturn(...) or .willThrow(...) to be chained", 247 | () => { 248 | const mock = Mock(TestObjectThree).create(); 249 | expect(mock.is_mock).toBe(true); 250 | 251 | // Original returns "World" 252 | expect(mock.test()).toBe("World"); 253 | 254 | // Don't fully pre-program the method. This should cause an error during assertions. 255 | mock.method("test"); 256 | 257 | try { 258 | mock.test(); 259 | } catch (error) { 260 | expect(error.message).toBe( 261 | `Pre-programmed method "test" does not have a return value.`, 262 | ); 263 | } 264 | expect(mock.calls.test).toBe(2); 265 | expect(mock.calls.hello).toBe(2); 266 | }, 267 | ); 268 | 269 | it(".willReturn(...) does not call original method and returns given value", () => { 270 | const mock = Mock(TestObjectThree).create(); 271 | expect(mock.is_mock).toBe(true); 272 | 273 | // Original returns "World" 274 | expect(mock.test()).toBe("World"); 275 | 276 | // Change original to return "Hello" 277 | mock.method("test").willReturn("Hello"); 278 | 279 | // Should output "Hello" and make the following calls 280 | expect(mock.test()).toBe("Hello"); 281 | expect(mock.calls.test).toBe(2); 282 | expect(mock.calls.hello).toBe(2); 283 | }); 284 | 285 | it( 286 | ".willReturn(...) can be performed more than once", 287 | () => { 288 | const mock = Mock(TestObjectThree).create(); 289 | expect(mock.is_mock).toBe(true); 290 | 291 | // Original returns "World" 292 | expect(mock.test()).toBe("World"); 293 | 294 | // Change original to return "Hello" 295 | mock.method("test").willReturn("Hello"); 296 | mock.method("test").willReturn("Hello!"); 297 | 298 | expect(mock.test()).toBe("Hello!"); 299 | expect(mock.calls.test).toBe(2); 300 | expect(mock.calls.hello).toBe(2); 301 | }, 302 | ); 303 | 304 | it( 305 | ".willReturn(...) returns specified value", 306 | () => { 307 | const mock = Mock(TestObjectThree).create(); 308 | expect(mock.is_mock).toBe(true); 309 | 310 | // Original returns "World" 311 | expect(mock.test()).toBe("World"); 312 | 313 | // Don't fully pre-program the method. This should cause an error during 314 | // assertions. 315 | mock 316 | .method("test") 317 | .willReturn({ 318 | "something": undefined, 319 | }); 320 | 321 | expect(mock.test()).toStrictEqual({ "something": undefined }); 322 | expect(mock.calls.test).toBe(2); 323 | expect(mock.calls.hello).toBe(2); 324 | }, 325 | ); 326 | 327 | it(".willReturn(mock) returns the mock object (basic)", () => { 328 | const mock = Mock(TestObjectFourBuilder).create(); 329 | expect(mock.is_mock).toBe(true); 330 | 331 | mock 332 | .method("someComplexMethod") 333 | .willReturn(mock); 334 | 335 | expect(mock.someComplexMethod()).toBe(mock); 336 | expect(mock.calls.someComplexMethod).toBe(1); 337 | }); 338 | 339 | it(".willReturn(mock) returns the mock object (extra)", () => { 340 | // Assert that the original implementation sets properties 341 | const mock1 = Mock(TestObjectFourBuilder).create(); 342 | expect(mock1.is_mock).toBe(true); 343 | mock1.someComplexMethod(); 344 | expect(mock1.something_one).toBe("one"); 345 | expect(mock1.something_two).toBe("two"); 346 | expect(mock1.calls.someComplexMethod).toBe(1); 347 | 348 | // Assert that the mock implementation will not set properties 349 | const mock2 = Mock(TestObjectFourBuilder).create(); 350 | expect(mock2.is_mock).toBe(true); 351 | mock2 352 | .method("someComplexMethod") 353 | .willReturn(mock2); 354 | 355 | expect(mock2.someComplexMethod()).toBe(mock2); 356 | expect(mock2.something_one).toBe(undefined); 357 | expect(mock2.something_two).toBe(undefined); 358 | expect(mock2.calls.someComplexMethod).toBe(1); 359 | 360 | // Assert that we can also use setters 361 | const mock3 = Mock(TestObjectFourBuilder).create(); 362 | expect(mock3.is_mock).toBe(true); 363 | mock3.someComplexMethod(); 364 | expect(mock3.something_one).toBe("one"); 365 | expect(mock3.something_two).toBe("two"); 366 | mock3.something_one = "you got changed"; 367 | expect(mock3.something_one).toBe("you got changed"); 368 | expect(mock3.calls.someComplexMethod).toBe(1); 369 | }); 370 | 371 | it( 372 | `.willReturn((...) => {...}) returns true|false depending on given args`, 373 | () => { 374 | const mockFiveService = Mock(TestObjectFiveService) 375 | .create(); 376 | 377 | const mockFive = Mock(TestObjectFive) 378 | .withConstructorArgs(mockFiveService) 379 | .create(); 380 | 381 | assertEquals(mockFive.is_mock, true); 382 | assertEquals(mockFiveService.is_mock, true); 383 | 384 | mockFiveService 385 | .method("get") 386 | .willReturn((key, _defaultValue) => { 387 | if (key == "host") { 388 | return "locaaaaaal"; 389 | } 390 | 391 | if (key == "port") { 392 | return 3000; 393 | } 394 | 395 | return undefined; 396 | }); 397 | 398 | // `false` because `mockFiveService.get("port") == 3000` 399 | assertEquals(mockFive.send(), false); 400 | 401 | mockFiveService 402 | .method("get") 403 | .willReturn((key, _defaultValue) => { 404 | if (key == "host") { 405 | return "locaaaaaal"; 406 | } 407 | 408 | if (key == "port") { 409 | return 4000; 410 | } 411 | 412 | return undefined; 413 | }); 414 | 415 | // `true` because `mockFiveService.get("port") != 3000` 416 | assertEquals(mockFive.send(), true); 417 | }, 418 | ); 419 | 420 | it( 421 | `.willReturn((...) => {...}) returns true|false depending on given args (multiple args)`, 422 | () => { 423 | const mockFiveService = Mock(TestObjectFiveServiceMultipleArgs) 424 | .create(); 425 | 426 | const mockFive = Mock(TestObjectFiveMultipleArgs) 427 | .withConstructorArgs(mockFiveService) 428 | .create(); 429 | 430 | assertEquals(mockFive.is_mock, true); 431 | assertEquals(mockFiveService.is_mock, true); 432 | 433 | mockFiveService 434 | .method("get") 435 | .willReturn((key, defaultValue) => { 436 | if (key == "host" && defaultValue == "localhost") { 437 | return null; 438 | } 439 | 440 | if (key == "port" && defaultValue == 5000) { 441 | return 4000; 442 | } 443 | 444 | return undefined; 445 | }); 446 | 447 | // `false` because `mockFiveService.get("port") == 3000` 448 | assertEquals(mockFive.send(), false); 449 | 450 | mockFiveService 451 | .method("get") 452 | .willReturn((key, defaultValue) => { 453 | if (key == "host" && defaultValue == "localhost") { 454 | return "locaaaaaal"; 455 | } 456 | 457 | if (key == "port" && defaultValue == 5000) { 458 | return 4000; 459 | } 460 | 461 | return undefined; 462 | }); 463 | 464 | // `true` because `mockFiveService.get("port") != 3000` 465 | assertEquals(mockFive.send(), true); 466 | }, 467 | ); 468 | 469 | it( 470 | ".willThrow() causes throwing RandomError (with constructor)", 471 | () => { 472 | const mock = Mock(TestObjectThree).create(); 473 | expect(mock.is_mock).toBe(true); 474 | 475 | // Original returns "World" 476 | expect(mock.test()).toBe("World"); 477 | 478 | // Make the original method throw RandomError 479 | mock 480 | .method("test") 481 | .willThrow(new RandomError("Random error message.")); 482 | 483 | expect( 484 | () => mock.test(), 485 | ).toThrow(new RandomError("Random error message.")); 486 | expect(mock.calls.test).toBe(2); 487 | }, 488 | ); 489 | 490 | it( 491 | ".willThrow() causes throwing RandomError2 (no constructor)", 492 | () => { 493 | const mock = Mock(TestObjectThree).create(); 494 | expect(mock.is_mock).toBe(true); 495 | 496 | // Original returns "World" 497 | expect(mock.test()).toBe("World"); 498 | 499 | // Make the original method throw RandomError 500 | mock 501 | .method("test") 502 | .willThrow(new RandomError2()); 503 | 504 | expect(() => mock.test()).toThrow( 505 | new RandomError2("Some message not by the constructor."), 506 | ); 507 | expect(mock.calls.test).toBe(2); 508 | }, 509 | ); 510 | 511 | it( 512 | ".expects(...).toBeCalled(...)", 513 | () => { 514 | const mock = Mock(TestObjectThree).create(); 515 | expect(mock.is_mock).toBe(true); 516 | 517 | mock.expects("hello").toBeCalled(2); 518 | mock.test(); 519 | mock.verifyExpectations(); 520 | }, 521 | ); 522 | }); 523 | 524 | // TODO(crookse) Put the below tests into one of the groups above this line 525 | 526 | describe("call count for outside nested function is increased", () => { 527 | const mockMathService = Mock(MathService) 528 | .create(); 529 | const mockTestObject = Mock(TestObjectLotsOfDataMembers) 530 | .withConstructorArgs("has mocked math service", mockMathService) 531 | .create(); 532 | expect(mockMathService.calls.add).toBe(0); 533 | expect(mockMathService.calls.nestedAdd).toBe(0); 534 | mockTestObject.sum(1, 1, true); 535 | expect(mockMathService.calls.add).toBe(1); 536 | expect(mockMathService.calls.nestedAdd).toBe(1); 537 | }); 538 | 539 | describe("can mock getters and setters", () => { 540 | const mock = Mock(TestObjectLotsOfDataMembers) 541 | .create(); 542 | mock.age = 999; 543 | expect(mock.age).toBe(999); 544 | }); 545 | 546 | describe("native request mock", () => { 547 | const router = Mock(TestRequestHandler).create(); 548 | 549 | const reqPost = new Request("https://google.com", { 550 | method: "post", 551 | headers: { 552 | "content-type": "application/json", 553 | }, 554 | }); 555 | expect(router.calls.handle).toBe(0); 556 | expect(router.handle(reqPost)).toBe("posted"); 557 | expect(router.calls.handle).toBe(1); 558 | 559 | const reqPostNotJson = new Request("https://google.com", { 560 | method: "post", 561 | }); 562 | expect(router.calls.handle).toBe(1); 563 | expect(router.handle(reqPostNotJson)).toBe("Content-Type is incorrect"); 564 | expect(router.calls.handle).toBe(2); 565 | 566 | const reqGet = new Request("https://google.com", { 567 | method: "get", 568 | }); 569 | 570 | expect(router.calls.handle).toBe(2); 571 | expect(router.handle(reqGet)).toBe("Method is not post"); 572 | expect(router.calls.handle).toBe(3); 573 | }); 574 | 575 | describe("sets the default value for getters", () => { 576 | class Game { 577 | } 578 | 579 | class PlayersEngine { 580 | game = new Game(); 581 | get Game() { 582 | return this.game; 583 | } 584 | set Game(val) { 585 | this.game = val; 586 | } 587 | } 588 | 589 | const mock = Mock(PlayersEngine).create(); 590 | expect(mock.Game instanceof Game).toBe(true); 591 | }); 592 | }); 593 | -------------------------------------------------------------------------------- /tests/cjs/unit/mod/stub.test.js: -------------------------------------------------------------------------------- 1 | const { Stub } = require("../../../../lib/cjs/mod"); 2 | 3 | class Server { 4 | greeting = "hello"; 5 | 6 | methodThatLogs() { 7 | return "server is running!"; 8 | } 9 | } 10 | 11 | describe("Stub()", () => { 12 | it("can stub a class property", () => { 13 | const server = new Server(); 14 | expect(server.greeting).toBe("hello"); 15 | Stub(server, "greeting", "you got changed"); 16 | expect(server.greeting).toBe("you got changed"); 17 | Stub(server, "greeting", null); 18 | expect(server.greeting).toBe(null); 19 | Stub(server, "greeting", true); 20 | expect(server.greeting).toBe(true); 21 | const obj = { test: "hello" }; 22 | Stub(server, "greeting", obj); 23 | expect(server.greeting).toBe(obj); 24 | }); 25 | 26 | it("can stub a class method", () => { 27 | const server = new Server(); 28 | expect(server.methodThatLogs()).toBe("server is running!"); 29 | Stub(server, "methodThatLogs"); 30 | expect(server.methodThatLogs()).toBe("stubbed"); 31 | Stub(server, "methodThatLogs", null); 32 | expect(server.methodThatLogs()).toBe(null); 33 | Stub(server, "methodThatLogs", true); 34 | expect(server.methodThatLogs()).toBe(true); 35 | const obj = { test: "hello" }; 36 | Stub(server, "methodThatLogs", obj); 37 | expect(server.methodThatLogs()).toBe(obj); 38 | }); 39 | 40 | it("can return a stubbed function", () => { 41 | const stub = Stub(); 42 | expect(stub()).toBe("stubbed"); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/deno/deps.ts: -------------------------------------------------------------------------------- 1 | export { 2 | assertEquals, 3 | assertThrows, 4 | } from "https://deno.land/std@0.167.0/testing/asserts.ts"; 5 | -------------------------------------------------------------------------------- /tests/deno/src/errors_test.ts: -------------------------------------------------------------------------------- 1 | import { VerificationError } from "../../../src/errors.ts"; 2 | import { assertEquals } from "../../deps.ts"; 3 | 4 | function throwError( 5 | message: string, 6 | codeThatThrew: string, 7 | actualResults: string, 8 | expectedResults: string, 9 | ): { message: string; stack: string } { 10 | let e; 11 | 12 | try { 13 | throw new VerificationError( 14 | message, 15 | codeThatThrew, 16 | actualResults, 17 | expectedResults, 18 | ); 19 | } catch (error) { 20 | e = error; 21 | } 22 | 23 | return { 24 | message: e.message, 25 | stack: e.stack 26 | .replace(/\.ts:\d+:\d+/g, ".ts:{line}:{column}") 27 | .replace(/line \d+/g, "line {line}"), 28 | }; 29 | } 30 | 31 | Deno.test("VerificationError", async (t) => { 32 | await t.step( 33 | "shows error message, code that threw, actual and expected results", 34 | () => { 35 | const error = throwError( 36 | "Some message", 37 | ".some().code()", 38 | "Actual: 1 call made", 39 | "Expected: 2 calls made ah ah ah", 40 | ); 41 | 42 | assertEquals( 43 | error.message, 44 | "Some message", 45 | ); 46 | 47 | const expected = ` 48 | 49 | VerificationError: Some message 50 | at throwError (/deno/src/errors_test.ts:{line}:{column}) 51 | 52 | Verification Results: 53 | Actual: 1 call made 54 | Expected: 2 calls made ah ah ah 55 | 56 | Check the above "errors_test.ts" file at/around line {line} for code like the following to fix this error: 57 | .some().code() 58 | 59 | 60 | `; 61 | 62 | assertEquals( 63 | error.stack.replace(/file.+tests/g, ""), // Remove file://path/to/local/rhum/tests 64 | expected, 65 | ); 66 | }, 67 | ); 68 | }); 69 | -------------------------------------------------------------------------------- /tests/deno/src/verifiers/function_expression_verifier_test.ts: -------------------------------------------------------------------------------- 1 | import { FunctionExpressionVerifier } from "../../../../src/verifiers/function_expression_verifier.ts"; 2 | import { assertEquals } from "../../deps.ts"; 3 | 4 | function throwError( 5 | cb: (...args: unknown[]) => void, 6 | ): { message: string; stack: string } { 7 | let e; 8 | 9 | try { 10 | cb(); 11 | } catch (error) { 12 | e = error; 13 | } 14 | 15 | return { 16 | message: e.message, 17 | stack: e.stack 18 | .replace(/\.ts:\d+:\d+/g, ".ts:{line}:{column}") 19 | .replace(/line \d+/g, "line {line}"), 20 | }; 21 | } 22 | 23 | Deno.test("FunctionExpressionVerifier", async (t) => { 24 | await t.step("toBeCalled()", async (t) => { 25 | await t.step( 26 | "shows error message, code that threw, actual and expected results", 27 | () => { 28 | const mv = new FunctionExpressionVerifier("doSomething"); 29 | 30 | const error = throwError(() => mv.toBeCalled(1, 2)); 31 | 32 | const expected = ` 33 | 34 | VerificationError: Function "doSomething" was not called 2 time(s). 35 | at /deno/src/verifiers/function_expression_verifier_test.ts:{line}:{column} 36 | 37 | Verification Results: 38 | Actual calls -> 1 39 | Expected calls -> 2 40 | 41 | Check the above "function_expression_verifier_test.ts" file at/around line {line} for code like the following to fix this error: 42 | .verify().toBeCalled(2) 43 | 44 | 45 | `; 46 | 47 | assertEquals( 48 | error.stack.replace(/file.+tests/g, ""), // Remove file://path/to/local/rhum/tests 49 | expected, 50 | ); 51 | }, 52 | ); 53 | }); 54 | 55 | await t.step("toBeCalledWithArgs()", async (t) => { 56 | await t.step( 57 | "shows error message, code that threw, actual and expected results", 58 | () => { 59 | const mv = new FunctionExpressionVerifier("doSomething"); 60 | 61 | const error = throwError(() => mv.toBeCalledWithArgs([1], [2])); 62 | 63 | const expected = ` 64 | 65 | VerificationError: Function "doSomething" received unexpected arg \`1\` at parameter position 1. 66 | at /deno/src/verifiers/function_expression_verifier_test.ts:{line}:{column} 67 | 68 | Verification Results: 69 | Actual call -> (1) 70 | Expected call -> (2) 71 | 72 | Check the above "function_expression_verifier_test.ts" file at/around line {line} for code like the following to fix this error: 73 | .verify().toBeCalledWithArgs(2) 74 | 75 | 76 | `; 77 | 78 | assertEquals( 79 | error.stack.replace(/file.+tests/g, ""), // Remove file://path/to/local/rhum/tests 80 | expected, 81 | ); 82 | }, 83 | ); 84 | }); 85 | 86 | await t.step("toBeCalledWithoutArgs()", async (t) => { 87 | await t.step( 88 | "shows error message, code that threw, actual and expected results", 89 | () => { 90 | const mv = new FunctionExpressionVerifier("doSomething"); 91 | 92 | const error = throwError(() => 93 | mv.toBeCalledWithoutArgs([2, "hello", {}]) 94 | ); 95 | 96 | const expected = ` 97 | 98 | VerificationError: Function "doSomething" was called with args when expected to receive no args. 99 | at /deno/src/verifiers/function_expression_verifier_test.ts:{line}:{column} 100 | 101 | Verification Results: 102 | Actual args -> (2, "hello", {}) 103 | Expected args -> (no args) 104 | 105 | Check the above "function_expression_verifier_test.ts" file at/around line {line} for code like the following to fix this error: 106 | .verify().toBeCalledWithoutArgs() 107 | 108 | 109 | `; 110 | 111 | assertEquals( 112 | error.stack.replace(/file.+tests/g, ""), // Remove file://path/to/local/rhum/tests 113 | expected, 114 | ); 115 | }, 116 | ); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /tests/deno/src/verifiers/method_verifier_test.ts: -------------------------------------------------------------------------------- 1 | import { MethodVerifier } from "../../../../src/verifiers/method_verifier.ts"; 2 | import { assertEquals } from "../../deps.ts"; 3 | 4 | class _MyClass { 5 | public doSomething(): void { 6 | return; 7 | } 8 | } 9 | 10 | function throwError( 11 | cb: (...args: unknown[]) => void, 12 | ): { message: string; stack: string } { 13 | let e; 14 | 15 | try { 16 | cb(); 17 | } catch (error) { 18 | e = error; 19 | } 20 | 21 | return { 22 | message: e.message, 23 | stack: e.stack 24 | .replace(/\.ts:\d+:\d+/g, ".ts:{line}:{column}") 25 | .replace(/line \d+/g, "line {line}"), 26 | }; 27 | } 28 | 29 | Deno.test("MethodVerifier", async (t) => { 30 | await t.step("toBeCalled()", async (t) => { 31 | await t.step( 32 | "shows error message, code that threw, actual and expected results", 33 | () => { 34 | const mv = new MethodVerifier<_MyClass>("doSomething"); 35 | 36 | const error = throwError(() => mv.toBeCalled(1, 2)); 37 | 38 | const expected = ` 39 | 40 | VerificationError: Method "doSomething" was not called 2 time(s). 41 | at /deno/src/verifiers/method_verifier_test.ts:{line}:{column} 42 | 43 | Verification Results: 44 | Actual calls -> 1 45 | Expected calls -> 2 46 | 47 | Check the above "method_verifier_test.ts" file at/around line {line} for code like the following to fix this error: 48 | .verify("doSomething").toBeCalled(2) 49 | 50 | 51 | `; 52 | 53 | assertEquals( 54 | error.stack.replace(/file.+tests/g, ""), // Remove file://path/to/local/rhum/tests 55 | expected, 56 | ); 57 | }, 58 | ); 59 | }); 60 | 61 | await t.step("toBeCalledWithArgs()", async (t) => { 62 | await t.step( 63 | "shows error message, code that threw, actual and expected results", 64 | () => { 65 | const mv = new MethodVerifier<_MyClass>("doSomething"); 66 | 67 | const error = throwError(() => mv.toBeCalledWithArgs([1], [2])); 68 | 69 | const expected = ` 70 | 71 | VerificationError: Method "doSomething" received unexpected arg \`1\` at parameter position 1. 72 | at /deno/src/verifiers/method_verifier_test.ts:{line}:{column} 73 | 74 | Verification Results: 75 | Actual call -> (1) 76 | Expected call -> (2) 77 | 78 | Check the above "method_verifier_test.ts" file at/around line {line} for code like the following to fix this error: 79 | .verify("doSomething").toBeCalledWithArgs(2) 80 | 81 | 82 | `; 83 | 84 | assertEquals( 85 | error.stack.replace(/file.+tests/g, ""), // Remove file://path/to/local/rhum/tests 86 | expected, 87 | ); 88 | }, 89 | ); 90 | }); 91 | 92 | await t.step("toBeCalledWithoutArgs()", async (t) => { 93 | await t.step( 94 | "shows error message, code that threw, actual and expected results", 95 | () => { 96 | const mv = new MethodVerifier<_MyClass>("doSomething"); 97 | 98 | const error = throwError(() => 99 | mv.toBeCalledWithoutArgs([2, "hello", {}]) 100 | ); 101 | 102 | const expected = ` 103 | 104 | VerificationError: Method "doSomething" was called with args when expected to receive no args. 105 | at /deno/src/verifiers/method_verifier_test.ts:{line}:{column} 106 | 107 | Verification Results: 108 | Actual args -> (2, "hello", {}) 109 | Expected args -> (no args) 110 | 111 | Check the above "method_verifier_test.ts" file at/around line {line} for code like the following to fix this error: 112 | .verify("doSomething").toBeCalledWithoutArgs() 113 | 114 | 115 | `; 116 | 117 | assertEquals( 118 | error.stack.replace(/file.+tests/g, ""), // Remove file://path/to/local/rhum/tests 119 | expected, 120 | ); 121 | }, 122 | ); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /tests/deno/unit/mod/dummy_test.ts: -------------------------------------------------------------------------------- 1 | import { Dummy, Mock } from "../../../../mod.ts"; 2 | import { assertEquals } from "../../../deps.ts"; 3 | 4 | Deno.test("Dummy()", async (t) => { 5 | await t.step({ 6 | name: "can fill parameter lists", 7 | fn(): void { 8 | const mockServiceOne = Mock(ServiceOne).create(); 9 | const dummy3 = Dummy(ServiceThree); 10 | 11 | const resource = new Resource( 12 | mockServiceOne, 13 | Dummy(ServiceTwo), 14 | dummy3, 15 | ); 16 | 17 | resource.callServiceOne(); 18 | assertEquals(mockServiceOne.calls.methodServiceOne, 1); 19 | }, 20 | }); 21 | 22 | await t.step({ 23 | name: "can be made without specifying constructor args", 24 | fn(): void { 25 | const dummy = Dummy(Resource); 26 | assertEquals(Object.getPrototypeOf(dummy), Resource); 27 | }, 28 | }); 29 | }); 30 | 31 | class Resource { 32 | #service_one: ServiceOne; 33 | #service_two: ServiceTwo; 34 | #service_three: ServiceThree; 35 | 36 | constructor( 37 | serviceOne: ServiceOne, 38 | serviceTwo: ServiceTwo, 39 | serviceThree: ServiceThree, 40 | ) { 41 | this.#service_one = serviceOne; 42 | this.#service_two = serviceTwo; 43 | this.#service_three = serviceThree; 44 | } 45 | 46 | public callServiceOne() { 47 | this.#service_one.methodServiceOne(); 48 | } 49 | 50 | public callServiceTwo() { 51 | this.#service_two.methodServiceTwo(); 52 | } 53 | 54 | public callServiceThree() { 55 | this.#service_three.methodServiceThree(); 56 | } 57 | } 58 | 59 | class ServiceOne { 60 | public methodServiceOne() { 61 | return "Method from ServiceOne was called."; 62 | } 63 | } 64 | 65 | class ServiceTwo { 66 | public methodServiceTwo() { 67 | return "Method from ServiceTwo was called."; 68 | } 69 | } 70 | 71 | class ServiceThree { 72 | public methodServiceThree() { 73 | return "Method from ServiceThree was called."; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/deno/unit/mod/fake_test.ts: -------------------------------------------------------------------------------- 1 | import { Fake, Mock } from "../../../../mod.ts"; 2 | import { assertEquals, assertThrows } from "../../../deps.ts"; 3 | 4 | Deno.test("Fake()", async (t) => { 5 | await t.step({ 6 | name: "creates fake builder", 7 | fn(): void { 8 | const fake = Fake(TestObjectOne); 9 | assertEquals(fake.constructor.name, "FakeBuilder"); 10 | }, 11 | }); 12 | 13 | await t.step(".create()", async (t) => { 14 | await t.step({ 15 | name: "creates fake object", 16 | fn(): void { 17 | const fake = Fake(TestObjectTwo).create(); 18 | assertEquals(fake.name, undefined); 19 | assertEquals(fake.is_fake, true); 20 | }, 21 | }); 22 | }); 23 | 24 | await t.step(".withConstructorArgs(...)", async (t) => { 25 | await t.step({ 26 | name: "can take 1 arg", 27 | fn(): void { 28 | const fake = Fake(TestObjectTwo) 29 | .withConstructorArgs("some name") 30 | .create(); 31 | assertEquals(fake.name, "some name"); 32 | assertEquals(fake.is_fake, true); 33 | }, 34 | }); 35 | 36 | await t.step({ 37 | name: "can take more than 1 arg", 38 | fn(): void { 39 | const fake = Fake(TestObjectTwoMore) 40 | .withConstructorArgs("some name", ["hello"]) 41 | .create(); 42 | assertEquals(fake.name, "some name"); 43 | assertEquals(fake.array, ["hello"]); 44 | assertEquals(fake.is_fake, true); 45 | }, 46 | }); 47 | 48 | await t.step({ 49 | name: "can set test double as arg and call it", 50 | fn(): void { 51 | const mock = Mock(PrivateService).create(); 52 | 53 | const fake = Fake(TestObjectFour) 54 | .withConstructorArgs(mock) 55 | .create(); 56 | 57 | mock.expects("doSomething").toBeCalled(1); 58 | fake.callPrivateService(); 59 | mock.verifyExpectations(); 60 | }, 61 | }); 62 | }); 63 | 64 | await t.step(".method(...)", async (t) => { 65 | await t.step({ 66 | name: "requires .willReturn(...) or .willThrow(...) to be chained", 67 | fn(): void { 68 | const fake = Fake(TestObjectThree).create(); 69 | assertEquals(fake.is_fake, true); 70 | 71 | // Original returns "World" 72 | assertEquals(fake.test(), "World"); 73 | 74 | // Don't fully pre-program the method. This should cause an error during assertions. 75 | fake.method("test"); 76 | 77 | try { 78 | fake.test(); 79 | } catch (error) { 80 | assertEquals( 81 | error.message, 82 | `Pre-programmed method "test" does not have a return value.`, 83 | ); 84 | } 85 | }, 86 | }); 87 | 88 | await t.step({ 89 | name: 90 | ".willReturn(...) does not call original method and returns given value", 91 | fn(): void { 92 | // Assert that a fake can make a class take a shortcut 93 | const fakeServiceDoingShortcut = Fake(Repository).create(); 94 | fakeServiceDoingShortcut.method("findAllUsers").willReturn("shortcut"); 95 | const resourceWithShortcut = new Resource( 96 | fakeServiceDoingShortcut, 97 | ); 98 | resourceWithShortcut.getUsers(); 99 | assertEquals(fakeServiceDoingShortcut.anotha_one_called, false); 100 | assertEquals(fakeServiceDoingShortcut.do_something_called, false); 101 | assertEquals(fakeServiceDoingShortcut.do_something_else_called, false); 102 | 103 | // Assert that the fake service can call original implementations 104 | const fakeServiceNotDoingShortcut = Fake(Repository).create(); 105 | const resourceWithoutShortcut = new Resource( 106 | fakeServiceNotDoingShortcut, 107 | ); 108 | resourceWithoutShortcut.getUsers(); 109 | assertEquals(fakeServiceNotDoingShortcut.anotha_one_called, true); 110 | assertEquals(fakeServiceNotDoingShortcut.do_something_called, true); 111 | assertEquals( 112 | fakeServiceNotDoingShortcut.do_something_else_called, 113 | true, 114 | ); 115 | }, 116 | }); 117 | 118 | await t.step({ 119 | name: ".willReturn(...) can be performed more than once", 120 | fn(): void { 121 | // Assert that a fake can make a class take a shortcut 122 | const fakeServiceDoingShortcut = Fake(Repository).create(); 123 | fakeServiceDoingShortcut.method("findUserById").willReturn("shortcut"); 124 | fakeServiceDoingShortcut.method("findUserById").willReturn("shortcut2"); 125 | const resourceWithShortcut = new Resource( 126 | fakeServiceDoingShortcut, 127 | ); 128 | resourceWithShortcut.getUser(1); 129 | assertEquals(fakeServiceDoingShortcut.anotha_one_called, false); 130 | assertEquals(fakeServiceDoingShortcut.do_something_called, false); 131 | assertEquals(fakeServiceDoingShortcut.do_something_else_called, false); 132 | 133 | // Assert that the fake service can call original implementations 134 | const fakeServiceNotDoingShortcut = Fake(Repository).create(); 135 | const resourceWithoutShortcut = new Resource( 136 | fakeServiceNotDoingShortcut, 137 | ); 138 | resourceWithoutShortcut.getUser(1); 139 | assertEquals(fakeServiceNotDoingShortcut.anotha_one_called, true); 140 | assertEquals(fakeServiceNotDoingShortcut.do_something_called, true); 141 | assertEquals( 142 | fakeServiceNotDoingShortcut.do_something_else_called, 143 | true, 144 | ); 145 | }, 146 | }); 147 | 148 | await t.step({ 149 | name: ".willThrow(...) does not call original method and throws error", 150 | fn(): void { 151 | // Assert that a fake can make a class take a shortcut 152 | const fakeServiceDoingShortcut = Fake(Repository).create(); 153 | fakeServiceDoingShortcut.method("findAllUsers").willReturn("shortcut"); 154 | const resourceWithShortcut = new Resource( 155 | fakeServiceDoingShortcut, 156 | ); 157 | resourceWithShortcut.getUsers(); 158 | assertEquals(fakeServiceDoingShortcut.anotha_one_called, false); 159 | assertEquals(fakeServiceDoingShortcut.do_something_called, false); 160 | assertEquals(fakeServiceDoingShortcut.do_something_else_called, false); 161 | 162 | // Assert that the fake service can call original implementations 163 | const fakeServiceNotDoingShortcut = Fake(Repository).create(); 164 | const resourceWithoutShortcut = new Resource( 165 | fakeServiceNotDoingShortcut, 166 | ); 167 | resourceWithoutShortcut.getUsers(); 168 | assertEquals(fakeServiceNotDoingShortcut.anotha_one_called, true); 169 | assertEquals(fakeServiceNotDoingShortcut.do_something_called, true); 170 | assertEquals( 171 | fakeServiceNotDoingShortcut.do_something_else_called, 172 | true, 173 | ); 174 | }, 175 | }); 176 | 177 | await t.step({ 178 | name: ".willReturn(fake) returns the fake object (basic)", 179 | fn(): void { 180 | const fake = Fake(TestObjectFourBuilder).create(); 181 | assertEquals(fake.is_fake, true); 182 | 183 | fake 184 | .method("someComplexMethod") 185 | .willReturn(fake); 186 | 187 | assertEquals(fake.someComplexMethod(), fake); 188 | }, 189 | }); 190 | 191 | await t.step({ 192 | name: ".willReturn(fake) returns the fake object (extra)", 193 | fn(): void { 194 | // Assert that the original implementation sets properties 195 | const fake1 = Fake(TestObjectFourBuilder).create(); 196 | assertEquals(fake1.is_fake, true); 197 | fake1.someComplexMethod(); 198 | assertEquals(fake1.something_one, "one"); 199 | assertEquals(fake1.something_two, "two"); 200 | 201 | // Assert that the fake implementation will not set properties due to 202 | // taking a shortcut 203 | const fake2 = Fake(TestObjectFourBuilder).create(); 204 | assertEquals(fake2.is_fake, true); 205 | fake2 206 | .method("someComplexMethod") 207 | .willReturn(fake2); 208 | 209 | assertEquals(fake2.someComplexMethod(), fake2); 210 | assertEquals(fake2.something_one, undefined); 211 | assertEquals(fake2.something_two, undefined); 212 | 213 | // Assert that we can also use setters 214 | const fake3 = Fake(TestObjectFourBuilder).create(); 215 | assertEquals(fake3.is_fake, true); 216 | fake3.someComplexMethod(); 217 | assertEquals(fake3.something_one, "one"); 218 | assertEquals(fake3.something_two, "two"); 219 | fake3.something_one = "you got changed"; 220 | assertEquals(fake3.something_one, "you got changed"); 221 | }, 222 | }); 223 | 224 | await t.step({ 225 | name: 226 | `.willReturn((...) => {...}) returns true|false depending on given args`, 227 | fn(): void { 228 | const fakeFiveService = Fake(TestObjectFiveService) 229 | .create(); 230 | 231 | const fakeFive = Fake(TestObjectFive) 232 | .withConstructorArgs(fakeFiveService) 233 | .create(); 234 | 235 | assertEquals(fakeFive.is_fake, true); 236 | assertEquals(fakeFiveService.is_fake, true); 237 | 238 | fakeFiveService 239 | .method("get") 240 | .willReturn((key: string, _defaultValue: number | string) => { 241 | if (key == "host") { 242 | return "locaaaaaal"; 243 | } 244 | 245 | if (key == "port") { 246 | return 3000; 247 | } 248 | 249 | return undefined; 250 | }); 251 | 252 | // `false` because `fakeFiveService.get("port") == 3000` 253 | assertEquals(fakeFive.send(), false); 254 | 255 | fakeFiveService 256 | .method("get") 257 | .willReturn((key: string, _defaultValue: number | string) => { 258 | if (key == "host") { 259 | return "locaaaaaal"; 260 | } 261 | 262 | if (key == "port") { 263 | return 4000; 264 | } 265 | 266 | return undefined; 267 | }); 268 | 269 | // `true` because `fakeFiveService.get("port") != 3000` 270 | assertEquals(fakeFive.send(), true); 271 | }, 272 | }); 273 | 274 | await t.step({ 275 | name: 276 | `.willReturn((...) => {...}) returns true|false depending on given args (multiple args)`, 277 | fn(): void { 278 | const fakeFiveService = Fake(TestObjectFiveServiceMultipleArgs) 279 | .create(); 280 | 281 | const fakeFive = Fake(TestObjectFiveMultipleArgs) 282 | .withConstructorArgs(fakeFiveService) 283 | .create(); 284 | 285 | assertEquals(fakeFive.is_fake, true); 286 | assertEquals(fakeFiveService.is_fake, true); 287 | 288 | fakeFiveService 289 | .method("get") 290 | .willReturn((key: string, defaultValue: number | string) => { 291 | if (key == "host" && defaultValue == "localhost") { 292 | return null; 293 | } 294 | 295 | if (key == "port" && defaultValue == 5000) { 296 | return 4000; 297 | } 298 | 299 | return undefined; 300 | }); 301 | 302 | // `false` because `fakeFiveService.get("port") == 3000` 303 | assertEquals(fakeFive.send(), false); 304 | 305 | fakeFiveService 306 | .method("get") 307 | .willReturn((key: string, defaultValue: string | number) => { 308 | if (key == "host" && defaultValue == "localhost") { 309 | return "locaaaaaal"; 310 | } 311 | 312 | if (key == "port" && defaultValue == 5000) { 313 | return 4000; 314 | } 315 | 316 | return undefined; 317 | }); 318 | 319 | // `true` because `fakeFiveService.get("port") != 3000` 320 | assertEquals(fakeFive.send(), true); 321 | }, 322 | }); 323 | 324 | await t.step({ 325 | name: ".willThrow() causes throwing RandomError (with constructor)", 326 | fn(): void { 327 | const fake = Fake(TestObjectThree).create(); 328 | assertEquals(fake.is_fake, true); 329 | 330 | // Original returns "World" 331 | assertEquals(fake.test(), "World"); 332 | 333 | // Make the original method throw RandomError 334 | fake 335 | .method("test") 336 | .willThrow(new RandomError("Random error message.")); 337 | 338 | assertThrows( 339 | () => fake.test(), 340 | RandomError, 341 | "Random error message.", 342 | ); 343 | }, 344 | }); 345 | 346 | await t.step({ 347 | name: ".willThrow() causes throwing RandomError2 (no constructor)", 348 | fn(): void { 349 | const fake = Fake(TestObjectThree).create(); 350 | assertEquals(fake.is_fake, true); 351 | 352 | // Original returns "World" 353 | assertEquals(fake.test(), "World"); 354 | 355 | // Make the original method throw RandomError 356 | fake 357 | .method("test") 358 | .willThrow(new RandomError2()); 359 | 360 | assertThrows( 361 | () => fake.test(), 362 | RandomError2, 363 | "Some message not by the constructor.", 364 | ); 365 | }, 366 | }); 367 | }); 368 | }); 369 | 370 | //////////////////////////////////////////////////////////////////////////////// 371 | // FILE MARKER - DATA ////////////////////////////////////////////////////////// 372 | //////////////////////////////////////////////////////////////////////////////// 373 | 374 | class TestObjectOne { 375 | } 376 | 377 | class TestObjectTwo { 378 | public name: string; 379 | constructor(name: string) { 380 | this.name = name; 381 | } 382 | } 383 | 384 | class TestObjectTwoMore { 385 | public name: string; 386 | public array: string[]; 387 | constructor(name: string, array: string[]) { 388 | this.name = name; 389 | this.array = array; 390 | } 391 | } 392 | 393 | class TestObjectThree { 394 | public hello(): void { 395 | return; 396 | } 397 | 398 | public test(): string { 399 | this.hello(); 400 | this.hello(); 401 | return "World"; 402 | } 403 | } 404 | 405 | class TestObjectFour { 406 | #private_service: PrivateService; 407 | constructor(privateService: PrivateService) { 408 | this.#private_service = privateService; 409 | } 410 | public callPrivateService() { 411 | this.#private_service.doSomething(); 412 | } 413 | } 414 | 415 | class TestObjectFourBuilder { 416 | #something_one?: string; 417 | #something_two?: string; 418 | 419 | get something_one(): string | undefined { 420 | return this.#something_one; 421 | } 422 | 423 | set something_one(value: string | undefined) { 424 | this.#something_one = value; 425 | } 426 | 427 | get something_two(): string | undefined { 428 | return this.#something_two; 429 | } 430 | 431 | someComplexMethod(): this { 432 | this.#setSomethingOne(); 433 | this.#setSomethingTwo(); 434 | return this; 435 | } 436 | 437 | #setSomethingOne(): void { 438 | this.#something_one = "one"; 439 | } 440 | 441 | #setSomethingTwo(): void { 442 | this.#something_two = "two"; 443 | } 444 | } 445 | 446 | class TestObjectFive { 447 | private readonly service: TestObjectFiveService; 448 | 449 | public constructor(service: TestObjectFiveService) { 450 | this.service = service; 451 | } 452 | 453 | public send(): boolean { 454 | const host = this.service.get("host"); 455 | const port = this.service.get("port"); 456 | 457 | if (host == null) { 458 | return false; 459 | } 460 | 461 | if (port === 3000) { 462 | return false; 463 | } 464 | 465 | return true; 466 | } 467 | } 468 | 469 | class TestObjectFiveService { 470 | #map = new Map(); 471 | constructor() { 472 | this.#map.set("host", "locaaaaaal"); 473 | this.#map.set("port", 3000); 474 | } 475 | public get(item: string): T { 476 | return this.#map.get(item) as T; 477 | } 478 | } 479 | 480 | class TestObjectFiveMultipleArgs { 481 | private readonly service: TestObjectFiveServiceMultipleArgs; 482 | 483 | public constructor(service: TestObjectFiveServiceMultipleArgs) { 484 | this.service = service; 485 | } 486 | 487 | public send(): boolean { 488 | const host = this.service.get("host", "localhost"); 489 | const port = this.service.get("port", 5000); 490 | 491 | if (host == null) { 492 | return false; 493 | } 494 | 495 | if (port === 3000) { 496 | return false; 497 | } 498 | 499 | return true; 500 | } 501 | } 502 | 503 | class TestObjectFiveServiceMultipleArgs { 504 | #map = new Map(); 505 | public get(key: string, defaultValue: T): T { 506 | return this.#map.get(key) as T ?? defaultValue; 507 | } 508 | } 509 | 510 | class PrivateService { 511 | public doSomething(): boolean { 512 | return true; 513 | } 514 | } 515 | 516 | class Resource { 517 | #repository: Repository; 518 | 519 | constructor( 520 | serviceOne: Repository, 521 | ) { 522 | this.#repository = serviceOne; 523 | } 524 | 525 | public getUsers() { 526 | this.#repository.findAllUsers(); 527 | } 528 | 529 | public getUser(id: number) { 530 | this.#repository.findUserById(id); 531 | } 532 | } 533 | 534 | class Repository { 535 | public anotha_one_called = false; 536 | public do_something_called = false; 537 | public do_something_else_called = false; 538 | 539 | public findAllUsers(): string { 540 | this.#doSomething(); 541 | this.#doSomethingElse(); 542 | this.#anothaOne(); 543 | return "Finding all users"; 544 | } 545 | 546 | public findUserById(id: number): string { 547 | this.#doSomething(); 548 | this.#doSomethingElse(); 549 | this.#anothaOne(); 550 | return `Finding user by id #${id}`; 551 | } 552 | 553 | #anothaOne() { 554 | this.anotha_one_called = true; 555 | } 556 | 557 | #doSomething() { 558 | this.do_something_called = true; 559 | } 560 | 561 | #doSomethingElse() { 562 | this.do_something_else_called = true; 563 | } 564 | } 565 | 566 | class RandomError extends Error {} 567 | class RandomError2 extends Error { 568 | public name = "RandomError2Name"; 569 | public message = "Some message not by the constructor."; 570 | } 571 | -------------------------------------------------------------------------------- /tests/deno/unit/mod/stub_test.ts: -------------------------------------------------------------------------------- 1 | import { Stub } from "../../../../mod.ts"; 2 | import { assertEquals } from "../../../deps.ts"; 3 | 4 | class Server { 5 | public greeting = "hello"; 6 | 7 | public methodThatLogs() { 8 | return "server is running!"; 9 | } 10 | } 11 | 12 | Deno.test("Stub()", async (t) => { 13 | await t.step("can stub a class property", () => { 14 | const server = new Server(); 15 | assertEquals(server.greeting, "hello"); 16 | Stub(server, "greeting", "you got changed"); 17 | assertEquals(server.greeting, "you got changed"); 18 | Stub(server, "greeting", null); 19 | assertEquals(server.greeting, null); 20 | Stub(server, "greeting", true); 21 | assertEquals(server.greeting as unknown, true); 22 | const obj = { test: "hello" }; 23 | Stub(server, "greeting", obj); 24 | assertEquals(server.greeting as unknown, obj); 25 | }); 26 | 27 | await t.step("can stub a class method", () => { 28 | const server = new Server(); 29 | assertEquals(server.methodThatLogs(), "server is running!"); 30 | Stub(server, "methodThatLogs"); 31 | assertEquals(server.methodThatLogs(), "stubbed"); 32 | Stub(server, "methodThatLogs", null); 33 | assertEquals(server.methodThatLogs(), null); 34 | Stub(server, "methodThatLogs", true); 35 | assertEquals(server.methodThatLogs() as unknown, true); 36 | const obj = { test: "hello" }; 37 | Stub(server, "methodThatLogs", obj); 38 | assertEquals(server.methodThatLogs() as unknown, obj); 39 | }); 40 | 41 | await t.step("can return a stubbed function", () => { 42 | const stub = Stub(); 43 | assertEquals(stub(), "stubbed"); 44 | }); 45 | 46 | await t.step("throws error on Stub(null) calls", () => { 47 | try { 48 | // @ts-ignore This test ensures an error is thrown when `null` is being 49 | // provided as the object containing the property or method to stub. It is 50 | // the first check in the `Stub()` call. Even though `Stub(null)` cannot 51 | // happen in TypeScript if type-checking is on, this can still happen in 52 | // JS. This is ignored because it is being tested in TypeScript, but this 53 | // SHOULD only happen in JavaScript. 54 | Stub(null, "prop"); 55 | } catch (error) { 56 | assertEquals(error.message, "Cannot create a stub using Stub(null)"); 57 | } 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/deps.ts: -------------------------------------------------------------------------------- 1 | export { 2 | assertEquals, 3 | assertThrows, 4 | } from "https://deno.land/std@0.167.0/testing/asserts.ts"; 5 | -------------------------------------------------------------------------------- /tests/esm/jest_assertions.ts: -------------------------------------------------------------------------------- 1 | export function assertEquals(actual: unknown, expected: unknown): void { 2 | expect(actual).toStrictEqual(expected); 3 | } 4 | 5 | export function assertThrows( 6 | actual: () => unknown, 7 | expected: new (message?: string) => Error, 8 | message: string, 9 | ): void { 10 | expect(actual).toThrow(new expected(message)); 11 | } 12 | -------------------------------------------------------------------------------- /tests/esm/unit/mod/dummy.test.ts: -------------------------------------------------------------------------------- 1 | import { Dummy, Mock } from "../../../../lib/esm/mod.js"; 2 | import { assertEquals } from "../../jest_assertions"; 3 | 4 | describe("Dummy()", () => { 5 | it( 6 | "can fill parameter lists", 7 | (): void => { 8 | const mockServiceOne = Mock(ServiceOne).create(); 9 | const dummy3 = Dummy(ServiceThree); 10 | 11 | const resource = new Resource( 12 | mockServiceOne, 13 | Dummy(ServiceTwo), 14 | dummy3, 15 | ); 16 | 17 | resource.callServiceOne(); 18 | assertEquals(mockServiceOne.calls.methodServiceOne, 1); 19 | }, 20 | ); 21 | 22 | it( 23 | "can be made without specifying constructor args", 24 | (): void => { 25 | const dummy = Dummy(Resource); 26 | assertEquals(Object.getPrototypeOf(dummy), Resource); 27 | }, 28 | ); 29 | }); 30 | 31 | class Resource { 32 | #service_one: ServiceOne; 33 | #service_two: ServiceTwo; 34 | #service_three: ServiceThree; 35 | 36 | constructor( 37 | serviceOne: ServiceOne, 38 | serviceTwo: ServiceTwo, 39 | serviceThree: ServiceThree, 40 | ) { 41 | this.#service_one = serviceOne; 42 | this.#service_two = serviceTwo; 43 | this.#service_three = serviceThree; 44 | } 45 | 46 | public callServiceOne() { 47 | this.#service_one.methodServiceOne(); 48 | } 49 | 50 | public callServiceTwo() { 51 | this.#service_two.methodServiceTwo(); 52 | } 53 | 54 | public callServiceThree() { 55 | this.#service_three.methodServiceThree(); 56 | } 57 | } 58 | 59 | class ServiceOne { 60 | public methodServiceOne() { 61 | return "Method from ServiceOne was called."; 62 | } 63 | } 64 | 65 | class ServiceTwo { 66 | public methodServiceTwo() { 67 | return "Method from ServiceTwo was called."; 68 | } 69 | } 70 | 71 | class ServiceThree { 72 | public methodServiceThree() { 73 | return "Method from ServiceThree was called."; 74 | } 75 | } 76 | 77 | export {}; 78 | -------------------------------------------------------------------------------- /tests/esm/unit/mod/fake.test.ts: -------------------------------------------------------------------------------- 1 | import { Fake } from "../../../../lib/esm/mod"; 2 | import { assertEquals, assertThrows } from "../../jest_assertions"; 3 | 4 | describe("Fake()", () => { 5 | it( 6 | "creates fake builder", 7 | (): void => { 8 | const fake = Fake(TestObjectOne); 9 | assertEquals(fake.constructor.name, "FakeBuilder"); 10 | }, 11 | ); 12 | 13 | describe(".create()", () => { 14 | it( 15 | "creates fake object", 16 | (): void => { 17 | const fake = Fake(TestObjectTwo).create(); 18 | assertEquals(fake.name, undefined); 19 | assertEquals(fake.is_fake, true); 20 | }, 21 | ); 22 | }); 23 | 24 | describe(".withConstructorArgs(...)", () => { 25 | it( 26 | "can take 1 arg", 27 | (): void => { 28 | const fake = Fake(TestObjectTwo) 29 | .withConstructorArgs("some name") 30 | .create(); 31 | assertEquals(fake.name, "some name"); 32 | assertEquals(fake.is_fake, true); 33 | }, 34 | ); 35 | 36 | it( 37 | "can take more than 1 arg", 38 | (): void => { 39 | const fake = Fake(TestObjectTwoMore) 40 | .withConstructorArgs("some name", ["hello"]) 41 | .create(); 42 | assertEquals(fake.name, "some name"); 43 | assertEquals(fake.array, ["hello"]); 44 | assertEquals(fake.is_fake, true); 45 | }, 46 | ); 47 | }); 48 | 49 | describe(".method(...)", () => { 50 | it( 51 | "requires .willReturn(...) or .willThrow(...) to be chained", 52 | (): void => { 53 | const fake = Fake(TestObjectThree).create(); 54 | assertEquals(fake.is_fake, true); 55 | 56 | // Original returns "World" 57 | assertEquals(fake.test(), "World"); 58 | 59 | // Don't fully pre-program the method. This should cause an error during assertions. 60 | fake.method("test"); 61 | 62 | try { 63 | fake.test(); 64 | } catch (error) { 65 | assertEquals( 66 | error.message, 67 | `Pre-programmed method "test" does not have a return value.`, 68 | ); 69 | } 70 | }, 71 | ); 72 | 73 | it( 74 | ".willReturn(...) does not call original method and returns given value", 75 | (): void => { 76 | // Assert that a fake can make a class take a shortcut 77 | const fakeServiceDoingShortcut = Fake(Repository).create(); 78 | fakeServiceDoingShortcut.method("findAllUsers").willReturn("shortcut"); 79 | const resourceWithShortcut = new Resource( 80 | fakeServiceDoingShortcut, 81 | ); 82 | resourceWithShortcut.getUsers(); 83 | assertEquals(fakeServiceDoingShortcut.anotha_one_called, false); 84 | assertEquals(fakeServiceDoingShortcut.do_something_called, false); 85 | assertEquals(fakeServiceDoingShortcut.do_something_else_called, false); 86 | 87 | // Assert that the fake service can call original implementations 88 | const fakeServiceNotDoingShortcut = Fake(Repository).create(); 89 | const resourceWithoutShortcut = new Resource( 90 | fakeServiceNotDoingShortcut, 91 | ); 92 | resourceWithoutShortcut.getUsers(); 93 | assertEquals(fakeServiceNotDoingShortcut.anotha_one_called, true); 94 | assertEquals(fakeServiceNotDoingShortcut.do_something_called, true); 95 | assertEquals( 96 | fakeServiceNotDoingShortcut.do_something_else_called, 97 | true, 98 | ); 99 | }, 100 | ); 101 | 102 | it( 103 | ".willReturn(...) can be performed more than once", 104 | (): void => { 105 | // Assert that a fake can make a class take a shortcut 106 | const fakeServiceDoingShortcut = Fake(Repository).create(); 107 | fakeServiceDoingShortcut.method("findUserById").willReturn("shortcut"); 108 | fakeServiceDoingShortcut.method("findUserById").willReturn("shortcut2"); 109 | const resourceWithShortcut = new Resource( 110 | fakeServiceDoingShortcut, 111 | ); 112 | resourceWithShortcut.getUser(1); 113 | assertEquals(fakeServiceDoingShortcut.anotha_one_called, false); 114 | assertEquals(fakeServiceDoingShortcut.do_something_called, false); 115 | assertEquals(fakeServiceDoingShortcut.do_something_else_called, false); 116 | 117 | // Assert that the fake service can call original implementations 118 | const fakeServiceNotDoingShortcut = Fake(Repository).create(); 119 | const resourceWithoutShortcut = new Resource( 120 | fakeServiceNotDoingShortcut, 121 | ); 122 | resourceWithoutShortcut.getUser(1); 123 | assertEquals(fakeServiceNotDoingShortcut.anotha_one_called, true); 124 | assertEquals(fakeServiceNotDoingShortcut.do_something_called, true); 125 | assertEquals( 126 | fakeServiceNotDoingShortcut.do_something_else_called, 127 | true, 128 | ); 129 | }, 130 | ); 131 | 132 | it( 133 | ".willThrow(...) does not call original method and throws error", 134 | (): void => { 135 | // Assert that a fake can make a class take a shortcut 136 | const fakeServiceDoingShortcut = Fake(Repository).create(); 137 | fakeServiceDoingShortcut.method("findAllUsers").willReturn("shortcut"); 138 | const resourceWithShortcut = new Resource( 139 | fakeServiceDoingShortcut, 140 | ); 141 | resourceWithShortcut.getUsers(); 142 | assertEquals(fakeServiceDoingShortcut.anotha_one_called, false); 143 | assertEquals(fakeServiceDoingShortcut.do_something_called, false); 144 | assertEquals(fakeServiceDoingShortcut.do_something_else_called, false); 145 | 146 | // Assert that the fake service can call original implementations 147 | const fakeServiceNotDoingShortcut = Fake(Repository).create(); 148 | const resourceWithoutShortcut = new Resource( 149 | fakeServiceNotDoingShortcut, 150 | ); 151 | resourceWithoutShortcut.getUsers(); 152 | assertEquals(fakeServiceNotDoingShortcut.anotha_one_called, true); 153 | assertEquals(fakeServiceNotDoingShortcut.do_something_called, true); 154 | assertEquals( 155 | fakeServiceNotDoingShortcut.do_something_else_called, 156 | true, 157 | ); 158 | }, 159 | ); 160 | 161 | it( 162 | ".willReturn(fake) returns the fake object (basic)", 163 | (): void => { 164 | const fake = Fake(TestObjectFourBuilder).create(); 165 | assertEquals(fake.is_fake, true); 166 | 167 | fake 168 | .method("someComplexMethod") 169 | .willReturn(fake); 170 | 171 | assertEquals(fake.someComplexMethod(), fake); 172 | }, 173 | ); 174 | 175 | it( 176 | ".willReturn(fake) returns the fake object (extra)", 177 | (): void => { 178 | // Assert that the original implementation sets properties 179 | const fake1 = Fake(TestObjectFourBuilder).create(); 180 | assertEquals(fake1.is_fake, true); 181 | fake1.someComplexMethod(); 182 | assertEquals(fake1.something_one, "one"); 183 | assertEquals(fake1.something_two, "two"); 184 | 185 | // Assert that the fake implementation will not set properties 186 | const fake2 = Fake(TestObjectFourBuilder).create(); 187 | assertEquals(fake2.is_fake, true); 188 | fake2 189 | .method("someComplexMethod") 190 | .willReturn(fake2); 191 | 192 | assertEquals(fake2.someComplexMethod(), fake2); 193 | assertEquals(fake2.something_one, undefined); 194 | assertEquals(fake2.something_two, undefined); 195 | 196 | // Assert that we can also use setters 197 | const fake3 = Fake(TestObjectFourBuilder).create(); 198 | assertEquals(fake3.is_fake, true); 199 | fake3.someComplexMethod(); 200 | assertEquals(fake3.something_one, "one"); 201 | assertEquals(fake3.something_two, "two"); 202 | fake3.something_one = "you got changed"; 203 | assertEquals(fake3.something_one, "you got changed"); 204 | }, 205 | ); 206 | 207 | it( 208 | ".willThrow() causes throwing RandomError (with constructor)", 209 | (): void => { 210 | const fake = Fake(TestObjectThree).create(); 211 | assertEquals(fake.is_fake, true); 212 | 213 | // Original returns "World" 214 | assertEquals(fake.test(), "World"); 215 | 216 | // Make the original method throw RandomError 217 | fake 218 | .method("test") 219 | .willThrow(new RandomError("Random error message.")); 220 | 221 | assertThrows( 222 | () => fake.test(), 223 | RandomError, 224 | "Random error message.", 225 | ); 226 | }, 227 | ); 228 | 229 | it( 230 | ".willThrow() causes throwing RandomError2 (no constructor)", 231 | (): void => { 232 | const fake = Fake(TestObjectThree).create(); 233 | assertEquals(fake.is_fake, true); 234 | 235 | // Original returns "World" 236 | assertEquals(fake.test(), "World"); 237 | 238 | // Make the original method throw RandomError 239 | fake 240 | .method("test") 241 | .willThrow(new RandomError2()); 242 | 243 | assertThrows( 244 | () => fake.test(), 245 | RandomError2, 246 | "Some message not by the constructor.", 247 | ); 248 | }, 249 | ); 250 | 251 | it( 252 | ".willReturn((...) => {...}) returns true|false depending on given args", 253 | (): void => { 254 | const fakeFiveService = Fake(TestObjectFiveService) 255 | .create(); 256 | 257 | const fakeFive = Fake(TestObjectFive) 258 | .withConstructorArgs(fakeFiveService) 259 | .create(); 260 | 261 | assertEquals(fakeFive.is_fake, true); 262 | assertEquals(fakeFiveService.is_fake, true); 263 | 264 | fakeFiveService 265 | .method("get") 266 | .willReturn((key: string, _defaultValue: number | string) => { 267 | if (key == "host") { 268 | return "locaaaaaal"; 269 | } 270 | 271 | if (key == "port") { 272 | return 3000; 273 | } 274 | 275 | return undefined; 276 | }); 277 | 278 | // `false` because `fakeFiveService.get("port") == 3000` 279 | assertEquals(fakeFive.send(), false); 280 | 281 | fakeFiveService 282 | .method("get") 283 | .willReturn((key: string, _defaultValue: number | string) => { 284 | if (key == "host") { 285 | return "locaaaaaal"; 286 | } 287 | 288 | if (key == "port") { 289 | return 4000; 290 | } 291 | 292 | return undefined; 293 | }); 294 | 295 | // `true` because `fakeFiveService.get("port") != 3000` 296 | assertEquals(fakeFive.send(), true); 297 | }, 298 | ); 299 | 300 | it( 301 | `.willReturn((...) => {...}) returns true|false depending on given args (multiple args)`, 302 | (): void => { 303 | const fakeFiveService = Fake(TestObjectFiveServiceMultipleArgs) 304 | .create(); 305 | 306 | const fakeFive = Fake(TestObjectFiveMultipleArgs) 307 | .withConstructorArgs(fakeFiveService) 308 | .create(); 309 | 310 | assertEquals(fakeFive.is_fake, true); 311 | assertEquals(fakeFiveService.is_fake, true); 312 | 313 | fakeFiveService 314 | .method("get") 315 | .willReturn((key: string, defaultValue: number | string) => { 316 | if (key == "host" && defaultValue == "localhost") { 317 | return null; 318 | } 319 | 320 | if (key == "port" && defaultValue == 5000) { 321 | return 4000; 322 | } 323 | 324 | return undefined; 325 | }); 326 | 327 | // `false` because `fakeFiveService.get("port") == 3000` 328 | assertEquals(fakeFive.send(), false); 329 | 330 | fakeFiveService 331 | .method("get") 332 | .willReturn((key: string, defaultValue: string | number) => { 333 | if (key == "host" && defaultValue == "localhost") { 334 | return "locaaaaaal"; 335 | } 336 | 337 | if (key == "port" && defaultValue == 5000) { 338 | return 4000; 339 | } 340 | 341 | return undefined; 342 | }); 343 | 344 | // `true` because `fakeFiveService.get("port") != 3000` 345 | assertEquals(fakeFive.send(), true); 346 | }, 347 | ); 348 | }); 349 | }); 350 | 351 | //////////////////////////////////////////////////////////////////////////////// 352 | // FILE MARKER - DATA ////////////////////////////////////////////////////////// 353 | //////////////////////////////////////////////////////////////////////////////// 354 | 355 | class TestObjectOne { 356 | } 357 | 358 | class TestObjectTwo { 359 | public name: string; 360 | constructor(name: string) { 361 | this.name = name; 362 | } 363 | } 364 | 365 | class TestObjectTwoMore { 366 | public name: string; 367 | public array: string[]; 368 | constructor(name: string, array: string[]) { 369 | this.name = name; 370 | this.array = array; 371 | } 372 | } 373 | 374 | class TestObjectThree { 375 | public hello(): void { 376 | return; 377 | } 378 | 379 | public test(): string { 380 | this.hello(); 381 | this.hello(); 382 | return "World"; 383 | } 384 | } 385 | 386 | class Resource { 387 | #repository: Repository; 388 | 389 | constructor( 390 | serviceOne: Repository, 391 | ) { 392 | this.#repository = serviceOne; 393 | } 394 | 395 | public getUsers() { 396 | this.#repository.findAllUsers(); 397 | } 398 | 399 | public getUser(id: number) { 400 | this.#repository.findUserById(id); 401 | } 402 | } 403 | 404 | class TestObjectFourBuilder { 405 | #something_one?: string; 406 | #something_two?: string; 407 | 408 | get something_one(): string | undefined { 409 | return this.#something_one; 410 | } 411 | 412 | set something_one(value: string | undefined) { 413 | this.#something_one = value; 414 | } 415 | 416 | get something_two(): string | undefined { 417 | return this.#something_two; 418 | } 419 | 420 | someComplexMethod(): this { 421 | this.#setSomethingOne(); 422 | this.#setSomethingTwo(); 423 | return this; 424 | } 425 | 426 | #setSomethingOne(): void { 427 | this.#something_one = "one"; 428 | } 429 | 430 | #setSomethingTwo(): void { 431 | this.#something_two = "two"; 432 | } 433 | } 434 | 435 | class TestObjectFive { 436 | private readonly service: TestObjectFiveService; 437 | 438 | public constructor(service: TestObjectFiveService) { 439 | this.service = service; 440 | } 441 | 442 | public send(): boolean { 443 | const host = this.service.get("host"); 444 | const port = this.service.get("port"); 445 | 446 | if (host == null) { 447 | return false; 448 | } 449 | 450 | if (port === 3000) { 451 | return false; 452 | } 453 | 454 | return true; 455 | } 456 | } 457 | 458 | class TestObjectFiveService { 459 | #map = new Map(); 460 | constructor() { 461 | this.#map.set("host", "locaaaaaal"); 462 | this.#map.set("port", 3000); 463 | } 464 | public get(item: string): T { 465 | return this.#map.get(item) as T; 466 | } 467 | } 468 | 469 | class TestObjectFiveMultipleArgs { 470 | private readonly service: TestObjectFiveServiceMultipleArgs; 471 | 472 | public constructor(service: TestObjectFiveServiceMultipleArgs) { 473 | this.service = service; 474 | } 475 | 476 | public send(): boolean { 477 | const host = this.service.get("host", "localhost"); 478 | const port = this.service.get("port", 5000); 479 | 480 | if (host == null) { 481 | return false; 482 | } 483 | 484 | if (port === 3000) { 485 | return false; 486 | } 487 | 488 | return true; 489 | } 490 | } 491 | 492 | class TestObjectFiveServiceMultipleArgs { 493 | #map = new Map(); 494 | public get(key: string, defaultValue: T): T { 495 | return this.#map.get(key) as T ?? defaultValue; 496 | } 497 | } 498 | 499 | class Repository { 500 | public anotha_one_called = false; 501 | public do_something_called = false; 502 | public do_something_else_called = false; 503 | 504 | public findAllUsers(): string { 505 | this.#doSomething(); 506 | this.#doSomethingElse(); 507 | this.#anothaOne(); 508 | return "Finding all users"; 509 | } 510 | 511 | public findUserById(id: number): string { 512 | this.#doSomething(); 513 | this.#doSomethingElse(); 514 | this.#anothaOne(); 515 | return `Finding user by id #${id}`; 516 | } 517 | 518 | #anothaOne() { 519 | this.anotha_one_called = true; 520 | } 521 | 522 | #doSomething() { 523 | this.do_something_called = true; 524 | } 525 | 526 | #doSomethingElse() { 527 | this.do_something_else_called = true; 528 | } 529 | } 530 | 531 | class RandomError extends Error {} 532 | class RandomError2 extends Error { 533 | public name = "RandomError2Name"; 534 | public message = "Some message not by the constructor."; 535 | } 536 | -------------------------------------------------------------------------------- /tests/esm/unit/mod/stub.test.ts: -------------------------------------------------------------------------------- 1 | import { Stub } from "../../../../lib/esm/mod"; 2 | import { assertEquals } from "../../jest_assertions"; 3 | 4 | class Server { 5 | public greeting = "hello"; 6 | 7 | public methodThatLogs() { 8 | return "server is running!"; 9 | } 10 | } 11 | 12 | describe("Stub()", () => { 13 | it("can stub a class property", () => { 14 | const server = new Server(); 15 | assertEquals(server.greeting, "hello"); 16 | Stub(server, "greeting", "you got changed"); 17 | assertEquals(server.greeting, "you got changed"); 18 | Stub(server, "greeting", null); 19 | assertEquals(server.greeting, null); 20 | Stub(server, "greeting", true); 21 | assertEquals(server.greeting, true); 22 | const obj = { test: "hello" }; 23 | Stub(server, "greeting", obj); 24 | assertEquals(server.greeting, obj); 25 | }); 26 | 27 | it("can stub a class method", () => { 28 | const server = new Server(); 29 | assertEquals(server.methodThatLogs(), "server is running!"); 30 | Stub(server, "methodThatLogs"); 31 | assertEquals(server.methodThatLogs(), "stubbed"); 32 | Stub(server, "methodThatLogs", null); 33 | assertEquals(server.methodThatLogs(), null); 34 | Stub(server, "methodThatLogs", true); 35 | assertEquals(server.methodThatLogs(), true); 36 | const obj = { test: "hello" }; 37 | Stub(server, "methodThatLogs", obj); 38 | assertEquals(server.methodThatLogs(), obj); 39 | }); 40 | 41 | it("can return a stubbed function", () => { 42 | const stub = Stub(); 43 | assertEquals(stub(), "stubbed"); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drashland/rhum/51594d57289c89f40e06017e9d148d5485a86059/tmp/.gitkeep -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "./lib/cjs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "es2015", 5 | "outDir": "./lib/esm" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "rootDir": "./tmp/conversion_workspace", 5 | "declaration": true, 6 | "sourceMap": true, 7 | "esModuleInterop": true, 8 | "moduleResolution": "node" 9 | }, 10 | "include": ["./tmp/conversion_workspace/**/*.ts"], 11 | "exclude": ["./tests/**/*"] 12 | } 13 | --------------------------------------------------------------------------------