diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 5e1e7dec..873bfec1 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -9,23 +9,57 @@ on: - main jobs: - tests: + tests_deno: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@master + - uses: actions/checkout@v2 + + - name: Install Deno + uses: denoland/setup-deno@v1 + + - name: Unit Tests + run: deno test tests/deno + + tests_node: + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + # We only support until EOL + # See https://nodejs.org/en/about/releases/ + node: ['14', '15', '16', '17'] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + # We need deno because the "Build CJS and ESM" step runs `deno run` - name: Install Deno - uses: denolib/setup-deno@master + uses: denoland/setup-deno@v1 + + - name: Install Node + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node }} + + - name: Install deps + run: yarn install + + - name: Build CJS and ESM (*nix) + if: matrix.os != 'windows-latest' + run: | + yarn build - - name: Unit - run: deno test --allow-env tests/unit + - name: Build CJS and ESM (windows) + if: matrix.os == 'windows-latest' + run: | + yarn build:windows - - name: Integration - run: deno test --allow-run --allow-env tests/integration + - name: Unit Test + run: yarn test linter: # Only one OS is required since fmt is cross platform @@ -35,10 +69,10 @@ jobs: - uses: actions/checkout@v2 - name: Install Deno - uses: denolib/setup-deno@master + uses: denoland/setup-deno@v1 - # - name: Lint - # run: deno lint --unstable + - name: Lint + run: deno lint - name: Formatter run: deno fmt --check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f7aef31..668f8510 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,67 @@ name: Release on: release: types: [published] + jobs: + + npm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + token: ${{ secrets.CI_USER_PAT }} + + # We need deno because the "Build CJS and ESM" step runs `deno run` + - name: Install Deno + uses: denoland/setup-deno@v1 + + # Setup .npmrc file to publish to npm + - name: Install Node + uses: actions/setup-node@v2 + with: + registry-url: 'https://registry.npmjs.org' + scope: '@drashland' + + - name: Install deps + run: yarn install + + - name: Build CJS and ESM + run: yarn build + + - name: Publish + run: yarn publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTOMATION_TOKEN }} + + github: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + token: ${{ secrets.CI_USER_PAT }} + + # We need deno because the "Build CJS and ESM" step runs `deno run` + - name: Install Deno + uses: denoland/setup-deno@v1 + + # Setup .npmrc file to publish to github + - name: Install Node + uses: actions/setup-node@v2 + with: + registry-url: 'https://npm.pkg.github.com' + scope: '@drashland' + + - name: Install deps + run: yarn install + + - name: Build CJS and ESM + run: yarn build + + - name: Publish + run: yarn publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + publish-egg: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 81bb71ec..087ca4d6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,15 @@ .DS_Store # directories -tmp*/* -tmp/* .idea +.vscode +lib/ +node_modules/ +tmp*/ +tmp/ # exceptions !.gitkeep -.vscode - +# locks +yarn.lock diff --git a/README.md b/README.md index d85f416e..44ef2258 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,17 @@ -

- Rhum - A lightweight testing framework for Deno. -

Rhum

-

-

A lightweight testing framework for Deno.

-

- - - - - - - - - - - - - - - -

+# Rhum ---- +[![Latest Release](https://img.shields.io/github/release/drashland/rhum.svg?color=bright_green&label=latest)](#) +[![CI master](https://img.shields.io/github/workflow/status/drashland/rhum/master?label=ci%20-%20master)](#) +[![YouTube](https://img.shields.io/badge/tutorials-youtube-red)](https://rb.gy/vxmeed) -### Features +Rhum logo -- Descriptive naming for your tests -- Lightweight -- Zero 3rd party dependencies -- Simple and easy to use -- Asynchronous support -- Still uses `Deno.test` under the hood -- Skip functionality -- Mock requests -- Hooks +Rhum is a test double library that follows +[test double definitions](https://martinfowler.com/bliki/TestDouble.html) from +Gerard Meszaros. -### Getting Started +View the full documentation at https://drash.land/rhum. -To add Rhum to your project, follow the quick start guide -[here](https://drash.land/rhum/#/#quickstart). - -### Why Use Rhum? - -Rhum allows you to write tests in a very descriptive way -- from a code -perspective or output perspective. - -Rhum is designed to aid your testing efforts -- providing many utilities as -wrappers around Deno's existing `Deno.test`. Rhum is meant to improve the user -experience when it comes to writing tests, such as: - -- Readability for test cases -- Features that aren't available in Deno yet (hooks) - -Rhum takes concepts from the following: - -- Mocha — For how you - write tests in Rhum, and the use of - hooks -- Baretest — - Being minimalistic - -Rhum can be added directly into any project. All you need to do is import Rhum -and you are ready to start writing tests or bring your existing tests under -Rhum. - ---- - -Want to contribute? Follow the Contributing Guidelines -[here](https://github.com/drashland/.github/blob/master/CONTRIBUTING.md). All -code is released under the [MIT License](./LICENSE). +In the event the documentation pages are not accessible, please view the raw +version of the documentation at +https://github.com/drashland/website-v2/tree/main/docs. diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000..721e8b82 --- /dev/null +++ b/babel.config.js @@ -0,0 +1 @@ +module.exports = { presets: ["@babel/preset-env"] }; diff --git a/console/build_esm_lib b/console/build_esm_lib new file mode 100755 index 00000000..82a53f24 --- /dev/null +++ b/console/build_esm_lib @@ -0,0 +1,9 @@ +#!/bin/bash + +( + rm -r tmp/conversion_workspace/ + mkdir -p tmp/conversion_workspace/ + cp -r src/ tmp/conversion_workspace/src/ + cp mod.ts tmp/conversion_workspace/mod.ts + deno run --allow-read --allow-run --allow-write ./console/build_esm_lib.ts +) diff --git a/console/build_esm_lib.ts b/console/build_esm_lib.ts new file mode 100644 index 00000000..6a683a74 --- /dev/null +++ b/console/build_esm_lib.ts @@ -0,0 +1,48 @@ +const decoder = new TextDecoder(); +const encoder = new TextEncoder(); + +const filesToRewrite = [ + "tmp/conversion_workspace/src/fake/fake_builder.ts", + "tmp/conversion_workspace/src/fake/fake_mixin.ts", + "tmp/conversion_workspace/src/mock/mock_builder.ts", + "tmp/conversion_workspace/src/mock/mock_mixin.ts", + "tmp/conversion_workspace/src/interfaces.ts", + "tmp/conversion_workspace/src/pre_programmed_method.ts", + "tmp/conversion_workspace/src/types.ts", + "tmp/conversion_workspace/mod.ts", +]; + +for (const index in filesToRewrite) { + const file = filesToRewrite[index]; + + // Step 1: Read contents + let contents = decoder.decode(Deno.readFileSync(file)); + + // Step 2: Create an array of import/export statements from the contents + const importStatements = contents.match(/import.*";/g); + const exportStatements = contents.match(/export.*";/g); + + // Step 3: Remove all .ts extensions from the import/export statements + const newImportStatements = importStatements?.map((statement: string) => { + return statement.replace(/\.ts";/, `";`); + }); + + const newExportStatements = exportStatements?.map((statement: string) => { + return statement.replace(/\.ts";/, `";`); + }); + + // Step 4: Replace the original contents with the new contents + if (newImportStatements) { + importStatements?.forEach((statement: string, index: number) => { + contents = contents.replace(statement, newImportStatements[index]); + }); + } + if (newExportStatements) { + exportStatements?.forEach((statement: string, index: number) => { + contents = contents.replace(statement, newExportStatements[index]); + }); + } + + // Step 5: Rewrite the original file without .ts extensions + Deno.writeFileSync(file, encoder.encode(contents)); +} diff --git a/deps.ts b/deps.ts deleted file mode 100644 index fa6c500d..00000000 --- a/deps.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { BufReader } from "https://deno.land/std@0.135.0/io/bufio.ts"; - -export * as StdAsserts from "https://deno.land/std@0.135.0/testing/asserts.ts"; - -export * as colors from "https://deno.land/std@0.135.0/fmt/colors.ts"; diff --git a/egg.json b/egg.json index 06fa2b92..ca372055 100644 --- a/egg.json +++ b/egg.json @@ -1,13 +1,11 @@ { "name": "rhum", - "description": "A lightweight testing framework for Deno.", + "description": "A test double module to stub and mock your code.", "version": "1.1.14", "stable": true, "repository": "https://github.com/drashland/rhum", "files": [ "./mod.ts", - "./deps.ts", - "./src/**/*", "./README.md", "./logo.svg", "LICENSE" diff --git a/example_tests/basic/1.ts b/example_tests/basic/1.ts deleted file mode 100644 index eaae3d3e..00000000 --- a/example_tests/basic/1.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Rhum } from "../../mod.ts"; - -Rhum.testPlan("test_plan_1", () => { - Rhum.testSuite("test_suite_1a", () => { - Rhum.testCase("test_case_1a1", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("test_case_1a2", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("test_case_1a3", () => { - Rhum.asserts.assertEquals(true, true); - }); - }); - - Rhum.testSuite("test_suite_1b", () => { - Rhum.testCase("test_case_1b1", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("test_case_1b2", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("test_case_1b3", () => { - Rhum.asserts.assertEquals(true, true); - }); - }); -}); - -Rhum.run(); diff --git a/example_tests/basic/1_fail.ts b/example_tests/basic/1_fail.ts deleted file mode 100644 index 5924602d..00000000 --- a/example_tests/basic/1_fail.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Rhum } from "../../mod.ts"; - -Rhum.testPlan("test_plan_1", () => { - Rhum.testSuite("test_suite_1a", () => { - Rhum.testCase("test_case_1a1", () => { - Rhum.asserts.assertEquals(true, false); - }); - Rhum.testCase("test_case_1a2", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("test_case_1a3", () => { - Rhum.asserts.assertEquals(true, true); - }); - }); - - Rhum.testSuite("test_suite_1b", () => { - Rhum.testCase("test_case_1b1", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("test_case_1b2", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("test_case_1b3", () => { - Rhum.asserts.assertEquals(true, false); - }); - }); -}); - -Rhum.run(); diff --git a/example_tests/basic/2.ts b/example_tests/basic/2.ts deleted file mode 100644 index c45c87fc..00000000 --- a/example_tests/basic/2.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Rhum } from "../../mod.ts"; - -Rhum.testPlan("test_plan_2", () => { - Rhum.testSuite("test_suite_2a", () => { - Rhum.testCase("test_case_2a1", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("test_case_2a2", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("test_case_2a3", () => { - Rhum.asserts.assertEquals(true, true); - }); - }); - - Rhum.testSuite("test_suite_2b", () => { - Rhum.testCase("test_case_2b1", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("test_case_2b2", () => { - Rhum.asserts.assertEquals(true, true); - }); - }); - - Rhum.testSuite("test_suite_2c", () => { - Rhum.testCase("test_case_2c1", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("test_case_2c2", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("test_case_2c3", () => { - Rhum.asserts.assertEquals(true, true); - }); - }); - - Rhum.testSuite("test_suite_2d", () => { - Rhum.testCase("test_case_2d1", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("test_case_2d2", () => { - Rhum.asserts.assertEquals(true, true); - }); - }); -}); - -Rhum.run(); diff --git a/example_tests/basic/3.ts b/example_tests/basic/3.ts deleted file mode 100644 index 40b52b0c..00000000 --- a/example_tests/basic/3.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Rhum } from "../../mod.ts"; - -Rhum.testPlan("test_plan_3", () => { - Rhum.testSuite("test_suite_3a", () => { - Rhum.testCase("test_case_3a1", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("test_case_3a2", () => { - Rhum.asserts.assertEquals(true, true); - }); - }); - - Rhum.testSuite("test_suite_3b", () => { - Rhum.testCase("test_case_3b1", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("test_case_3b2", () => { - Rhum.asserts.assertEquals(true, true); - }); - }); - - Rhum.testSuite("test_suite_3c", () => { - Rhum.testCase("test_case_3c1", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("test_case_3c2", () => { - Rhum.asserts.assertEquals(true, true); - }); - }); -}); - -Rhum.run(); diff --git a/example_tests/basic/tests_fail.ts b/example_tests/basic/tests_fail.ts deleted file mode 100644 index 978ca4eb..00000000 --- a/example_tests/basic/tests_fail.ts +++ /dev/null @@ -1,3 +0,0 @@ -import "./1_fail.ts"; -import "./2.ts"; -import "./3.ts"; diff --git a/example_tests/basic/tests_pass.ts b/example_tests/basic/tests_pass.ts deleted file mode 100644 index a48c96b7..00000000 --- a/example_tests/basic/tests_pass.ts +++ /dev/null @@ -1,3 +0,0 @@ -import "./1.ts"; -import "./2.ts"; -import "./3.ts"; diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 00000000..4b1f85bb --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,14 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +import type { Config } from "@jest/types"; + +const config: Config.InitialOptions = { + preset: "ts-jest", + testEnvironment: "node", + verbose: true, + moduleFileExtensions: ["ts", "js"], + transform: { + "^.+\\.(ts|tsx)?$": "ts-jest", + "^.+\\.(js|jsx)$": "babel-jest", + }, +}; +export default config; diff --git a/mod.ts b/mod.ts index 8a58e201..53715a7d 100644 --- a/mod.ts +++ b/mod.ts @@ -1,453 +1,124 @@ -import { assertions, asserts } from "./src/rhum_asserts.ts"; -import { TestCase } from "./src/test_case.ts"; -import type { ITestPlan } from "./src/interfaces.ts"; -import type { Constructor, Stubbed } from "./src/types.ts"; -import { MockBuilder } from "./src/mock_builder.ts"; - -export type { Constructor, Stubbed } from "./src/types.ts"; -export { MockBuilder } from "./src/mock_builder.ts"; +import type { Constructor, StubReturnValue } from "./src/types.ts"; +import { MockBuilder } from "./src/mock/mock_builder.ts"; +import { FakeBuilder } from "./src/fake/fake_builder.ts"; +export * as Types from "./src/types.ts"; +export * as Interfaces from "./src/interfaces.ts"; /** - * Deno's test runner outputs "test ", which has a length of 5. This module - * erases the "test " string by backspacing the test plan line and test suite - * line by that number. For safety, it substracts twice that number. This is - * how we get the number 10 here. + * Create a dummy. + * + * Per Martin Fowler (based on Gerard Meszaros), "Dummy objects are passed + * around but never actually used. Usually they are just used to fill parameter + * lists." + * + * @param constructorFn - The constructor function to use to become the + * prototype of the dummy. Dummy objects should be the same instance as what + * they are standing in for. For example, if a `SomeClass` parameter needs to be + * filled with a dummy because it is out of scope for a test, then the dummy + * should be an instance of `SomeClass`. + * @returns A dummy object being an instance of the given constructor function. */ -const extraChars = 10; +export function Dummy(constructorFn?: Constructor): T { + const dummy = Object.create({}); + Object.setPrototypeOf(dummy, constructorFn ?? Object); + return dummy; +} /** - * This testing framework allows the following syntax: + * Get the builder to create fake objects. * - * import { Rhum } from "/path/to/rhum/mod.ts"; + * Per Martin Fowler (based on Gerard Meszaros), "Fake objects actually have + * working implementations, but usually take some shortcut which makes them not + * suitable for production (an InMemoryTestDatabase is a good example)." * - * Rhum.testPlan("test_plan_1", () => { + * @param constructorFn - The constructor function of the object to fake. + * + * @returns Instance of `FakeBuilder`. + */ +export function Fake(constructorFn: Constructor): FakeBuilder { + return new FakeBuilder(constructorFn); +} + +/** + * Get the builder to create mocked objects. * - * Rhum.testSuite("test_suite_1a", () => { - * Rhum.testCase("test_case_1a1", () => { - * Rhum.asserts.assertEquals(true, true); - * }); - * Rhum.testCase("test_case_1a2", () => { - * Rhum.asserts.assertEquals(true, true); - * }); - * Rhum.testCase("test_case_1a3", () => { - * Rhum.asserts.assertEquals(true, true); - * }); - * }); + * Per Martin Fowler (based on Gerard Meszaros), "Mocks are pre-programmed with + * expectations which form a specification of the calls they are expected to + * receive. They can throw an exception if they receive a call they don't expect + * and are checked during verification to ensure they got all the calls they + * were expecting." * - * Rhum.testSuite("test_suite_1b", () => { - * Rhum.testCase("test_case_1b1", () => { - * Rhum.asserts.assertEquals(true, true); - * }); - * Rhum.testCase("test_case_1b2", () => { - * Rhum.asserts.assertEquals(true, true); - * }); - * Rhum.testCase("test_case_1b3", () => { - * Rhum.asserts.assertEquals(true, true); - * }); - * }); + * @param constructorFn - The constructor function of the object to mock. * - * }); + * @returns Instance of `MockBuilder`. */ -export class RhumRunner { - /** - * The asserts module from https://deno.land/std/testing, but attached to Rhum - * for accessibility. - * - * Rhum.asserts.assertEquals(true, true); // pass - * Rhum.asserts.assertEquals(true, false); // fail - */ - // deno-lint-ignore ban-types Reason for this is, deno lint no longer allows `Function` and instead needs us to be explicit: `() => void`, but because we couldn't use that to type the properties (we would just be copying Deno's interfaces word for word), we have to deal with `Function - public asserts: { [key in assertions]: Function } = asserts; - - protected passed_in_test_plan = ""; - - protected passed_in_test_suite = ""; - - protected test_plan_in_progress = ""; - - protected test_suite_in_progress = ""; - - protected plan: ITestPlan = { suites: {} }; - - // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// - - /** - * Used to define a hook that will execute before each test suite or test - * case. If this is used inside of a test plan, then it will execute before - * each test suite. If this is used inside of a test suite, then it will - * execute before each test case. - * - * @param cb - The callback to invoke. Would contain the required logic you - * need to do what you want, before each test suite or case. - * - * Rhum.testPlan("My Plan", () => { - * Rhum.beforeEach(() => { - * // Runs before each test suite in this test plan - * }); - * Rhum.testSuite("My Suite 1", () => { - * Rhum.beforeEach(() => { - * // Runs before each test case in this test suite - * }); - * Rhum.testCase("My Test Case 1", () => { - * ... - * }); - * }); - * }); - */ - public beforeEach(cb: () => void): void { - // Check if the hook is for test cases inside of a suite - if (this.passed_in_test_plan && this.passed_in_test_suite) { - // is a before each inside a suite for every test case - this.plan.suites![this.passed_in_test_suite].before_each_case_hook = cb; - } else if (this.passed_in_test_plan && !this.passed_in_test_suite) { - // before each hooks for the suites - this.plan.before_each_suite_hook = cb; - } - } - - /** - * Used to define a hook that will execute after each test suite or test case. - * If this is used inside of a test plan, then it will execute after each test - * suite. If this is used inside of a test suite, then it will execute after - * each test case. - * - * @param cb - The callback to invoke. Would contain the required logic you - * need to do what you want, after each test suite or case. - * - * Rhum.testPlan("My Plan", () => { - * Rhum.afterEach(() => { - * // Runs after each test suite in this test plan - * }); - * Rhum.testSuite("My Suite 1", () => { - * Rhum.afterEach(() => { - * // Runs after each test case in this test suite - * }); - * Rhum.testCase("My Test Case 1", () => { - * ... - * }); - * }); - * }); - */ - public afterEach(cb: () => void): void { - // Check if the hook is for test cases inside of a suite - if (this.passed_in_test_plan && this.passed_in_test_suite) { - // is a after each inside a suite for every test case - this.plan.suites![this.passed_in_test_suite].after_each_case_hook = cb; - } else if (this.passed_in_test_plan && !this.passed_in_test_suite) { - // after each hooks for the suites - this.plan.after_each_suite_hook = cb; - } - } - - /** - * Used to define a hook that will execute after all test suites or test - * cases. If this is used inside of a test plan, then it will execute after - * all test suites. If this is used inside of a test suite, then it will - * execute after all test cases. - * - * @param cb - The callback to invoke. Would contain the required logic you - * need to do what you want, after all test suites or cases. - * - * Rhum.testPlan("My Plan", () => { - * Rhum.afterAll(() => { - * // Runs once after all test suites in this test plan - * }); - * Rhum.testSuite("My Suite 1", () => { - * Rhum.afterAll(() => { - * // Runs once after all test cases in this test suite - * }); - * Rhum.testCase("My Test Case 1", () => { - * ... - * }); - * }); - * }); - */ - public afterAll(cb: () => void): void { - // Check if the hook is for test cases inside of a suite - if (this.passed_in_test_plan && this.passed_in_test_suite) { - // is a before all inside a suite for every test case - this.plan.suites![this.passed_in_test_suite].after_all_case_hook = cb; - } else if (this.passed_in_test_plan && !this.passed_in_test_suite) { - // before all hooks for the suites - this.plan.after_all_suite_hook = cb; - } - } - - /** - * Used to define a hook that will execute before all test suites or test - * cases. If this is used inside of a test plan, then it will execute before - * all test suites. If this is used inside of a test suite, then it will - * execute before all test cases. - * - * @param cb - The callback to invoke. Would contain the required logic you - * need to do what you want, before all test suites or cases. - * - * Rhum.testPlan("My Plan", () => { - * Rhum.beforeAll(() => { - * // Runs once before all test suites in this test plan - * }); - * Rhum.testSuite("My Suite 1", () => { - * Rhum.beforeAll(() => { - * // Runs once before all test cases in this test suite - * }); - * Rhum.testCase("My Test Case 1", () => { - * ... - * }); - * }); - * }); - */ - public beforeAll(cb: () => void): void { - // Check if the hook is for test cases inside of a suite - if (this.passed_in_test_plan && this.passed_in_test_suite) { - // is a before all inside a suite for every test case - this.plan.suites![this.passed_in_test_suite].before_all_case_hook = cb; - } else if (this.passed_in_test_plan && !this.passed_in_test_suite) { - // before all hooks for the suites - this.plan.before_all_suite_hook = cb; - } - } - - // public only(cb: Function): void { - // // Do something - // } - - /** - * Allows a test plan, suite, or case to be skipped when the tests run. - * - * Rhum.testPlan("My Plan", () => { - * Rhum.skip("My Suite 1", () => { // will not run this block - * Rhum.testCase("My Test Case In Suite 1", () => { - * ... - * }); - * }); - * Rhum.testSuite("My Suite 2", () => { - * Rhum.testCase("My Test Case In Suite 2", () => { - * ... - * }); - * Rhum.skip("My Other Test Case In Suite 2", () => { // will not run this block - * ... - * }); - * }); - * }); - */ - public skip(name: string, cb: () => void): void { - // TODO(ebebbington|crookse) Maybe we could still call run, but pass in { - // ignore: true } which the Deno.Test will use? just so it displays ignored - // in the console - } +export function Mock(constructorFn: Constructor): MockBuilder { + return new MockBuilder(constructorFn); +} - /** - * Stub a member of an object. - * - * @param obj -The object containing the member to stub. - * @param member -The member to stub. - * @param value - The return value of the stubbed member. - * - * Returns the object in question as a Stubbed type. Being a Stubbed type - * means it has access to a `.stub()` method for stubbing properties and - * methods. - * - * class MyObject { - * public some_property = "someValue"; - * } - * - * // Define the object that will have stubbed members as a stubbed object - * const myStubbedObject = Rhum.stubbed(new MyObject()); - * - * // Stub the object's some_property property to a certain value - * myStubbedObject.stub("some_property", "this property is now stubbed"); - * - * // Assert that the property was stubbed - * Rhum.asserts.assertEquals(myStubbedObject.some_property, "this property is now stubbed"); - */ - public stubbed(obj: T): Stubbed { - (obj as unknown as { [key: string]: boolean }).is_stubbed = true; - (obj as unknown as { - [key: string]: (property: string, value: unknown) => void; - }).stub = function ( - property: string, - value: unknown, - ): void { - Object.defineProperty(obj, property, { - value: value, - }); +/** + * Create a stub function that returns "stubbed". + */ +export function Stub(): () => "stubbed"; +/** + * Take the given object and stub its given data member to return the given + * return value. + * + * @param obj - The object receiving the stub. + * @param dataMember - The data member on the object to be stubbed. + * @param returnValue - (optional) What the stub should return. Defaults to + * "stubbed". + */ +export function Stub( + obj: T, + dataMember: keyof T, + returnValue?: R, +): StubReturnValue; +/** + * Take the given object and stub its given data member to return the given + * return value. + * + * Per Martin Fowler (based on Gerard Meszaros), "Stubs provide canned answers + * to calls made during the test, usually not responding at all to anything + * outside what's programmed in for the test." + * + * @param obj - (optional) The object receiving the stub. Defaults to a stub + * function. + * @param dataMember - (optional) The data member on the object to be stubbed. + * Only used if `obj` is an object. + * @param returnValue - (optional) What the stub should return. Defaults to + * "stubbed" for class properties and a function that returns "stubbed" for + * class methods. Only used if `object` is an object and `dataMember` is a + * member of that object. + */ +export function Stub( + obj?: T, + dataMember?: keyof T, + returnValue?: R, +): unknown { + if (obj === undefined) { + return function stubbed() { + return "stubbed"; }; - - return obj as Stubbed; - } - - /** - * Get the mock builder to mock classes. - * - * @param constructorFn - The constructor function of the object to mock. - * - * Returns an instance of the MockBuilder class. - * - * class ToBeMocked { ... } - * - * const mock = Rhum - * .mock(ToBeMocked) - * .withConstructorArgs("someArg") // if the class to be mocked has a constructor and it requires args - * .create(); - */ - public mock(constructorFn: Constructor): MockBuilder { - return new MockBuilder(constructorFn); - } - - /** - * A test case is grouped by a test suite and it is what makes the assertions - * - it is the test. You can define multiple test cases under a test suite. - * Test cases can also be asynchronous. Test cases can only be defined inside - * of a test suite. - * - * @param name - The name of the test case. - * @param testFn - The test to execute. - * - * Rhum.testPlan("My Plan", () => { - * Rhum.testSuite("My Suite 1", () => { - * Rhum.testCase("My Test Case 1", () => { - * Rhum.assert.assertEquals(something, true); - * }); - * Rhum.testCase("My Test Case 2", () => { - * Rhum.assert.assertEquals(something, false); - * }); - * }); - * }); - */ - public testCase(name: string, testFn: () => void): void { - this.plan.suites[this.passed_in_test_suite].cases!.push({ - name, - new_name: this.formatTestCaseName(name), - testFn, - }); - } - - /** - * Groups up test suites to describe a test plan. Usually, a test plan is per - * file and contains the tests suites and test cases for a single file. Test - * plans are required in order to define a test suite with test cases. - * - * @param name - The name of the test plan. - * @param testSuites - The test suites to execute. - * - * Rhum.testPlan("My Plan", () => { - * ... - * }); - */ - public testPlan(name: string, testSuites: () => void): void { - this.passed_in_test_suite = ""; // New plan - this.passed_in_test_plan = name; - testSuites(); } - /** - * A test suite usually describes a method or property name and groups up all - * test cases for that method or property. You can define multiple test suites - * under a test plan. Test suites can only be defined inside of a test plan. - * - * @param name - The name of the test suite. - * @param testCases - The test cases to execute. - * - * Rhum.testPlan("My Plan", () => { - * Rhum.testSuite("My Suite 1", () => { - * ... - * }); - * Rhum.testSuite("My Suite 2", () => { - * ... - * }); - * }); - */ - public testSuite(name: string, testCases: () => void): void { - this.passed_in_test_suite = name; - this.plan.suites![name] = { cases: [] }; - testCases(); - } - - /** - * Run the test plan. - * - * Rhum.testPlan("My Plan", () => { - * ... - * }); - * - * Rhum.run(); - */ - public run(): void { - const tc = new TestCase(this.plan); - tc.run(); - this.deconstruct(); - } - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - METHODS - PROTECTED ///////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * Figure out the name of the test case for output purposes. - * - * @param name - The name of the test case. - * - * Returns the new test name for outputting purposes. - */ - protected formatTestCaseName(name: string): string { - let newName: string; - // (ebebbington) Unfortunately, due to the CI not correctly displaying output - // (it is all over the place and just completely unreadable as - // it doesn't play well with our control characters), we need to - // display the test output differently, based on if the tests are - // being ran inside a CI or not. Nothing will change for the current - // way of doing things, but if the tests are being ran inside a CI, - // the format would be: - // test | | ... ok (2ms) - // test | | ... ok (2ms) - // Even if plans and/or suites are the same. I believe this the best - // way we can display the output - if (Deno.env.get("CI") === "true") { - newName = - `${this.passed_in_test_plan} | ${this.passed_in_test_suite} | ${name}`; - return newName; - } - - if (this.test_plan_in_progress != this.passed_in_test_plan) { - this.test_plan_in_progress = this.passed_in_test_plan; - this.test_suite_in_progress = this.passed_in_test_suite; - newName = - `${ - "\u0008".repeat(name.length + extraChars) - } ${this.passed_in_test_suite}` + - `\n ${name}`; + // If we get here, then we know for a fact that we are stubbing object + // properties. Also, we do not care if `returnValue` was passed in here. If it + // is not passed in, then `returnValue` defaults to "stubbed". Otherwise, use + // the value of `returnValue`. + if (typeof obj === "object" && dataMember !== undefined) { + // If we are stubbing a method, then make sure the method is still callable + if (typeof obj[dataMember] === "function") { + Object.defineProperty(obj, dataMember, { + value: () => returnValue !== undefined ? returnValue : "stubbed", + writable: true, + }); } else { - if (this.test_suite_in_progress != this.passed_in_test_suite) { - this.test_suite_in_progress = this.passed_in_test_suite; - newName = `${"\u0008".repeat(name.length + extraChars)}` + - ` ${this.passed_in_test_suite}` + - `${" ".repeat(name.length + extraChars)}` + - `\n ${name}`; - } else { - newName = `${"\u0008".repeat(name.length + extraChars)}` + - ` ${name}`; - } + // If we are stubbing a property, then just reassign the property + Object.defineProperty(obj, dataMember, { + value: returnValue !== undefined ? returnValue : "stubbed", + writable: true, + }); } - - return newName; - } - - /** - * 'Empty' this object. After calling this, Rhum should be ready for another - * test plan. - */ - protected deconstruct(): void { - this.passed_in_test_suite = ""; - this.passed_in_test_plan = ""; - this.test_plan_in_progress = ""; - this.test_suite_in_progress = ""; - this.plan = { suites: {} }; } } - -/** - * An instance of the RhumRunner. - * - * const Rhum = new RhumRunner(); - */ -export const Rhum = new RhumRunner(); diff --git a/package.json b/package.json new file mode 100644 index 00000000..8d4ed42a --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "@drashland/rhum", + "version": "2.0.0", + "description": "A test double library", + "main": "./lib/cjs/mod.js", + "types": "./lib/cjs/mod.d.ts", + "repository": "git@github.com:drashland/rhum.git", + "author": "Drash Land", + "license": "MIT", + "scripts": { + "build": "console/build_esm_lib && yarn build:cjs && yarn build:esm", + "build:windows": "bash console/build_esm_lib && yarn build:cjs && yarn build:esm", + "build:cjs": "tsc --project tsconfig.cjs.json", + "build:esm": "tsc --project tsconfig.esm.json", + "check": "rm -rf node_modules/ && rm yarn.lock && yarn install && yarn build && yarn test", + "test": "jest tests" + }, + "devDependencies": { + "@babel/preset-env": "7.x", + "@types/jest": "27.x", + "@types/node": "16.x", + "babel-jest": "27.x", + "jest": "27.x", + "ts-jest": "27.x", + "ts-node": "10.x", + "typescript": "4.x" + }, + "files": [ + "./lib" + ] +} diff --git a/src/fake/fake_builder.ts b/src/fake/fake_builder.ts new file mode 100644 index 00000000..c3818088 --- /dev/null +++ b/src/fake/fake_builder.ts @@ -0,0 +1,281 @@ +import type { Constructor } from "../types.ts"; +import type { IFake } from "../interfaces.ts"; +import { createFake } from "./fake_mixin.ts"; +import { PreProgrammedMethod } from "../pre_programmed_method.ts"; + +/** + * Builder to help build a fake object. This does all of the heavy-lifting to + * create a fake object. + */ +export class FakeBuilder { + /** + * The class to fake (should be constructable). + */ + #constructor_fn: Constructor; + + /** + * A list of arguments the class constructor takes. + */ + #constructor_args: unknown[] = []; + + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - CONSTRUCTOR ///////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + /** + * Construct an object of this class. + * + * @param constructorFn - The class to fake (should be constructable). + */ + constructor(constructorFn: Constructor) { + this.#constructor_fn = constructorFn; + } + + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + /** + * Create the fake object. + * + * @returns The original object with capabilities from the fake class. + */ + public create(): ClassToFake & IFake { + const original = new this.#constructor_fn(...this.#constructor_args); + + const fake = createFake, ClassToFake>( + this.#constructor_fn, + ); + fake.init(original, this.#getAllFunctionNames(original)); + + // Attach all of the original's properties to the fake + this.#getAllPropertyNames(original).forEach((property: string) => { + this.#addOriginalObjectPropertyToFakeObject( + original, + fake, + property, + ); + }); + + // Attach all of the original's functions to the fake + this.#getAllFunctionNames(original).forEach((method: string) => { + this.#addOriginalObjectMethodToFakeObject( + original, + fake, + method, + ); + }); + + return fake as ClassToFake & IFake; + } + + /** + * Before constructing the fake object, track any constructor function args + * that need to be passed in when constructing the fake object. + * + * @param args - A rest parameter of arguments that will get passed in to the + * constructor function of the object being faked. + * + * @returns `this` so that methods in this class can be chained. + */ + public withConstructorArgs(...args: unknown[]): this { + this.#constructor_args = args; + return this; + } + + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - METHODS - PRIVATE /////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + /** + * Add an original object's method to a fake object without doing anything + * else. + * + * @param original - The original object containing the method to fake. + * @param fake - The fake object receiving the method to fake. + * @param method - The name of the method to fake -- callable via + * `fake[method](...)`. + */ + #addMethodToFakeObject( + original: ClassToFake, + fake: IFake, + method: string, + ): void { + Object.defineProperty(fake, method, { + value: original[method as keyof ClassToFake], + }); + } + + /** + * Add an original object's method to a fake object -- determining whether the + * method should or should not be trackable. + * + * @param original - The original object containing the method to add. + * @param fake - The fake object receiving the method. + * @param method - The name of the method to fake -- callable via + * `fake[method](...)`. + */ + #addOriginalObjectMethodToFakeObject( + original: ClassToFake, + fake: IFake, + method: string, + ): void { + const nativeMethods = [ + "__defineGetter__", + "__defineSetter__", + "__lookupGetter__", + "__lookupSetter__", + "constructor", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "toLocaleString", + "toString", + "valueOf", + ]; + + // If this is a native method, then do not do anything fancy. Just add it to + // the fake. + if (nativeMethods.indexOf(method as string) !== -1) { + return this.#addMethodToFakeObject( + original, + fake, + method, + ); + } + + // Otherwise, make the method trackable via `.calls` usage. + this.#addTrackableMethodToFakeObject( + original, + fake, + method as keyof ClassToFake, + ); + } + + /** + * Add an original object's property to a fake object. + * + * @param original The original object containing the property. + * @param fake The fake object receiving the property. + * @param property The name of the property -- retrievable via + * `fake[property]`. + */ + #addOriginalObjectPropertyToFakeObject( + original: ClassToFake, + fake: IFake, + property: string, + ): void { + const desc = Object.getOwnPropertyDescriptor(original, property) ?? + Object.getOwnPropertyDescriptor( + this.#constructor_fn.prototype, + property, + ); + + // If we do not have a desc, then we have no idea what the value should be. + // Also, we have no idea what we are copying, so we should just not do it. + if (!desc) { + return; + } + + // Basic property (e.g., public test = "hello"). We do not handle get() and + // set() because those are handled by the fake mixin. + if (("value" in desc)) { + Object.defineProperty(fake, property, { + value: desc.value, + writable: true, + }); + } + } + + /** + * Add a trackable method to a fake object. A trackable method is one that can + * be verified using `fake.calls[someMethod]`. + * + * @param original - The original object containing the method to add. + * @param fake - The fake object receiving the method. + * @param method - The name of the method. + */ + #addTrackableMethodToFakeObject( + original: ClassToFake, + fake: IFake, + method: keyof ClassToFake, + ): void { + Object.defineProperty(fake, method, { + value: (...args: unknown[]) => { + // Make sure the method calls its original self + const methodToCall = + (original[method as keyof ClassToFake] as unknown as ( + ...params: unknown[] + ) => unknown); + + // We need to check if the method was pre-preprogrammed to return + // something. If it was, then we make sure that this method we are + // currently defining returns that pre-programmed value. + if (methodToCall instanceof PreProgrammedMethod) { + if (methodToCall.will_throw) { + throw methodToCall.error; + } + return methodToCall.return; + } + + // When method calls its original self, let the `this` context of the + // original be the fake. Reason being the fake has tracking and the + // original does not. + const bound = methodToCall.bind(fake); + + // Use `return` because the original function could return a value + return bound(...args); + }, + }); + } + + /** + * Get all properties from the original so they can be added to the fake. + * + * @param obj - The object that will be faked. + * + * @returns An array of the object's properties. + */ + #getAllPropertyNames(obj: ClassToFake): string[] { + let functions: string[] = []; + let clone = obj; + do { + functions = functions.concat(Object.getOwnPropertyNames(clone)); + } while ((clone = Object.getPrototypeOf(clone))); + + return functions.sort().filter( + function (e: string, i: number, arr: unknown[]) { + if ( + e != arr[i + 1] && typeof obj[e as keyof ClassToFake] != "function" + ) { + return true; + } + }, + ); + } + + /** + * Get all functions from the original so they can be added to the fake. + * + * @param obj - The object that will be faked. + * + * @returns An array of the object's functions. + */ + #getAllFunctionNames(obj: ClassToFake): string[] { + let functions: string[] = []; + let clone = obj; + do { + functions = functions.concat(Object.getOwnPropertyNames(clone)); + } while ((clone = Object.getPrototypeOf(clone))); + + return functions.sort().filter( + function (e: string, i: number, arr: unknown[]) { + if ( + e != arr[i + 1] && typeof obj[e as keyof ClassToFake] == "function" + ) { + return true; + } + }, + ); + } +} diff --git a/src/fake/fake_mixin.ts b/src/fake/fake_mixin.ts new file mode 100644 index 00000000..87cd4303 --- /dev/null +++ b/src/fake/fake_mixin.ts @@ -0,0 +1,66 @@ +import type { Constructor, MethodOf } from "../types.ts"; +import { PreProgrammedMethod } from "../pre_programmed_method.ts"; +import type { IFake } from "../interfaces.ts"; + +class FakeError extends Error {} + +export function createFake( + OriginalClass: OriginalConstructor, +): IFake { + const Original = OriginalClass as unknown as Constructor< + // deno-lint-ignore no-explicit-any + (...args: any[]) => any + >; + return new class FakeExtension extends Original { + /** + * Helper property to see that this is a fake object and not the original. + */ + is_fake = true; + + /** + * The original object that this class creates a fake of. + */ + #original!: OriginalObject; + + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + /** + * @param original - The original object to fake. + */ + public init(original: OriginalObject) { + this.#original = original; + } + + /** + * Pre-program a method on the original to return a specific value. + * + * @param methodName The method name on the original. + * @returns A pre-programmed method that will be called instead of original. + */ + public method( + methodName: MethodOf, + ): PreProgrammedMethod { + const methodConfiguration = new PreProgrammedMethod< + OriginalObject, + ReturnValueType + >( + methodName, + ); + + if (!((methodName as string) in this.#original)) { + throw new FakeError( + `Method "${methodName}" does not exist.`, + ); + } + + Object.defineProperty(this.#original, methodName, { + value: methodConfiguration, + writable: true, + }); + + return methodConfiguration; + } + }(); +} diff --git a/src/interfaces.ts b/src/interfaces.ts index 5f987bfe..72374642 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,121 +1,46 @@ -/** - * @remarks - * suites - * An object of objects matching the ITestSuite interface. - * - * after_all_suite_hook? - * A callback function to execute after all test suites. - * - * after_each_suite_hook? - * A callback function to execute after each test suite. - * - * before_all_suite_hook? - * A callback function to execute before all test suites. - * - * before_each_suite_hook? - * A callback function to execute before each test suite. - * - * Example below ... - * - * { - * suites: { - * "My Suite": { - * cases: [ - * { - * name: "My Case", - * new_name: this.formatTestCaseName(name), - * testFn: Function - * }, - * ... - * ], - * after_all_case_hook: Function, - * after_each_case_hook: Function, - * before_all_case_hook: Function, - * before_each_case_hook: Function - * }, - * ... // More suites allowed - * }, - * after_all_suite_hook: Function; - * after_each_suite_hook: Function; - * before_all_suite_hook: Function; - * before_each_suite_hook: Function; - * } - * - * ... or ... - * - * { - * suites: { - * run(): { - * cases: [Array], - * after_all_case_hook: [Function], - * before_all_case_hook: [Function], - * before_each_case_hook: [Function], - * after_each_case_hook: [Function] - * }, - * close(): { - * cases: [Array], - * after_all_case_hook: [Function], - * before_all_case_hook: [Function], - * before_each_case_hook: [Function], - * after_each_case_hook: [Function] - * } - * }, - * before_each_suite_hook: [Function], - * after_each_suite_hook: [Function], - * after_all_suite_hook: [Function], - * before_all_suite_hook: [Function] - * } - */ -export interface ITestPlan { - suites: { - [key: string]: ITestSuite; // "key" is the suite name - }; - after_all_suite_hook?: () => void; - after_each_suite_hook?: () => void; - before_all_suite_hook?: () => void; - before_each_suite_hook?: () => void; -} +import type { MethodCalls, MethodOf } from "./types.ts"; -/** - * cases? - * An array of objects matching the ITestCase interface. - * - * after_all_case_hook? - * A callback function to execute after all test cases. - * - * after_each_case_hook? - * A callback function to execute after each test case. - * - * before_all_case_hook? - * A callback function to execute before all test cases. - * - * before_each_case_hook? - * A callback function to execute before each test case. - */ -export interface ITestSuite { - cases?: ITestCase[]; - after_all_case_hook?: () => void; - after_each_case_hook?: () => void; - before_all_case_hook?: () => void; - before_each_case_hook?: () => void; +export interface IMethodExpectation { + toBeCalled(expectedCalls: number): void; } -/** - * name - * The name of the test case. - * - * new_name - * The new name of the test. This is strictly for outputting purposes. - * Deno's test runner outputs "test name of test" and we want to - * overwrite that text. This new_name string helps us do that. See - * formatTestCaseName() in mod.ts for more information. - * - * testFn - * The test function. Ultimately, this gets passed as the second - * argument of Deno.test(). - */ -export interface ITestCase { +export interface IError { name: string; - new_name: string; - testFn: () => void; + message?: string; +} + +export interface IPreProgrammedMethod { + willReturn(returnValue: ReturnValue): void; + willThrow(error: IError): void; +} + +export interface IFake { + is_fake: boolean; + + init( + original: OriginalObject, + methodsToTrack: string[], + ): void; + + method( + methodName: MethodOf, + ): IPreProgrammedMethod; +} + +export interface IMock { + calls: MethodCalls; + is_mock: boolean; + + init( + original: OriginalObject, + methodsToTrack: string[], + ): void; + + expects(method: MethodOf): IMethodExpectation; + + method( + methodName: MethodOf, + ): IPreProgrammedMethod; + + verifyExpectations(): void; } diff --git a/src/mock/mock_builder.ts b/src/mock/mock_builder.ts new file mode 100644 index 00000000..612fbdfc --- /dev/null +++ b/src/mock/mock_builder.ts @@ -0,0 +1,286 @@ +import type { Constructor } from "../types.ts"; +import type { IMock } from "../interfaces.ts"; +import { createMock } from "./mock_mixin.ts"; +import { PreProgrammedMethod } from "../pre_programmed_method.ts"; + +/** + * Builder to help build a mock object. This does all of the heavy-lifting to + * create a mock object. Its `create()` method returns an instance of `Mock`, + * which is basically an original object with added data members for verifying + * behavior. + */ +export class MockBuilder { + /** + * The class object passed into the constructor + */ + #constructor_fn: Constructor; + + /** + * A list of arguments the class constructor takes + */ + #constructor_args: unknown[] = []; + + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - CONSTRUCTOR ///////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + /** + * Construct an object of this class. + * + * @param constructorFn - The constructor function of the object to mock. + */ + constructor(constructorFn: Constructor) { + this.#constructor_fn = constructorFn; + } + + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + /** + * Create the mock object. + * + * @returns The original object with capabilities from the Mock class. + */ + public create(): ClassToMock & IMock { + const original = new this.#constructor_fn(...this.#constructor_args); + + const mock = createMock, ClassToMock>( + this.#constructor_fn, + ); + mock.init(original, this.#getAllFunctionNames(original)); + + // Attach all of the original's properties to the mock + this.#getAllPropertyNames(original).forEach((property: string) => { + this.#addOriginalObjectPropertyToMockObject( + original, + mock, + property, + ); + }); + + // Attach all of the original's functions to the mock + this.#getAllFunctionNames(original).forEach((method: string) => { + this.#addOriginalObjectMethodToMockObject( + original, + mock, + method, + ); + }); + + return mock as ClassToMock & IMock; + } + + /** + * Before constructing the mock object, track any constructor function args + * that need to be passed in when constructing the mock object. + * + * @param args - A rest parameter of arguments that will get passed in to the + * constructor function of the object being mocked. + * + * @returns `this` so that methods in this class can be chained. + */ + public withConstructorArgs(...args: unknown[]): this { + this.#constructor_args = args; + return this; + } + + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - METHODS - PRIVATE /////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + /** + * Add an original object's method to a mock object without doing anything + * else. + * + * @param original - The original object containing the method to mock. + * @param mock - The mock object receiving the method to mock. + * @param method - The name of the method to mock -- callable via + * `mock[method](...)`. + */ + #addMethodToMockObject( + original: ClassToMock, + mock: IMock, + method: string, + ): void { + Object.defineProperty(mock, method, { + value: original[method as keyof ClassToMock], + }); + } + + /** + * Add an original object's method to a mock object -- determining whether the + * method should or should not be trackable. + * + * @param original - The original object containing the method to add. + * @param mock - The mock object receiving the method. + * @param method - The name of the method to mock -- callable via + * `mock[method](...)`. + */ + #addOriginalObjectMethodToMockObject( + original: ClassToMock, + mock: IMock, + method: string, + ): void { + const nativeMethods = [ + "__defineGetter__", + "__defineSetter__", + "__lookupGetter__", + "__lookupSetter__", + "constructor", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "toLocaleString", + "toString", + "valueOf", + ]; + + // If this is a native method, then do not do anything fancy. Just add it to + // the mock. + if (nativeMethods.indexOf(method as string) !== -1) { + return this.#addMethodToMockObject( + original, + mock, + method, + ); + } + + // Otherwise, make the method trackable via `.calls` usage. + this.#addTrackableMethodToMockObject( + original, + mock, + method as keyof ClassToMock, + ); + } + + /** + * Add an original object's property to a mock object. + * + * @param original The original object containing the property. + * @param mock The mock object receiving the property. + * @param property The name of the property -- retrievable via + * `mock[property]`. + */ + #addOriginalObjectPropertyToMockObject( + original: ClassToMock, + mock: IMock, + property: string, + ): void { + const desc = Object.getOwnPropertyDescriptor(original, property) ?? + Object.getOwnPropertyDescriptor( + this.#constructor_fn.prototype, + property, + ); + + // If we do not have a desc, then we have no idea what the value should be. + // Also, we have no idea what we are copying, so we should just not do it. + if (!desc) { + return; + } + + // Basic property (e.g., public test = "hello"). We do not handle get() and + // set() because those are handled by the mock mixin. + if (("value" in desc)) { + Object.defineProperty(mock, property, { + value: desc.value, + writable: true, + }); + } + } + + /** + * Add a trackable method to a mock object. A trackable method is one that can + * be verified using `mock.calls[someMethod]`. + * + * @param original - The original object containing the method to add. + * @param mock - The mock object receiving the method. + * @param method - The name of the method. + */ + #addTrackableMethodToMockObject( + original: ClassToMock, + mock: IMock, + method: keyof ClassToMock, + ): void { + Object.defineProperty(mock, method, { + value: (...args: unknown[]) => { + // Track that this method was called + mock.calls[method]++; + + // Make sure the method calls its original self + const methodToCall = + (original[method as keyof ClassToMock] as unknown as ( + ...params: unknown[] + ) => unknown); + + // We need to check if the method was pre-preprogrammed to return + // something. If it was, then we make sure that this method we are + // currently defining returns that pre-programmed value. + if (methodToCall instanceof PreProgrammedMethod) { + if (methodToCall.will_throw) { + throw methodToCall.error; + } + return methodToCall.return; + } + + // When method calls its original self, let the `this` context of the + // original be the mock. Reason being the mock has tracking and the + // original does not. + const bound = methodToCall.bind(mock); + + // Use `return` because the original function could return a value + return bound(...args); + }, + }); + } + + /** + * Get all properties from the original so they can be added to the mock. + * + * @param obj - The object that will be mocked. + * + * @returns An array of the object's properties. + */ + #getAllPropertyNames(obj: ClassToMock): string[] { + let functions: string[] = []; + let clone = obj; + do { + functions = functions.concat(Object.getOwnPropertyNames(clone)); + } while ((clone = Object.getPrototypeOf(clone))); + + return functions.sort().filter( + function (e: string, i: number, arr: unknown[]) { + if ( + e != arr[i + 1] && typeof obj[e as keyof ClassToMock] != "function" + ) { + return true; + } + }, + ); + } + + /** + * Get all functions from the original so they can be added to the mock. + * + * @param obj - The object that will be mocked. + * + * @returns An array of the object's functions. + */ + #getAllFunctionNames(obj: ClassToMock): string[] { + let functions: string[] = []; + let clone = obj; + do { + functions = functions.concat(Object.getOwnPropertyNames(clone)); + } while ((clone = Object.getPrototypeOf(clone))); + + return functions.sort().filter( + function (e: string, i: number, arr: unknown[]) { + if ( + e != arr[i + 1] && typeof obj[e as keyof ClassToMock] == "function" + ) { + return true; + } + }, + ); + } +} diff --git a/src/mock/mock_mixin.ts b/src/mock/mock_mixin.ts new file mode 100644 index 00000000..8b642dd1 --- /dev/null +++ b/src/mock/mock_mixin.ts @@ -0,0 +1,160 @@ +import type { Constructor, MethodCalls, MethodOf } from "../types.ts"; +import { PreProgrammedMethod } from "../pre_programmed_method.ts"; +import type { IMock } from "../interfaces.ts"; + +class MockError extends Error {} + +class MethodExpectation { + #method_name: MethodOf; + #expected_calls = 0; + + get method_name(): MethodOf { + return this.#method_name; + } + + get expected_calls(): number { + return this.#expected_calls; + } + + constructor(methodName: MethodOf) { + this.#method_name = methodName; + } + + public toBeCalled(expectedCalls: number) { + this.#expected_calls = expectedCalls; + } +} + +export function createMock( + OriginalClass: OriginalConstructor, +): IMock { + const Original = OriginalClass as unknown as Constructor< + // deno-lint-ignore no-explicit-any + (...args: any[]) => any + >; + return new class MockExtension extends Original { + /** + * Helper property to see that this is a mock object and not the original. + */ + is_mock = true; + + /** + * Property to track method calls. + */ + #calls!: MethodCalls; + + /** + * An array of expectations to verify (if any). + */ + #expectations: MethodExpectation[] = []; + + /** + * The original object that this class creates a mock of. + */ + #original!: OriginalObject; + + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - GETTERS / SETTERS /////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + get calls(): MethodCalls { + return this.#calls; + } + + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + /** + * @param original - The original object to mock. + * @param methodsToTrack - The original object's method to make trackable. + */ + public init(original: OriginalObject, methodsToTrack: string[]) { + this.#original = original; + this.#calls = this.#constructCallsProperty(methodsToTrack); + } + + /** + * Create a method expectation, which is basically asserting calls. + * + * @param method - The method to create an expectation for. + * @returns A method expectation. + */ + public expects( + method: MethodOf, + ): MethodExpectation { + const expectation = new MethodExpectation(method); + this.#expectations.push(expectation); + return expectation; + } + + /** + * Pre-program a method on the original to return a specific value. + * + * @param methodName The method name on the original. + * @returns A pre-programmed method that will be called instead of original. + */ + public method( + methodName: MethodOf, + ): PreProgrammedMethod { + const methodConfiguration = new PreProgrammedMethod< + OriginalObject, + ReturnValueType + >( + methodName, + ); + + if (!((methodName as string) in this.#original)) { + throw new MockError( + `Method "${methodName}" does not exist.`, + ); + } + + Object.defineProperty(this.#original, methodName, { + value: methodConfiguration, + writable: true, + }); + + return methodConfiguration; + } + + /** + * Verify all expectations created in this mock. + */ + public verifyExpectations(): void { + this.#expectations.forEach((e: MethodExpectation) => { + const expectedCalls = e.expected_calls; + const actualCalls = this.#calls[e.method_name]; + if (expectedCalls !== actualCalls) { + throw new MockError( + `Method "${e.method_name}" expected ${expectedCalls} call(s), but received ${actualCalls} call(s).`, + ); + } + }); + } + + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - METHODS - PRIVATE /////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + /** + * Construct the calls property. Only construct it, do not set it. The + * constructor will set it. + * @param methodsToTrack - All of the methods on the original object to make + * trackable. + * @returns - Key-value object where the key is the method name and the value + * is the number of calls. All calls start at 0. + */ + #constructCallsProperty( + methodsToTrack: string[], + ): Record { + const calls: Partial> = {}; + + methodsToTrack.map((key: string) => { + calls[key as keyof OriginalObject] = 0; + }); + + return calls as Record; + } + }(); +} diff --git a/src/mock_builder.ts b/src/mock_builder.ts deleted file mode 100644 index e0a31c0a..00000000 --- a/src/mock_builder.ts +++ /dev/null @@ -1,187 +0,0 @@ -import type { Constructor, Mocked } from "./types.ts"; - -export class MockBuilder { - /** - * Properties of the class that is passed in - */ - protected properties: string[] = []; - - /** - * Functions of the class passed in - */ - protected functions: string[] = []; - - /** - * The class object passed into the constructor - */ - protected constructor_fn: Constructor; - - /** - * A list of arguments the class constructor takes - */ - protected constructor_args: unknown[] = []; - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - CONSTRUCTOR ///////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * Construct an object of this class. - * - * @param constructorFn - The object's constructor function to instantiate. - */ - constructor(constructorFn: Constructor) { - this.constructor_fn = constructorFn; - } - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * Create the mock object. - * - * @returns A mocked object. - */ - public create(): Mocked { - // deno-lint-ignore no-explicit-any - const mock: Mocked = { - calls: {}, - is_mock: true, - }; - const original = new this.constructor_fn(...this.constructor_args); - - // Attach all of the original's properties to the mock - this.getAllProperties(original).forEach((property: string) => { - const desc = Object.getOwnPropertyDescriptor(original, property) ?? - Object.getOwnPropertyDescriptor( - this.constructor_fn.prototype, - property, - ); - - // Handle getter/setters differently so we can initialise their default - // getter value - if (typeof desc!.get === "function") { - // deno-lint-ignore ban-ts-comment - //@ts-ignore - mock[property] = original[property]; - return; // continue - } - - mock[property] = desc!.value; - }); - - // Attach all of the original's functions to the mock - this.getAllFunctions(original).forEach((method: string) => { - const nativeMethods = [ - "__defineGetter__", - "__defineSetter__", - "__lookupGetter__", - "__lookupSetter__", - "constructor", - "hasOwnProperty", - "isPrototypeOf", - "propertyIsEnumerable", - "toLocaleString", - "toString", - "valueOf", - ]; - - if (nativeMethods.indexOf(method) == -1) { - if (!mock.calls[method]) { - mock.calls[method] = 0; - } - mock[method] = function (...args: unknown[]) { - // Add tracking - mock.calls[method]++; - - // Make sure the method calls its original self - let unbound = (original[method as keyof T] as unknown as ( - ...params: unknown[] - ) => unknown); - - // When method calls its original self, let `this` be the mock. Reason - // being the mock has tracking and the original does not. - const bound = unbound.bind(mock); - - return bound(...args); - }; - } else { - // copy nativeMethod directly without mocking - mock[method] = original[method as keyof T]; - } - }); - - return mock; - } - - /** - * Before constructing the mock object, track any constructur function args - * that need to be passed in when constructing the mock object. - * - * @param args - A rest parameter of arguments that will get passed in to - * the constructor function of the class being mocked. - * - * @returns `this` so that methods in this class can be chained. - */ - public withConstructorArgs(...args: unknown[]): this { - this.constructor_args = args; - return this; - } - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - METHODS - PROTECTED ///////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * Get all properties--public, protected, private--from the object that will - * be mocked. - * - * @param obj - The object that will be mocked. - * - * @returns An array of the object's properties. - */ - protected getAllProperties(obj: T): string[] { - let functions: string[] = []; - let clone = obj; - do { - functions = functions.concat(Object.getOwnPropertyNames(clone)); - } while ((clone = Object.getPrototypeOf(clone))); - - return functions.sort().filter( - function (e: string, i: number, arr: unknown[]) { - if ( - e != arr[i + 1] && typeof obj[e as keyof T] != "function" - ) { - return true; - } - }, - ); - } - - /** - * Get all functions--public, protected, private--from the object that will be - * mocked. - * - * @param obj - The object that will be mocked. - * - * @returns An array of the object's functions. - */ - protected getAllFunctions(obj: T): string[] { - let functions: string[] = []; - let clone = obj; - do { - functions = functions.concat(Object.getOwnPropertyNames(clone)); - } while ((clone = Object.getPrototypeOf(clone))); - - return functions.sort().filter( - function (e: string, i: number, arr: unknown[]) { - if ( - e != arr[i + 1] && typeof obj[e as keyof T] == "function" - ) { - return true; - } - }, - ); - } -} diff --git a/src/pre_programmed_method.ts b/src/pre_programmed_method.ts new file mode 100644 index 00000000..9ae32f85 --- /dev/null +++ b/src/pre_programmed_method.ts @@ -0,0 +1,92 @@ +import type { MethodOf } from "./types.ts"; +import type { IError } from "./interfaces.ts"; + +class PreProgrammedMethodError extends Error {} + +/** + * Class that allows to be a "stand-in" for a method. For example, when used in + * a mock object, the mock object can replace methods with pre-programmed + * methods (using this class), and have a system under test use the + * pre-programmed methods. + */ +export class PreProgrammedMethod { + #method_name: MethodOf; + #will_throw = false; + #will_return = false; + #return?: ReturnValue; + #error?: IError; + + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - CONSTRUCTOR ///////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + /** + * @param methodName - The name of the method to program. Must be a method of + * the original object in question. + */ + constructor(methodName: MethodOf) { + this.#method_name = methodName; + } + + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - GETTERS / SETTERS ////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + get error(): void { + if (!this.#will_throw) { + throw new PreProgrammedMethodError( + `Pre-programmed method "${this.#method_name}" is not set up to throw an error.`, + ); + } + if (this.#error === undefined) { + throw new PreProgrammedMethodError( + `Pre-programmed method "${this.#method_name}" is set up to throw an error, but no error was provided.`, + ); + } + + throw this.#error; + } + + get return(): ReturnValue { + if (this.#return === undefined) { + throw new PreProgrammedMethodError( + `Pre-programmed method "${this.#method_name}" does not have a return value.`, + ); + } + + return this.#return; + } + + get will_return(): boolean { + return this.#will_return; + } + + get will_throw(): boolean { + return this.#will_throw; + } + + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - METHODS - PUBLIC /////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + /** + * Pre-program this method to return the given value. + + * @param returnValue The value that should be returned when this object is + * being used in place of an original method. + */ + public willReturn(returnValue: ReturnValue): void { + this.#will_return = true; + this.#return = returnValue; + } + + /** + * Pre-program this method to throw the given error. + * + * @param error - The error to throw. + */ + public willThrow(error: IError): void { + this.#will_throw = true; + this.#error = error; + } +} diff --git a/src/rhum_asserts.ts b/src/rhum_asserts.ts deleted file mode 100644 index 981caac8..00000000 --- a/src/rhum_asserts.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { StdAsserts } from "../deps.ts"; - -// Saving this so i don't have to type it out for the 10th time -// export const asserts = { -// AssertionError: StdAsserts.AssertionError, -// _format: StdAsserts._format, -// assert: StdAsserts.assert, -// assertArrayIncludes: StdAsserts.assertArrayIncludes, -// assertEquals: StdAsserts.assertEquals, -// assertNotEquals: StdAsserts.assertNotEquals, -// assertNotMatch: StdAsserts.assertNotMatch, -// assertNotStrictEquals: StdAsserts.assertNotStrictEquals, -// assertStrictEquals: StdAsserts.assertStrictEquals, -// assertStringIncludes: StdAsserts.assertStringIncludes, -// assertMatch: StdAsserts.assertMatch, -// assertThrows: StdAsserts.assertThrows, -// assertThrowsAsync: StdAsserts.assertThrowsAsync, -// equal: StdAsserts.equal, -// fail: StdAsserts.fail, -// unimplemented: StdAsserts.unimplemented, -// unreachable: StdAsserts.unreachable -// }; - -export const asserts = { - ...StdAsserts, - assertArrayContains: StdAsserts.assertArrayIncludes, - assertStringContains: StdAsserts.assertStringIncludes, -}; - -export type assertions = keyof typeof asserts; diff --git a/src/test_case.ts b/src/test_case.ts deleted file mode 100644 index c02193bf..00000000 --- a/src/test_case.ts +++ /dev/null @@ -1,119 +0,0 @@ -const encoder = new TextEncoder(); -import type { ITestCase, ITestPlan } from "./interfaces.ts"; - -/** - * A class to help create uniform test case objects. - */ -export class TestCase { - /** - * The whole test plan for a given test - */ - protected plan: ITestPlan; - - /** - * @param plan - The test plan for a given test - */ - constructor(plan: ITestPlan) { - this.plan = plan; - } - - /** - * Runs the test plan and each test and hook - */ - public async run() { - // deno-lint-ignore no-prototype-builtins, eslint-ignore-next-line no-prototype-builtins - if (this.plan.hasOwnProperty("suites") === false) { - return; - } - - // Track the execution of hooks - let executedBeforeAllSuiteHook = false; - let executedAfterAllSuiteHook = false; - - Object.keys(this.plan.suites).forEach((suiteName, suiteIndex) => { - // Track the execution of hooks - let executedBeforeEachSuiteHook = false; - let executedAfterEachSuiteHook = false; - let executedBeforeAllCaseHook = false; - let executedAfterAllCaseHook = false; - - // Run cases - this.plan!.suites[suiteName].cases!.forEach( - async (c: ITestCase, caseIndex) => { - const isLastCase = - (this.plan!.suites[suiteName].cases!.length - 1) == caseIndex; - const isLastSuite = - (Object.keys(this.plan!.suites).length - 1) == suiteIndex; - // Run the case - required to run like this because the - // hooks need to be ran inside the Deno.test call. Deno.test seems to queue - // the tests, meaning all hooks are ran, and **then** the tests are ran - const hookAttachedTestFn = async () => { - if ( - this.plan.before_all_suite_hook && !executedBeforeAllSuiteHook - ) { - await this.plan.before_all_suite_hook(); - executedBeforeAllSuiteHook = true; - } - if ( - this.plan.before_each_suite_hook && !executedBeforeEachSuiteHook - ) { - await this.plan.before_each_suite_hook(); - executedBeforeEachSuiteHook = true; - } - if ( - this.plan.suites[suiteName].before_all_case_hook && - !executedBeforeAllCaseHook - ) { - await this.plan.suites[suiteName].before_all_case_hook!(); - executedBeforeAllCaseHook = true; - } - if (this.plan.suites[suiteName].before_each_case_hook) { - await this.plan.suites[suiteName].before_each_case_hook!(); - } - - await c.testFn(); - - if (this.plan.suites[suiteName].after_each_case_hook) { - await this.plan.suites[suiteName].after_each_case_hook!(); - } - if ( - this.plan.suites[suiteName].after_all_case_hook && - !executedAfterAllCaseHook && isLastCase - ) { - await this.plan.suites[suiteName].after_all_case_hook!(); - executedAfterAllCaseHook = true; - } - if ( - this.plan.after_each_suite_hook && !executedAfterEachSuiteHook - ) { - await this.plan.after_each_suite_hook(); - executedAfterEachSuiteHook = true; - } - if ( - this.plan.after_all_suite_hook && !executedAfterAllSuiteHook && - isLastSuite - ) { - await this.plan.after_all_suite_hook(); - executedAfterAllSuiteHook = true; - } - }; - // (ebebbington) To stop the output of test running being horrible - // in the CI, we will only display the new name which should be - // "plan | suite " case", as opposed to the "super saiyan" - // version. This name is generated differently inside `formatTestCaseName` - // based on if the tests are being ran inside a CI job - if (Deno.env.get("CI") === "true") { - await Deno.test(c.new_name, async () => { - await hookAttachedTestFn(); - }); - } else { - await Deno.test(c.new_name, async () => { - // Deno.stdout.writeSync(encoder.encode(c.new_name)); - await hookAttachedTestFn(); - }); - } - }, - ); - }); - } -} diff --git a/src/types.ts b/src/types.ts index 4c6125a6..17a6c0bd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,18 +1,20 @@ -export type TConstructorFunction = { - new (...args: unknown[]): T; - [key: string]: unknown; -}; - // deno-lint-ignore no-explicit-any -export type Constructor = new (...args: any[]) => T; +export type Constructor = new (...args: any[]) => T; + +export type MethodCalls = Record; + +export type MethodOf = { + // deno-lint-ignore no-explicit-any + [K in keyof Object]: Object[K] extends (...args: any[]) => unknown ? K + : never; +}[keyof Object]; + +export type MemberOf = { + [K in keyof Object]: Object[K]; +}[keyof Object]; -export type Mocked = T & { - calls: { [k in keyof T]: T[k] extends () => void ? number : never }; - is_mock: true; -}; +export type MockedObject = { [k: string]: unknown }; -export type Stubbed = T & { - calls: { [k in keyof T]?: T[k] extends () => void ? number : never }; - stub: (p: string, v: unknown) => void; - is_stubbed: true; -}; +export type StubReturnValue = T extends (...args: unknown[]) => unknown + ? () => R + : string; diff --git a/tests/cjs/unit/mod/dummy.test.js b/tests/cjs/unit/mod/dummy.test.js new file mode 100644 index 00000000..6ffbb45d --- /dev/null +++ b/tests/cjs/unit/mod/dummy.test.js @@ -0,0 +1,64 @@ +const { Dummy, Mock } = require("../../../../lib/cjs/mod"); + +describe("Dummy()", () => { + it("can fill parameter lists", () => { + const mockServiceOne = Mock(ServiceOne).create(); + const dummy3 = Dummy(ServiceThree); + + const resource = new Resource( + mockServiceOne, + Dummy(ServiceTwo), + dummy3, + ); + + resource.callServiceOne(); + expect(mockServiceOne.calls.methodServiceOne).toBe(1); + }); + + it("can be made without specifying constructor args", () => { + const dummy = Dummy(Resource); + expect(Object.getPrototypeOf(dummy)).toBe(Resource); + }); +}); + +class Resource { + constructor( + serviceOne, + serviceTwo, + serviceThree, + ) { + this.service_one = serviceOne; + this.service_two = serviceTwo; + this.service_three = serviceThree; + } + + callServiceOne() { + this.service_one.methodServiceOne(); + } + + callServiceTwo() { + this.service_two.methodServiceTwo(); + } + + callServiceThree() { + this.service_three.methodServiceThree(); + } +} + +class ServiceOne { + methodServiceOne() { + return "Method from ServiceOne was called."; + } +} + +class ServiceTwo { + methodServiceTwo() { + return "Method from ServiceTwo was called."; + } +} + +class ServiceThree { + methodServiceThree() { + return "Method from ServiceThree was called."; + } +} diff --git a/tests/cjs/unit/mod/fake.test.js b/tests/cjs/unit/mod/fake.test.js new file mode 100644 index 00000000..04eda05c --- /dev/null +++ b/tests/cjs/unit/mod/fake.test.js @@ -0,0 +1,304 @@ +const { Fake } = require("../../../../lib/cjs/mod"); + +describe("Fake()", () => { + it("creates fake builder", () => { + const fake = Fake(TestObjectOne); + expect(fake.constructor.name).toBe("FakeBuilder"); + }); + + describe(".create()", () => { + it("creates fake object", () => { + const fake = Fake(TestObjectTwo).create(); + expect(fake.name).toBe(undefined); + expect(fake.is_fake).toBe(true); + }); + }); + + describe(".withConstructorArgs(...)", () => { + it("can take 1 arg", () => { + const fake = Fake(TestObjectTwo) + .withConstructorArgs("some name") + .create(); + expect(fake.name).toBe("some name"); + expect(fake.is_fake).toBe(true); + }); + + it("can take more than 1 arg", () => { + const fake = Fake(TestObjectTwoMore) + .withConstructorArgs("some name", ["hello"]) + .create(); + expect(fake.name).toBe("some name"); + expect(fake.array).toStrictEqual(["hello"]); + expect(fake.is_fake).toBe(true); + }); + }); + + describe(".method(...)", () => { + it("requires .willReturn(...) or .willThrow(...) to be chained", () => { + const fake = Fake(TestObjectThree).create(); + expect(fake.is_fake).toBe(true); + + // Original returns "World" + expect(fake.test()).toBe("World"); + + // Don't fully pre-program the method. This should cause an error during assertions. + fake.method("test"); + + try { + fake.test(); + } catch (error) { + expect(error.message).toBe( + `Pre-programmed method "test" does not have a return value.`, + ); + } + }); + + it(".willReturn(...) does not call original method and returns given value", () => { + // Assert that a fake can make a class take a shortcut + const fakeServiceDoingShortcut = Fake(Repository).create(); + fakeServiceDoingShortcut.method("findAllUsers").willReturn("shortcut"); + const resourceWithShortcut = new Resource( + fakeServiceDoingShortcut, + ); + resourceWithShortcut.getUsers(); + expect(fakeServiceDoingShortcut.anotha_one_called).toBe(false); + expect(fakeServiceDoingShortcut.do_something_called).toBe(false); + expect(fakeServiceDoingShortcut.do_something_else_called).toBe(false); + + // Assert that the fake service can call original implementations + const fakeServiceNotDoingShortcut = Fake(Repository).create(); + const resourceWithoutShortcut = new Resource( + fakeServiceNotDoingShortcut, + ); + resourceWithoutShortcut.getUsers(); + expect(fakeServiceNotDoingShortcut.anotha_one_called).toBe(true); + expect(fakeServiceNotDoingShortcut.do_something_called).toBe(true); + expect(fakeServiceNotDoingShortcut.do_something_else_called).toBe(true); + }); + + it(".willReturn(...) can be performed more than once", () => { + // Assert that a fake can make a class take a shortcut + const fakeServiceDoingShortcut = Fake(Repository).create(); + fakeServiceDoingShortcut.method("findUserById").willReturn("shortcut"); + fakeServiceDoingShortcut.method("findUserById").willReturn("shortcut2"); + const resourceWithShortcut = new Resource( + fakeServiceDoingShortcut, + ); + resourceWithShortcut.getUser(1); + expect(fakeServiceDoingShortcut.anotha_one_called).toBe(false); + expect(fakeServiceDoingShortcut.do_something_called).toBe(false); + expect(fakeServiceDoingShortcut.do_something_else_called).toBe(false); + + // Assert that the fake service can call original implementations + const fakeServiceNotDoingShortcut = Fake(Repository).create(); + const resourceWithoutShortcut = new Resource( + fakeServiceNotDoingShortcut, + ); + resourceWithoutShortcut.getUser(1); + expect(fakeServiceNotDoingShortcut.anotha_one_called).toBe(true); + expect(fakeServiceNotDoingShortcut.do_something_called).toBe(true); + expect(fakeServiceNotDoingShortcut.do_something_else_called).toBe(true); + }); + + it(".willThrow(...) does not call original method and throws error", () => { + // Assert that a fake can make a class take a shortcut + const fakeServiceDoingShortcut = Fake(Repository).create(); + fakeServiceDoingShortcut.method("findAllUsers").willReturn("shortcut"); + const resourceWithShortcut = new Resource( + fakeServiceDoingShortcut, + ); + resourceWithShortcut.getUsers(); + expect(fakeServiceDoingShortcut.anotha_one_called).toBe(false); + expect(fakeServiceDoingShortcut.do_something_called).toBe(false); + expect(fakeServiceDoingShortcut.do_something_else_called).toBe(false); + + // Assert that the fake service can call original implementations + const fakeServiceNotDoingShortcut = Fake(Repository).create(); + const resourceWithoutShortcut = new Resource( + fakeServiceNotDoingShortcut, + ); + resourceWithoutShortcut.getUsers(); + expect(fakeServiceNotDoingShortcut.anotha_one_called).toBe(true); + expect(fakeServiceNotDoingShortcut.do_something_called).toBe(true); + expect(fakeServiceNotDoingShortcut.do_something_else_called).toBe(true); + }); + + it(".willReturn(fake) returns the fake object (basic)", () => { + const fake = Fake(TestObjectFourBuilder).create(); + expect(fake.is_fake).toBe(true); + + fake + .method("someComplexMethod") + .willReturn(fake); + + expect(fake.someComplexMethod()).toBe(fake); + }); + + it(".willReturn(fake) returns the fake object (extra)", () => { + // Assert that the original implementation sets properties + const fake1 = Fake(TestObjectFourBuilder).create(); + expect(fake1.is_fake).toBe(true); + fake1.someComplexMethod(); + expect(fake1.something_one).toBe("one"); + expect(fake1.something_two).toBe("two"); + + // Assert that the fake implementation will not set properties + const fake2 = Fake(TestObjectFourBuilder).create(); + expect(fake2.is_fake).toBe(true); + fake2 + .method("someComplexMethod") + .willReturn(fake2); + + expect(fake2.someComplexMethod()).toBe(fake2); + expect(fake2.something_one).toBe(undefined); + expect(fake2.something_two).toBe(undefined); + + // Assert that we can also use setters + const fake3 = Fake(TestObjectFourBuilder).create(); + expect(fake3.is_fake).toBe(true); + fake3.someComplexMethod(); + expect(fake3.something_one).toBe("one"); + expect(fake3.something_two).toBe("two"); + fake3.something_one = "you got changed"; + expect(fake3.something_one).toBe("you got changed"); + }); + + it(".willThrow() causes throwing RandomError (with constructor)", () => { + const fake = Fake(TestObjectThree).create(); + expect(fake.is_fake).toBe(true); + + // Original returns "World" + expect(fake.test()).toBe("World"); + + // Make the original method throw RandomError + fake + .method("test") + .willThrow(new RandomError("Random error message.")); + + expect(() => fake.test()).toThrow( + new RandomError("Random error message."), + ); + }); + + it(".willThrow() causes throwing RandomError2 (no constructor)", () => { + const fake = Fake(TestObjectThree).create(); + expect(fake.is_fake).toBe(true); + + // Original returns "World" + expect(fake.test()).toBe("World"); + + // Make the original method throw RandomError + fake + .method("test") + .willThrow(new RandomError2()); + + expect(() => fake.test()).toThrow( + new RandomError2("Some message not by the constructor."), + ); + }); + }); +}); + +//////////////////////////////////////////////////////////////////////////////// +// FILE MARKER - DATA ////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +class TestObjectOne { +} + +class TestObjectTwo { + constructor(name) { + this.name = name; + } +} + +class TestObjectTwoMore { + constructor(name, array) { + this.name = name; + this.array = array; + } +} + +class TestObjectThree { + hello() { + return; + } + + test() { + this.hello(); + this.hello(); + return "World"; + } +} + +class Resource { + constructor( + serviceOne, + ) { + this.repository = serviceOne; + } + + getUsers() { + this.repository.findAllUsers(); + } + + getUser(id) { + this.repository.findUserById(id); + } +} + +class TestObjectFourBuilder { + someComplexMethod() { + this.setSomethingOne(); + this.setSomethingTwo(); + return this; + } + + setSomethingOne() { + this.something_one = "one"; + } + + setSomethingTwo() { + this.something_two = "two"; + } +} + +class Repository { + constructor() { + this.anotha_one_called = false; + this.do_something_called = false; + this.do_something_else_called = false; + } + + findAllUsers() { + this.doSomething(); + this.doSomethingElse(); + this.anothaOne(); + return "Finding all users"; + } + + findUserById(id) { + this.doSomething(); + this.doSomethingElse(); + this.anothaOne(); + return `Finding user by id #${id}`; + } + + anothaOne() { + this.anotha_one_called = true; + } + + doSomething() { + this.do_something_called = true; + } + + doSomethingElse() { + this.do_something_else_called = true; + } +} + +class RandomError extends Error {} +class RandomError2 extends Error { + name = "RandomError2Name"; + message = "Some message not by the constructor."; +} diff --git a/tests/cjs/unit/mod/mock.test.js b/tests/cjs/unit/mod/mock.test.js new file mode 100644 index 00000000..004b91bd --- /dev/null +++ b/tests/cjs/unit/mod/mock.test.js @@ -0,0 +1,433 @@ +const { Mock } = require("../../../../lib/cjs/mod"); + +class Request { + constructor( + url, + options = {}, + ) { + this.url = url; + this.options = options; + this.headers = this.createHeaders(options.headers); + this.method = options.method ? options.method : "get"; + } + + createHeaders(headers) { + const map = new Map(); + for (const header in headers) { + const value = headers[header]; + map.set(header, value); + } + return map; + } +} + +class TestObjectOne { +} + +class TestObjectTwo { + constructor(name) { + this.name = name; + } +} + +class TestObjectTwoMore { + constructor(name, array) { + this.name = name; + this.array = array; + } +} + +class TestObjectThree { + hello() { + return; + } + + test() { + this.hello(); + this.hello(); + return "World"; + } +} + +class TestObjectFourBuilder { + someComplexMethod() { + this.setSomethingOne(); + this.setSomethingTwo(); + return this; + } + + setSomethingOne() { + this.something_one = "one"; + } + + setSomethingTwo() { + this.something_two = "two"; + } +} + +class RandomError extends Error {} +class RandomError2 extends Error { + name = "RandomError2Name"; + message = "Some message not by the constructor."; +} + +class MathService { + add( + num1, + num2, + useNestedAdd = false, + ) { + if (useNestedAdd) { + return this.nestedAdd(num1, num2); + } + return num1 + num2; + } + + nestedAdd(num1, num2) { + return num1 + num2; + } +} + +class TestObjectLotsOfDataMembers { + age = 0; + math_service = undefined; + protected_property = "I AM PROTECTED PROPERTY."; + constructor(name, mathService) { + this.math_service = mathService; + this.name = name; + } + sum( + num1, + num2, + useNestedAdd = false, + ) { + const sum = this.math_service.add(num1, num2, useNestedAdd); + return sum; + } + protectedMethod() { + return "I AM A PROTECTED METHOD."; + } +} + +class TestRequestHandler { + handle(request) { + const method = request.method.toLowerCase(); + const contentType = request.headers.get("content-type"); + + if (method !== "post") { + return "Method is not post"; + } + + if (contentType !== "application/json") { + return "Content-Type is incorrect"; + } + + return "posted"; + } +} + +describe("Mock()", () => { + it( + "creates mock builder", + () => { + const mock = Mock(TestObjectOne); + expect(mock.constructor.name).toBe("MockBuilder"); + }, + ); + + describe(".create()", () => { + it( + "creates mock object", + () => { + const mock = Mock(TestObjectTwo).create(); + expect(mock.name).toBe(undefined); + expect(mock.is_mock).toBe(true); + }, + ); + }); + + describe(".withConstructorArgs(...)", () => { + it( + "can take 1 arg", + () => { + const mock = Mock(TestObjectTwo) + .withConstructorArgs("some name") + .create(); + expect(mock.name).toBe("some name"); + expect(mock.is_mock).toBe(true); + }, + ); + + it( + "can take more than 1 arg", + () => { + const mock = Mock(TestObjectTwoMore) + .withConstructorArgs("some name", ["hello"]) + .create(); + expect(mock.name).toBe("some name"); + expect(mock.array).toStrictEqual(["hello"]); + expect(mock.is_mock).toBe(true); + + const mockMathService = Mock(MathService) + .create(); + const mockTestObject = Mock(TestObjectLotsOfDataMembers) + .withConstructorArgs("has mocked math service", mockMathService) + .create(); + expect(mockMathService.calls.add).toBe(0); + mockTestObject.sum(1, 1); + expect(mockMathService.calls.add).toBe(1); + }, + ); + }); + + describe("method(...)", () => { + it( + "requires .willReturn(...) or .willThrow(...) to be chained", + () => { + const mock = Mock(TestObjectThree).create(); + expect(mock.is_mock).toBe(true); + + // Original returns "World" + expect(mock.test()).toBe("World"); + + // Don't fully pre-program the method. This should cause an error during assertions. + mock.method("test"); + + try { + mock.test(); + } catch (error) { + expect(error.message).toBe( + `Pre-programmed method "test" does not have a return value.`, + ); + } + expect(mock.calls.test).toBe(2); + expect(mock.calls.hello).toBe(2); + }, + ); + + it(".willReturn(...) does not call original method and returns given value", () => { + const mock = Mock(TestObjectThree).create(); + expect(mock.is_mock).toBe(true); + + // Original returns "World" + expect(mock.test()).toBe("World"); + + // Change original to return "Hello" + mock.method("test").willReturn("Hello"); + + // Should output "Hello" and make the following calls + expect(mock.test()).toBe("Hello"); + expect(mock.calls.test).toBe(2); + expect(mock.calls.hello).toBe(2); + }); + + it( + ".willReturn(...) can be performed more than once", + () => { + const mock = Mock(TestObjectThree).create(); + expect(mock.is_mock).toBe(true); + + // Original returns "World" + expect(mock.test()).toBe("World"); + + // Change original to return "Hello" + mock.method("test").willReturn("Hello"); + mock.method("test").willReturn("Hello!"); + + expect(mock.test()).toBe("Hello!"); + expect(mock.calls.test).toBe(2); + expect(mock.calls.hello).toBe(2); + }, + ); + + it( + ".willReturn(...) returns specified value", + () => { + const mock = Mock(TestObjectThree).create(); + expect(mock.is_mock).toBe(true); + + // Original returns "World" + expect(mock.test()).toBe("World"); + + // Don't fully pre-program the method. This should cause an error during + // assertions. + mock + .method("test") + .willReturn({ + "something": undefined, + }); + + expect(mock.test()).toStrictEqual({ "something": undefined }); + expect(mock.calls.test).toBe(2); + expect(mock.calls.hello).toBe(2); + }, + ); + + it(".willReturn(mock) returns the mock object (basic)", () => { + const mock = Mock(TestObjectFourBuilder).create(); + expect(mock.is_mock).toBe(true); + + mock + .method("someComplexMethod") + .willReturn(mock); + + expect(mock.someComplexMethod()).toBe(mock); + expect(mock.calls.someComplexMethod).toBe(1); + }); + + it(".willReturn(mock) returns the mock object (extra)", () => { + // Assert that the original implementation sets properties + const mock1 = Mock(TestObjectFourBuilder).create(); + expect(mock1.is_mock).toBe(true); + mock1.someComplexMethod(); + expect(mock1.something_one).toBe("one"); + expect(mock1.something_two).toBe("two"); + expect(mock1.calls.someComplexMethod).toBe(1); + + // Assert that the mock implementation will not set properties + const mock2 = Mock(TestObjectFourBuilder).create(); + expect(mock2.is_mock).toBe(true); + mock2 + .method("someComplexMethod") + .willReturn(mock2); + + expect(mock2.someComplexMethod()).toBe(mock2); + expect(mock2.something_one).toBe(undefined); + expect(mock2.something_two).toBe(undefined); + expect(mock2.calls.someComplexMethod).toBe(1); + + // Assert that we can also use setters + const mock3 = Mock(TestObjectFourBuilder).create(); + expect(mock3.is_mock).toBe(true); + mock3.someComplexMethod(); + expect(mock3.something_one).toBe("one"); + expect(mock3.something_two).toBe("two"); + mock3.something_one = "you got changed"; + expect(mock3.something_one).toBe("you got changed"); + expect(mock3.calls.someComplexMethod).toBe(1); + }); + + it( + ".willThrow() causes throwing RandomError (with constructor)", + () => { + const mock = Mock(TestObjectThree).create(); + expect(mock.is_mock).toBe(true); + + // Original returns "World" + expect(mock.test()).toBe("World"); + + // Make the original method throw RandomError + mock + .method("test") + .willThrow(new RandomError("Random error message.")); + + expect( + () => mock.test(), + ).toThrow(new RandomError("Random error message.")); + expect(mock.calls.test).toBe(2); + }, + ); + + it( + ".willThrow() causes throwing RandomError2 (no constructor)", + () => { + const mock = Mock(TestObjectThree).create(); + expect(mock.is_mock).toBe(true); + + // Original returns "World" + expect(mock.test()).toBe("World"); + + // Make the original method throw RandomError + mock + .method("test") + .willThrow(new RandomError2()); + + expect(() => mock.test()).toThrow( + new RandomError2("Some message not by the constructor."), + ); + expect(mock.calls.test).toBe(2); + }, + ); + + it( + ".expects(...).toBeCalled(...)", + () => { + const mock = Mock(TestObjectThree).create(); + expect(mock.is_mock).toBe(true); + + mock.expects("hello").toBeCalled(2); + mock.test(); + mock.verifyExpectations(); + }, + ); + }); + + // TODO(crookse) Put the below tests into one of the groups above this line + + describe("call count for outside nested function is increased", () => { + const mockMathService = Mock(MathService) + .create(); + const mockTestObject = Mock(TestObjectLotsOfDataMembers) + .withConstructorArgs("has mocked math service", mockMathService) + .create(); + expect(mockMathService.calls.add).toBe(0); + expect(mockMathService.calls.nestedAdd).toBe(0); + mockTestObject.sum(1, 1, true); + expect(mockMathService.calls.add).toBe(1); + expect(mockMathService.calls.nestedAdd).toBe(1); + }); + + describe("can mock getters and setters", () => { + const mock = Mock(TestObjectLotsOfDataMembers) + .create(); + mock.age = 999; + expect(mock.age).toBe(999); + }); + + describe("native request mock", () => { + const router = Mock(TestRequestHandler).create(); + + const reqPost = new Request("https://google.com", { + method: "post", + headers: { + "content-type": "application/json", + }, + }); + expect(router.calls.handle).toBe(0); + expect(router.handle(reqPost)).toBe("posted"); + expect(router.calls.handle).toBe(1); + + const reqPostNotJson = new Request("https://google.com", { + method: "post", + }); + expect(router.calls.handle).toBe(1); + expect(router.handle(reqPostNotJson)).toBe("Content-Type is incorrect"); + expect(router.calls.handle).toBe(2); + + const reqGet = new Request("https://google.com", { + method: "get", + }); + + expect(router.calls.handle).toBe(2); + expect(router.handle(reqGet)).toBe("Method is not post"); + expect(router.calls.handle).toBe(3); + }); + + describe("sets the default value for getters", () => { + class Game { + } + + class PlayersEngine { + game = new Game(); + get Game() { + return this.game; + } + set Game(val) { + this.game = val; + } + } + + const mock = Mock(PlayersEngine).create(); + expect(mock.Game instanceof Game).toBe(true); + }); +}); diff --git a/tests/cjs/unit/mod/stub.test.js b/tests/cjs/unit/mod/stub.test.js new file mode 100644 index 00000000..4bf8afd3 --- /dev/null +++ b/tests/cjs/unit/mod/stub.test.js @@ -0,0 +1,44 @@ +const { Stub } = require("../../../../lib/cjs/mod"); + +class Server { + greeting = "hello"; + + methodThatLogs() { + return "server is running!"; + } +} + +describe("Stub()", () => { + it("can stub a class property", () => { + const server = new Server(); + expect(server.greeting).toBe("hello"); + Stub(server, "greeting", "you got changed"); + expect(server.greeting).toBe("you got changed"); + Stub(server, "greeting", null); + expect(server.greeting).toBe(null); + Stub(server, "greeting", true); + expect(server.greeting).toBe(true); + const obj = { test: "hello" }; + Stub(server, "greeting", obj); + expect(server.greeting).toBe(obj); + }); + + it("can stub a class method", () => { + const server = new Server(); + expect(server.methodThatLogs()).toBe("server is running!"); + Stub(server, "methodThatLogs"); + expect(server.methodThatLogs()).toBe("stubbed"); + Stub(server, "methodThatLogs", null); + expect(server.methodThatLogs()).toBe(null); + Stub(server, "methodThatLogs", true); + expect(server.methodThatLogs()).toBe(true); + const obj = { test: "hello" }; + Stub(server, "methodThatLogs", obj); + expect(server.methodThatLogs()).toBe(obj); + }); + + it("can return a stubbed function", () => { + const stub = Stub(); + expect(stub()).toBe("stubbed"); + }); +}); diff --git a/tests/deno/deps.ts b/tests/deno/deps.ts new file mode 100644 index 00000000..28112501 --- /dev/null +++ b/tests/deno/deps.ts @@ -0,0 +1,4 @@ +export { + assertEquals, + assertThrows, +} from "https://deno.land/std@0.134.0/testing/asserts.ts"; diff --git a/tests/deno/unit/mod/dummy_test.ts b/tests/deno/unit/mod/dummy_test.ts new file mode 100644 index 00000000..0559d506 --- /dev/null +++ b/tests/deno/unit/mod/dummy_test.ts @@ -0,0 +1,75 @@ +import { Dummy, Mock } from "../../../../mod.ts"; +import { assertEquals } from "../../../deps.ts"; + +Deno.test("Dummy()", async (t) => { + await t.step({ + name: "can fill parameter lists", + fn(): void { + const mockServiceOne = Mock(ServiceOne).create(); + const dummy3 = Dummy(ServiceThree); + + const resource = new Resource( + mockServiceOne, + Dummy(ServiceTwo), + dummy3, + ); + + resource.callServiceOne(); + assertEquals(mockServiceOne.calls.methodServiceOne, 1); + }, + }); + + await t.step({ + name: "can be made without specifying constructor args", + fn(): void { + const dummy = Dummy(Resource); + assertEquals(Object.getPrototypeOf(dummy), Resource); + }, + }); +}); + +class Resource { + #service_one: ServiceOne; + #service_two: ServiceTwo; + #service_three: ServiceThree; + + constructor( + serviceOne: ServiceOne, + serviceTwo: ServiceTwo, + serviceThree: ServiceThree, + ) { + this.#service_one = serviceOne; + this.#service_two = serviceTwo; + this.#service_three = serviceThree; + } + + public callServiceOne() { + this.#service_one.methodServiceOne(); + } + + public callServiceTwo() { + this.#service_two.methodServiceTwo(); + } + + public callServiceThree() { + this.#service_three.methodServiceThree(); + } +} + +class ServiceOne { + public methodServiceOne() { + return "Method from ServiceOne was called."; + } +} + +class ServiceTwo { + public methodServiceTwo() { + return "Method from ServiceTwo was called."; + } +} + +class ServiceThree { + public methodServiceThree() { + return "Method from ServiceThree was called."; + } +} diff --git a/tests/deno/unit/mod/fake_test.ts b/tests/deno/unit/mod/fake_test.ts new file mode 100644 index 00000000..7575c61f --- /dev/null +++ b/tests/deno/unit/mod/fake_test.ts @@ -0,0 +1,374 @@ +import { Fake } from "../../../../mod.ts"; +import { assertEquals, assertThrows } from "../../../deps.ts"; + +Deno.test("Fake()", async (t) => { + await t.step({ + name: "creates fake builder", + fn(): void { + const fake = Fake(TestObjectOne); + assertEquals(fake.constructor.name, "FakeBuilder"); + }, + }); + + await t.step(".create()", async (t) => { + await t.step({ + name: "creates fake object", + fn(): void { + const fake = Fake(TestObjectTwo).create(); + assertEquals(fake.name, undefined); + assertEquals(fake.is_fake, true); + }, + }); + }); + + await t.step(".withConstructorArgs(...)", async (t) => { + await t.step({ + name: "can take 1 arg", + fn(): void { + const fake = Fake(TestObjectTwo) + .withConstructorArgs("some name") + .create(); + assertEquals(fake.name, "some name"); + assertEquals(fake.is_fake, true); + }, + }); + + await t.step({ + name: "can take more than 1 arg", + fn(): void { + const fake = Fake(TestObjectTwoMore) + .withConstructorArgs("some name", ["hello"]) + .create(); + assertEquals(fake.name, "some name"); + assertEquals(fake.array, ["hello"]); + assertEquals(fake.is_fake, true); + }, + }); + }); + + await t.step(".method(...)", async (t) => { + await t.step({ + name: "requires .willReturn(...) or .willThrow(...) to be chained", + fn(): void { + const fake = Fake(TestObjectThree).create(); + assertEquals(fake.is_fake, true); + + // Original returns "World" + assertEquals(fake.test(), "World"); + + // Don't fully pre-program the method. This should cause an error during assertions. + fake.method("test"); + + try { + fake.test(); + } catch (error) { + assertEquals( + error.message, + `Pre-programmed method "test" does not have a return value.`, + ); + } + }, + }); + + await t.step({ + name: + ".willReturn(...) does not call original method and returns given value", + fn(): void { + // Assert that a fake can make a class take a shortcut + const fakeServiceDoingShortcut = Fake(Repository).create(); + fakeServiceDoingShortcut.method("findAllUsers").willReturn("shortcut"); + const resourceWithShortcut = new Resource( + fakeServiceDoingShortcut, + ); + resourceWithShortcut.getUsers(); + assertEquals(fakeServiceDoingShortcut.anotha_one_called, false); + assertEquals(fakeServiceDoingShortcut.do_something_called, false); + assertEquals(fakeServiceDoingShortcut.do_something_else_called, false); + + // Assert that the fake service can call original implementations + const fakeServiceNotDoingShortcut = Fake(Repository).create(); + const resourceWithoutShortcut = new Resource( + fakeServiceNotDoingShortcut, + ); + resourceWithoutShortcut.getUsers(); + assertEquals(fakeServiceNotDoingShortcut.anotha_one_called, true); + assertEquals(fakeServiceNotDoingShortcut.do_something_called, true); + assertEquals( + fakeServiceNotDoingShortcut.do_something_else_called, + true, + ); + }, + }); + + await t.step({ + name: ".willReturn(...) can be performed more than once", + fn(): void { + // Assert that a fake can make a class take a shortcut + const fakeServiceDoingShortcut = Fake(Repository).create(); + fakeServiceDoingShortcut.method("findUserById").willReturn("shortcut"); + fakeServiceDoingShortcut.method("findUserById").willReturn("shortcut2"); + const resourceWithShortcut = new Resource( + fakeServiceDoingShortcut, + ); + resourceWithShortcut.getUser(1); + assertEquals(fakeServiceDoingShortcut.anotha_one_called, false); + assertEquals(fakeServiceDoingShortcut.do_something_called, false); + assertEquals(fakeServiceDoingShortcut.do_something_else_called, false); + + // Assert that the fake service can call original implementations + const fakeServiceNotDoingShortcut = Fake(Repository).create(); + const resourceWithoutShortcut = new Resource( + fakeServiceNotDoingShortcut, + ); + resourceWithoutShortcut.getUser(1); + assertEquals(fakeServiceNotDoingShortcut.anotha_one_called, true); + assertEquals(fakeServiceNotDoingShortcut.do_something_called, true); + assertEquals( + fakeServiceNotDoingShortcut.do_something_else_called, + true, + ); + }, + }); + + await t.step({ + name: ".willThrow(...) does not call original method and throws error", + fn(): void { + // Assert that a fake can make a class take a shortcut + const fakeServiceDoingShortcut = Fake(Repository).create(); + fakeServiceDoingShortcut.method("findAllUsers").willReturn("shortcut"); + const resourceWithShortcut = new Resource( + fakeServiceDoingShortcut, + ); + resourceWithShortcut.getUsers(); + assertEquals(fakeServiceDoingShortcut.anotha_one_called, false); + assertEquals(fakeServiceDoingShortcut.do_something_called, false); + assertEquals(fakeServiceDoingShortcut.do_something_else_called, false); + + // Assert that the fake service can call original implementations + const fakeServiceNotDoingShortcut = Fake(Repository).create(); + const resourceWithoutShortcut = new Resource( + fakeServiceNotDoingShortcut, + ); + resourceWithoutShortcut.getUsers(); + assertEquals(fakeServiceNotDoingShortcut.anotha_one_called, true); + assertEquals(fakeServiceNotDoingShortcut.do_something_called, true); + assertEquals( + fakeServiceNotDoingShortcut.do_something_else_called, + true, + ); + }, + }); + + await t.step({ + name: ".willReturn(fake) returns the fake object (basic)", + fn(): void { + const fake = Fake(TestObjectFourBuilder).create(); + assertEquals(fake.is_fake, true); + + fake + .method("someComplexMethod") + .willReturn(fake); + + assertEquals(fake.someComplexMethod(), fake); + }, + }); + + await t.step({ + name: ".willReturn(fake) returns the fake object (extra)", + fn(): void { + // Assert that the original implementation sets properties + const fake1 = Fake(TestObjectFourBuilder).create(); + assertEquals(fake1.is_fake, true); + fake1.someComplexMethod(); + assertEquals(fake1.something_one, "one"); + assertEquals(fake1.something_two, "two"); + + // Assert that the fake implementation will not set properties + const fake2 = Fake(TestObjectFourBuilder).create(); + assertEquals(fake2.is_fake, true); + fake2 + .method("someComplexMethod") + .willReturn(fake2); + + assertEquals(fake2.someComplexMethod(), fake2); + assertEquals(fake2.something_one, undefined); + assertEquals(fake2.something_two, undefined); + + // Assert that we can also use setters + const fake3 = Fake(TestObjectFourBuilder).create(); + assertEquals(fake3.is_fake, true); + fake3.someComplexMethod(); + assertEquals(fake3.something_one, "one"); + assertEquals(fake3.something_two, "two"); + fake3.something_one = "you got changed"; + assertEquals(fake3.something_one, "you got changed"); + }, + }); + + await t.step({ + name: ".willThrow() causes throwing RandomError (with constructor)", + fn(): void { + const fake = Fake(TestObjectThree).create(); + assertEquals(fake.is_fake, true); + + // Original returns "World" + assertEquals(fake.test(), "World"); + + // Make the original method throw RandomError + fake + .method("test") + .willThrow(new RandomError("Random error message.")); + + assertThrows( + () => fake.test(), + RandomError, + "Random error message.", + ); + }, + }); + + await t.step({ + name: ".willThrow() causes throwing RandomError2 (no constructor)", + fn(): void { + const fake = Fake(TestObjectThree).create(); + assertEquals(fake.is_fake, true); + + // Original returns "World" + assertEquals(fake.test(), "World"); + + // Make the original method throw RandomError + fake + .method("test") + .willThrow(new RandomError2()); + + assertThrows( + () => fake.test(), + RandomError2, + "Some message not by the constructor.", + ); + }, + }); + }); +}); + +//////////////////////////////////////////////////////////////////////////////// +// FILE MARKER - DATA ////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +class TestObjectOne { +} + +class TestObjectTwo { + public name: string; + constructor(name: string) { + this.name = name; + } +} + +class TestObjectTwoMore { + public name: string; + public array: string[]; + constructor(name: string, array: string[]) { + this.name = name; + this.array = array; + } +} + +class TestObjectThree { + public hello(): void { + return; + } + + public test(): string { + this.hello(); + this.hello(); + return "World"; + } +} + +class Resource { + #repository: Repository; + + constructor( + serviceOne: Repository, + ) { + this.#repository = serviceOne; + } + + public getUsers() { + this.#repository.findAllUsers(); + } + + public getUser(id: number) { + this.#repository.findUserById(id); + } +} + +class TestObjectFourBuilder { + #something_one?: string; + #something_two?: string; + + get something_one(): string | undefined { + return this.#something_one; + } + + set something_one(value: string | undefined) { + this.#something_one = value; + } + + get something_two(): string | undefined { + return this.#something_two; + } + + someComplexMethod(): this { + this.#setSomethingOne(); + this.#setSomethingTwo(); + return this; + } + + #setSomethingOne(): void { + this.#something_one = "one"; + } + + #setSomethingTwo(): void { + this.#something_two = "two"; + } +} + +class Repository { + public anotha_one_called = false; + public do_something_called = false; + public do_something_else_called = false; + + public findAllUsers(): string { + this.#doSomething(); + this.#doSomethingElse(); + this.#anothaOne(); + return "Finding all users"; + } + + public findUserById(id: number): string { + this.#doSomething(); + this.#doSomethingElse(); + this.#anothaOne(); + return `Finding user by id #${id}`; + } + + #anothaOne() { + this.anotha_one_called = true; + } + + #doSomething() { + this.do_something_called = true; + } + + #doSomethingElse() { + this.do_something_else_called = true; + } +} + +class RandomError extends Error {} +class RandomError2 extends Error { + public name = "RandomError2Name"; + public message = "Some message not by the constructor."; +} diff --git a/tests/deno/unit/mod/mock_test.ts b/tests/deno/unit/mod/mock_test.ts new file mode 100644 index 00000000..1bf2c115 --- /dev/null +++ b/tests/deno/unit/mod/mock_test.ts @@ -0,0 +1,466 @@ +import { Mock } from "../../../../mod.ts"; +import { assertEquals, assertThrows } from "../../../deps.ts"; + +Deno.test("Mock()", async (t) => { + await t.step({ + name: "creates mock builder", + fn(): void { + const mock = Mock(TestObjectOne); + assertEquals(mock.constructor.name, "MockBuilder"); + }, + }); + + await t.step(".create()", async (t) => { + await t.step({ + name: "creates mock object", + fn(): void { + const mock = Mock(TestObjectTwo).create(); + assertEquals(mock.name, undefined); + assertEquals(mock.is_mock, true); + }, + }); + }); + + await t.step(".withConstructorArgs(...)", async (t) => { + await t.step({ + name: "can take 1 arg", + fn(): void { + const mock = Mock(TestObjectTwo) + .withConstructorArgs("some name") + .create(); + assertEquals(mock.name, "some name"); + assertEquals(mock.is_mock, true); + }, + }); + + await t.step({ + name: "can take more than 1 arg", + fn(): void { + const mock = Mock(TestObjectTwoMore) + .withConstructorArgs("some name", ["hello"]) + .create(); + assertEquals(mock.name, "some name"); + assertEquals(mock.array, ["hello"]); + assertEquals(mock.is_mock, true); + + const mockMathService = Mock(MathService) + .create(); + const mockTestObject = Mock(TestObjectLotsOfDataMembers) + .withConstructorArgs("has mocked math service", mockMathService) + .create(); + assertEquals(mockMathService.calls.add, 0); + mockTestObject.sum(1, 1); + assertEquals(mockMathService.calls.add, 1); + }, + }); + }); + + await t.step("method(...)", async (t) => { + await t.step({ + name: "requires .willReturn(...) or .willThrow(...) to be chained", + fn(): void { + const mock = Mock(TestObjectThree).create(); + assertEquals(mock.is_mock, true); + + // Original returns "World" + assertEquals(mock.test(), "World"); + + // Don't fully pre-program the method. This should cause an error during assertions. + mock.method("test"); + + try { + mock.test(); + } catch (error) { + assertEquals( + error.message, + `Pre-programmed method "test" does not have a return value.`, + ); + } + assertEquals(mock.calls.test, 2); + assertEquals(mock.calls.hello, 2); + }, + }); + + await t.step({ + name: + ".willReturn(...) does not call original method and returns given value", + fn(): void { + const mock = Mock(TestObjectThree).create(); + assertEquals(mock.is_mock, true); + + // Original returns "World" + assertEquals(mock.test(), "World"); + + // Change original to return "Hello" + mock.method("test").willReturn("Hello"); + + // Should output "Hello" and make the following calls + assertEquals(mock.test(), "Hello"); + assertEquals(mock.calls.test, 2); + assertEquals(mock.calls.hello, 2); + }, + }); + + await t.step({ + name: ".willReturn(...) can be performed more than once", + fn(): void { + const mock = Mock(TestObjectThree).create(); + assertEquals(mock.is_mock, true); + + // Original returns "World" + assertEquals(mock.test(), "World"); + + // Change original to return "Hello" + mock.method("test").willReturn("Hello"); + mock.method("test").willReturn("Hello!"); + + assertEquals(mock.test(), "Hello!"); + assertEquals(mock.calls.test, 2); + assertEquals(mock.calls.hello, 2); + }, + }); + + await t.step({ + name: ".willReturn(...) returns specified value", + fn(): void { + const mock = Mock(TestObjectThree).create(); + assertEquals(mock.is_mock, true); + + // Original returns "World" + assertEquals(mock.test(), "World"); + + // Don't fully pre-program the method. This should cause an error during + // assertions. + mock + .method("test") + .willReturn({ + name: "something", + }); + + assertEquals(mock.test(), { name: "something" }); + assertEquals(mock.calls.test, 2); + assertEquals(mock.calls.hello, 2); + }, + }); + + await t.step({ + name: ".willReturn(mock) returns the mock object (basic)", + fn(): void { + const mock = Mock(TestObjectFourBuilder).create(); + assertEquals(mock.is_mock, true); + + mock + .method("someComplexMethod") + .willReturn(mock); + + assertEquals(mock.someComplexMethod(), mock); + assertEquals(mock.calls.someComplexMethod, 1); + }, + }); + + await t.step({ + name: ".willReturn(mock) returns the mock object (extra)", + fn(): void { + // Assert that the original implementation sets properties + const mock1 = Mock(TestObjectFourBuilder).create(); + assertEquals(mock1.is_mock, true); + mock1.someComplexMethod(); + assertEquals(mock1.something_one, "one"); + assertEquals(mock1.something_two, "two"); + assertEquals(mock1.calls.someComplexMethod, 1); + + // Assert that the mock implementation will not set properties + const mock2 = Mock(TestObjectFourBuilder).create(); + assertEquals(mock2.is_mock, true); + mock2 + .method("someComplexMethod") + .willReturn(mock2); + + assertEquals(mock2.someComplexMethod(), mock2); + assertEquals(mock2.something_one, undefined); + assertEquals(mock2.something_two, undefined); + assertEquals(mock2.calls.someComplexMethod, 1); + + // Assert that we can also use setters + const mock3 = Mock(TestObjectFourBuilder).create(); + assertEquals(mock3.is_mock, true); + mock3.someComplexMethod(); + assertEquals(mock3.something_one, "one"); + assertEquals(mock3.something_two, "two"); + mock3.something_one = "you got changed"; + assertEquals(mock3.something_one, "you got changed"); + assertEquals(mock3.calls.someComplexMethod, 1); + }, + }); + + await t.step({ + name: ".willThrow() causes throwing RandomError (with constructor)", + fn(): void { + const mock = Mock(TestObjectThree).create(); + assertEquals(mock.is_mock, true); + + // Original returns "World" + assertEquals(mock.test(), "World"); + + // Make the original method throw RandomError + mock + .method("test") + .willThrow(new RandomError("Random error message.")); + + assertThrows( + () => mock.test(), + RandomError, + "Random error message.", + ); + assertEquals(mock.calls.test, 2); + }, + }); + + await t.step({ + name: ".willThrow() causes throwing RandomError2 (no constructor)", + fn(): void { + const mock = Mock(TestObjectThree).create(); + assertEquals(mock.is_mock, true); + + // Original returns "World" + assertEquals(mock.test(), "World"); + + // Make the original method throw RandomError + mock + .method("test") + .willThrow(new RandomError2()); + + assertThrows( + () => mock.test(), + RandomError2, + "Some message not by the constructor.", + ); + assertEquals(mock.calls.test, 2); + }, + }); + + await t.step({ + name: ".expects(...).toBeCalled(...)", + fn(): void { + const mock = Mock(TestObjectThree).create(); + assertEquals(mock.is_mock, true); + + mock.expects("hello").toBeCalled(2); + mock.test(); + mock.verifyExpectations(); + }, + }); + }); + + // TODO(crookse) Put the below tests into one of the groups above this line + + await t.step("call count for outside nested function is increased", () => { + const mockMathService = Mock(MathService) + .create(); + const mockTestObject = Mock(TestObjectLotsOfDataMembers) + .withConstructorArgs("has mocked math service", mockMathService) + .create(); + assertEquals(mockMathService.calls.add, 0); + assertEquals(mockMathService.calls.nestedAdd, 0); + mockTestObject.sum(1, 1, true); + assertEquals(mockMathService.calls.add, 1); + assertEquals(mockMathService.calls.nestedAdd, 1); + }); + + await t.step("can mock getters and setters", () => { + const mock = Mock(TestObjectLotsOfDataMembers) + .create(); + mock.age = 999; + assertEquals(mock.age, 999); + }); + + await t.step("native request mock", async () => { + const router = Mock(TestRequestHandler).create(); + + const reqPost = new Request("https://google.com", { + method: "post", + headers: { + "content-type": "application/json", + }, + }); + assertEquals(router.calls.handle, 0); + assertEquals(await router.handle(reqPost), "posted"); + assertEquals(router.calls.handle, 1); + + const reqPostNotJson = new Request("https://google.com", { + method: "post", + }); + assertEquals(router.calls.handle, 1); + assertEquals( + await router.handle(reqPostNotJson), + "Content-Type is incorrect", + ); + assertEquals(router.calls.handle, 2); + + const reqGet = new Request("https://google.com", { + method: "get", + }); + + assertEquals(router.calls.handle, 2); + assertEquals( + await router.handle(reqGet), + "Method is not post", + ); + assertEquals(router.calls.handle, 3); + }); + + await t.step("sets the default value for getters", () => { + class Game { + } + + class PlayersEngine { + private game = new Game(); + get Game() { + return this.game; + } + set Game(val: Game) { + this.game = val; + } + } + + const mock = Mock(PlayersEngine).create(); + assertEquals(mock.Game instanceof Game, true); + }); +}); + +//////////////////////////////////////////////////////////////////////////////// +// FILE MARKER - DATA ////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +class TestObjectOne { +} + +class TestObjectTwo { + public name: string; + constructor(name: string) { + this.name = name; + } +} + +class TestObjectTwoMore { + public name: string; + public array: string[]; + constructor(name: string, array: string[]) { + this.name = name; + this.array = array; + } +} + +class TestObjectThree { + public hello(): void { + return; + } + + public test(): string { + this.hello(); + this.hello(); + return "World"; + } +} + +class TestObjectFourBuilder { + #something_one?: string; + #something_two?: string; + + get something_one(): string | undefined { + return this.#something_one; + } + + set something_one(value: string | undefined) { + this.#something_one = value; + } + + get something_two(): string | undefined { + return this.#something_two; + } + + someComplexMethod(): this { + this.#setSomethingOne(); + this.#setSomethingTwo(); + return this; + } + + #setSomethingOne(): void { + this.#something_one = "one"; + } + + #setSomethingTwo(): void { + this.#something_two = "two"; + } +} + +class RandomError extends Error {} +class RandomError2 extends Error { + public name = "RandomError2Name"; + public message = "Some message not by the constructor."; +} + +class MathService { + public add( + num1: number, + num2: number, + useNestedAdd = false, + ): number { + if (useNestedAdd) { + return this.nestedAdd(num1, num2); + } + return num1 + num2; + } + + public nestedAdd(num1: number, num2: number): number { + return num1 + num2; + } +} + +class TestObjectLotsOfDataMembers { + public name: string; + #age = 0; + protected math_service: MathService; + protected protected_property = "I AM PROTECTED PROPERTY."; + constructor(name: string, mathService: MathService) { + this.math_service = mathService; + this.name = name; + } + public sum( + num1: number, + num2: number, + useNestedAdd = false, + ): number { + const sum = this.math_service.add(num1, num2, useNestedAdd); + return sum; + } + protected protectedMethod() { + return "I AM A PROTECTED METHOD."; + } + + public get age() { + return this.#age; + } + + public set age(val: number) { + this.#age = val; + } +} + +class TestRequestHandler { + // deno-lint-ignore require-await + async handle(request: Request): Promise { + const method = request.method.toLowerCase(); + const contentType = request.headers.get("Content-Type"); + + if (method !== "post") { + return "Method is not post"; + } + + if (contentType !== "application/json") { + return "Content-Type is incorrect"; + } + + return "posted"; + } +} diff --git a/tests/deno/unit/mod/stub_test.ts b/tests/deno/unit/mod/stub_test.ts new file mode 100644 index 00000000..bc1ca8fc --- /dev/null +++ b/tests/deno/unit/mod/stub_test.ts @@ -0,0 +1,45 @@ +import { Stub } from "../../../../mod.ts"; +import { assertEquals } from "../../../deps.ts"; + +class Server { + public greeting = "hello"; + + public methodThatLogs() { + return "server is running!"; + } +} + +Deno.test("Stub()", async (t) => { + await t.step("can stub a class property", () => { + const server = new Server(); + assertEquals(server.greeting, "hello"); + Stub(server, "greeting", "you got changed"); + assertEquals(server.greeting, "you got changed"); + Stub(server, "greeting", null); + assertEquals(server.greeting, null); + Stub(server, "greeting", true); + assertEquals(server.greeting, true); + const obj = { test: "hello" }; + Stub(server, "greeting", obj); + assertEquals(server.greeting, obj); + }); + + await t.step("can stub a class method", () => { + const server = new Server(); + assertEquals(server.methodThatLogs(), "server is running!"); + Stub(server, "methodThatLogs"); + assertEquals(server.methodThatLogs(), "stubbed"); + Stub(server, "methodThatLogs", null); + assertEquals(server.methodThatLogs(), null); + Stub(server, "methodThatLogs", true); + assertEquals(server.methodThatLogs(), true); + const obj = { test: "hello" }; + Stub(server, "methodThatLogs", obj); + assertEquals(server.methodThatLogs(), obj); + }); + + await t.step("can return a stubbed function", () => { + const stub = Stub(); + assertEquals(stub(), "stubbed"); + }); +}); diff --git a/tests/deps.ts b/tests/deps.ts index 061b48c1..81d6d28d 100644 --- a/tests/deps.ts +++ b/tests/deps.ts @@ -1 +1,4 @@ -export { assertEquals } from "https://deno.land/std@0.134.0/testing/asserts.ts"; +export { + assertEquals, + assertThrows, +} from "https://deno.land/std@0.135.0/testing/asserts.ts"; diff --git a/tests/esm/jest_assertions.ts b/tests/esm/jest_assertions.ts new file mode 100644 index 00000000..78587464 --- /dev/null +++ b/tests/esm/jest_assertions.ts @@ -0,0 +1,11 @@ +export function assertEquals(actual: unknown, expected: unknown): void { + expect(actual).toStrictEqual(expected); +} + +export function assertThrows( + actual: () => unknown, + expected: new (message?: string) => Error, + message: string, +): void { + expect(actual).toThrow(new expected(message)); +} diff --git a/tests/esm/unit/mod/dummy.test.ts b/tests/esm/unit/mod/dummy.test.ts new file mode 100644 index 00000000..3ded1079 --- /dev/null +++ b/tests/esm/unit/mod/dummy.test.ts @@ -0,0 +1,77 @@ +import { Dummy, Mock } from "../../../../lib/esm/mod.js"; +import { assertEquals } from "../../jest_assertions"; + +describe("Dummy()", () => { + it( + "can fill parameter lists", + (): void => { + const mockServiceOne = Mock(ServiceOne).create(); + const dummy3 = Dummy(ServiceThree); + + const resource = new Resource( + mockServiceOne, + Dummy(ServiceTwo), + dummy3, + ); + + resource.callServiceOne(); + assertEquals(mockServiceOne.calls.methodServiceOne, 1); + }, + ); + + it( + "can be made without specifying constructor args", + (): void => { + const dummy = Dummy(Resource); + assertEquals(Object.getPrototypeOf(dummy), Resource); + }, + ); +}); + +class Resource { + #service_one: ServiceOne; + #service_two: ServiceTwo; + #service_three: ServiceThree; + + constructor( + serviceOne: ServiceOne, + serviceTwo: ServiceTwo, + serviceThree: ServiceThree, + ) { + this.#service_one = serviceOne; + this.#service_two = serviceTwo; + this.#service_three = serviceThree; + } + + public callServiceOne() { + this.#service_one.methodServiceOne(); + } + + public callServiceTwo() { + this.#service_two.methodServiceTwo(); + } + + public callServiceThree() { + this.#service_three.methodServiceThree(); + } +} + +class ServiceOne { + public methodServiceOne() { + return "Method from ServiceOne was called."; + } +} + +class ServiceTwo { + public methodServiceTwo() { + return "Method from ServiceTwo was called."; + } +} + +class ServiceThree { + public methodServiceThree() { + return "Method from ServiceThree was called."; + } +} + +export {}; diff --git a/tests/esm/unit/mod/fake.test.ts b/tests/esm/unit/mod/fake.test.ts new file mode 100644 index 00000000..0cdb8afa --- /dev/null +++ b/tests/esm/unit/mod/fake.test.ts @@ -0,0 +1,373 @@ +import { Fake } from "../../../../lib/esm/mod"; +import { assertEquals, assertThrows } from "../../jest_assertions"; + +describe("Fake()", () => { + it( + "creates fake builder", + (): void => { + const fake = Fake(TestObjectOne); + assertEquals(fake.constructor.name, "FakeBuilder"); + }, + ); + + describe(".create()", () => { + it( + "creates fake object", + (): void => { + const fake = Fake(TestObjectTwo).create(); + assertEquals(fake.name, undefined); + assertEquals(fake.is_fake, true); + }, + ); + }); + + describe(".withConstructorArgs(...)", () => { + it( + "can take 1 arg", + (): void => { + const fake = Fake(TestObjectTwo) + .withConstructorArgs("some name") + .create(); + assertEquals(fake.name, "some name"); + assertEquals(fake.is_fake, true); + }, + ); + + it( + "can take more than 1 arg", + (): void => { + const fake = Fake(TestObjectTwoMore) + .withConstructorArgs("some name", ["hello"]) + .create(); + assertEquals(fake.name, "some name"); + assertEquals(fake.array, ["hello"]); + assertEquals(fake.is_fake, true); + }, + ); + }); + + describe(".method(...)", () => { + it( + "requires .willReturn(...) or .willThrow(...) to be chained", + (): void => { + const fake = Fake(TestObjectThree).create(); + assertEquals(fake.is_fake, true); + + // Original returns "World" + assertEquals(fake.test(), "World"); + + // Don't fully pre-program the method. This should cause an error during assertions. + fake.method("test"); + + try { + fake.test(); + } catch (error) { + assertEquals( + error.message, + `Pre-programmed method "test" does not have a return value.`, + ); + } + }, + ); + + it( + ".willReturn(...) does not call original method and returns given value", + (): void => { + // Assert that a fake can make a class take a shortcut + const fakeServiceDoingShortcut = Fake(Repository).create(); + fakeServiceDoingShortcut.method("findAllUsers").willReturn("shortcut"); + const resourceWithShortcut = new Resource( + fakeServiceDoingShortcut, + ); + resourceWithShortcut.getUsers(); + assertEquals(fakeServiceDoingShortcut.anotha_one_called, false); + assertEquals(fakeServiceDoingShortcut.do_something_called, false); + assertEquals(fakeServiceDoingShortcut.do_something_else_called, false); + + // Assert that the fake service can call original implementations + const fakeServiceNotDoingShortcut = Fake(Repository).create(); + const resourceWithoutShortcut = new Resource( + fakeServiceNotDoingShortcut, + ); + resourceWithoutShortcut.getUsers(); + assertEquals(fakeServiceNotDoingShortcut.anotha_one_called, true); + assertEquals(fakeServiceNotDoingShortcut.do_something_called, true); + assertEquals( + fakeServiceNotDoingShortcut.do_something_else_called, + true, + ); + }, + ); + + it( + ".willReturn(...) can be performed more than once", + (): void => { + // Assert that a fake can make a class take a shortcut + const fakeServiceDoingShortcut = Fake(Repository).create(); + fakeServiceDoingShortcut.method("findUserById").willReturn("shortcut"); + fakeServiceDoingShortcut.method("findUserById").willReturn("shortcut2"); + const resourceWithShortcut = new Resource( + fakeServiceDoingShortcut, + ); + resourceWithShortcut.getUser(1); + assertEquals(fakeServiceDoingShortcut.anotha_one_called, false); + assertEquals(fakeServiceDoingShortcut.do_something_called, false); + assertEquals(fakeServiceDoingShortcut.do_something_else_called, false); + + // Assert that the fake service can call original implementations + const fakeServiceNotDoingShortcut = Fake(Repository).create(); + const resourceWithoutShortcut = new Resource( + fakeServiceNotDoingShortcut, + ); + resourceWithoutShortcut.getUser(1); + assertEquals(fakeServiceNotDoingShortcut.anotha_one_called, true); + assertEquals(fakeServiceNotDoingShortcut.do_something_called, true); + assertEquals( + fakeServiceNotDoingShortcut.do_something_else_called, + true, + ); + }, + ); + + it( + ".willThrow(...) does not call original method and throws error", + (): void => { + // Assert that a fake can make a class take a shortcut + const fakeServiceDoingShortcut = Fake(Repository).create(); + fakeServiceDoingShortcut.method("findAllUsers").willReturn("shortcut"); + const resourceWithShortcut = new Resource( + fakeServiceDoingShortcut, + ); + resourceWithShortcut.getUsers(); + assertEquals(fakeServiceDoingShortcut.anotha_one_called, false); + assertEquals(fakeServiceDoingShortcut.do_something_called, false); + assertEquals(fakeServiceDoingShortcut.do_something_else_called, false); + + // Assert that the fake service can call original implementations + const fakeServiceNotDoingShortcut = Fake(Repository).create(); + const resourceWithoutShortcut = new Resource( + fakeServiceNotDoingShortcut, + ); + resourceWithoutShortcut.getUsers(); + assertEquals(fakeServiceNotDoingShortcut.anotha_one_called, true); + assertEquals(fakeServiceNotDoingShortcut.do_something_called, true); + assertEquals( + fakeServiceNotDoingShortcut.do_something_else_called, + true, + ); + }, + ); + + it( + ".willReturn(fake) returns the fake object (basic)", + (): void => { + const fake = Fake(TestObjectFourBuilder).create(); + assertEquals(fake.is_fake, true); + + fake + .method("someComplexMethod") + .willReturn(fake); + + assertEquals(fake.someComplexMethod(), fake); + }, + ); + + it( + ".willReturn(fake) returns the fake object (extra)", + (): void => { + // Assert that the original implementation sets properties + const fake1 = Fake(TestObjectFourBuilder).create(); + assertEquals(fake1.is_fake, true); + fake1.someComplexMethod(); + assertEquals(fake1.something_one, "one"); + assertEquals(fake1.something_two, "two"); + + // Assert that the fake implementation will not set properties + const fake2 = Fake(TestObjectFourBuilder).create(); + assertEquals(fake2.is_fake, true); + fake2 + .method("someComplexMethod") + .willReturn(fake2); + + assertEquals(fake2.someComplexMethod(), fake2); + assertEquals(fake2.something_one, undefined); + assertEquals(fake2.something_two, undefined); + + // Assert that we can also use setters + const fake3 = Fake(TestObjectFourBuilder).create(); + assertEquals(fake3.is_fake, true); + fake3.someComplexMethod(); + assertEquals(fake3.something_one, "one"); + assertEquals(fake3.something_two, "two"); + fake3.something_one = "you got changed"; + assertEquals(fake3.something_one, "you got changed"); + }, + ); + + it( + ".willThrow() causes throwing RandomError (with constructor)", + (): void => { + const fake = Fake(TestObjectThree).create(); + assertEquals(fake.is_fake, true); + + // Original returns "World" + assertEquals(fake.test(), "World"); + + // Make the original method throw RandomError + fake + .method("test") + .willThrow(new RandomError("Random error message.")); + + assertThrows( + () => fake.test(), + RandomError, + "Random error message.", + ); + }, + ); + + it( + ".willThrow() causes throwing RandomError2 (no constructor)", + (): void => { + const fake = Fake(TestObjectThree).create(); + assertEquals(fake.is_fake, true); + + // Original returns "World" + assertEquals(fake.test(), "World"); + + // Make the original method throw RandomError + fake + .method("test") + .willThrow(new RandomError2()); + + assertThrows( + () => fake.test(), + RandomError2, + "Some message not by the constructor.", + ); + }, + ); + }); +}); + +//////////////////////////////////////////////////////////////////////////////// +// FILE MARKER - DATA ////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +class TestObjectOne { +} + +class TestObjectTwo { + public name: string; + constructor(name: string) { + this.name = name; + } +} + +class TestObjectTwoMore { + public name: string; + public array: string[]; + constructor(name: string, array: string[]) { + this.name = name; + this.array = array; + } +} + +class TestObjectThree { + public hello(): void { + return; + } + + public test(): string { + this.hello(); + this.hello(); + return "World"; + } +} + +class Resource { + #repository: Repository; + + constructor( + serviceOne: Repository, + ) { + this.#repository = serviceOne; + } + + public getUsers() { + this.#repository.findAllUsers(); + } + + public getUser(id: number) { + this.#repository.findUserById(id); + } +} + +class TestObjectFourBuilder { + #something_one?: string; + #something_two?: string; + + get something_one(): string | undefined { + return this.#something_one; + } + + set something_one(value: string | undefined) { + this.#something_one = value; + } + + get something_two(): string | undefined { + return this.#something_two; + } + + someComplexMethod(): this { + this.#setSomethingOne(); + this.#setSomethingTwo(); + return this; + } + + #setSomethingOne(): void { + this.#something_one = "one"; + } + + #setSomethingTwo(): void { + this.#something_two = "two"; + } +} + +class Repository { + public anotha_one_called = false; + public do_something_called = false; + public do_something_else_called = false; + + public findAllUsers(): string { + this.#doSomething(); + this.#doSomethingElse(); + this.#anothaOne(); + return "Finding all users"; + } + + public findUserById(id: number): string { + this.#doSomething(); + this.#doSomethingElse(); + this.#anothaOne(); + return `Finding user by id #${id}`; + } + + #anothaOne() { + this.anotha_one_called = true; + } + + #doSomething() { + this.do_something_called = true; + } + + #doSomethingElse() { + this.do_something_else_called = true; + } +} + +class RandomError extends Error {} +class RandomError2 extends Error { + public name = "RandomError2Name"; + public message = "Some message not by the constructor."; +} diff --git a/tests/esm/unit/mod/mock.test.ts b/tests/esm/unit/mod/mock.test.ts new file mode 100644 index 00000000..ef5ef7db --- /dev/null +++ b/tests/esm/unit/mod/mock.test.ts @@ -0,0 +1,494 @@ +import { Mock } from "../../../../lib/esm/mod"; +import { assertEquals, assertThrows } from "../../jest_assertions"; + +interface IRequestOptions { + headers?: Record; + method?: string; +} + +class Request { + public options: IRequestOptions; + public url: string; + public headers: Map; + public method: string; + + constructor( + url: string, + options: IRequestOptions, + ) { + this.url = url; + this.options = options; + this.headers = this.createHeaders(options.headers ?? {}); + this.method = options.method ? options.method : "get"; + } + + createHeaders(headers: Record) { + const map = new Map(); + for (const header in headers) { + const value = headers[header]; + map.set(header, value); + } + return map; + } +} + +class TestObjectOne { +} + +class TestObjectTwo { + public name: string; + constructor(name: string) { + this.name = name; + } +} + +class TestObjectTwoMore { + public name: string; + public array: string[]; + constructor(name: string, array: string[]) { + this.name = name; + this.array = array; + } +} + +class TestObjectThree { + public hello(): void { + return; + } + + public test(): string { + this.hello(); + this.hello(); + return "World"; + } +} + +class TestObjectFourBuilder { + #something_one?: string; + #something_two?: string; + + get something_one(): string | undefined { + return this.#something_one; + } + + set something_one(value: string | undefined) { + this.#something_one = value; + } + + get something_two(): string | undefined { + return this.#something_two; + } + + someComplexMethod(): this { + this.#setSomethingOne(); + this.#setSomethingTwo(); + return this; + } + + #setSomethingOne(): void { + this.#something_one = "one"; + } + + #setSomethingTwo(): void { + this.#something_two = "two"; + } +} + +class RandomError extends Error {} +class RandomError2 extends Error { + public name = "RandomError2Name"; + public message = "Some message not by the constructor."; +} + +class MathService { + public add( + num1: number, + num2: number, + useNestedAdd = false, + ): number { + if (useNestedAdd) { + return this.nestedAdd(num1, num2); + } + return num1 + num2; + } + + public nestedAdd(num1: number, num2: number): number { + return num1 + num2; + } +} + +class TestObjectLotsOfDataMembers { + public name: string; + #age = 0; + protected math_service: MathService; + protected protected_property = "I AM PROTECTED PROPERTY."; + constructor(name: string, mathService: MathService) { + this.math_service = mathService; + this.name = name; + } + public sum( + num1: number, + num2: number, + useNestedAdd = false, + ): number { + const sum = this.math_service.add(num1, num2, useNestedAdd); + return sum; + } + protected protectedMethod() { + return "I AM A PROTECTED METHOD."; + } + + public get age() { + return this.#age; + } + + public set age(val: number) { + this.#age = val; + } +} + +class TestRequestHandler { + // deno-lint-ignore require-await + async handle(request: Request): Promise { + const method = request.method.toLowerCase(); + const contentType = request.headers.get("content-type"); + + if (method !== "post") { + return "Method is not post"; + } + + if (contentType !== "application/json") { + return "Content-Type is incorrect"; + } + + return "posted"; + } +} + +describe("Mock()", () => { + it( + "creates mock builder", + (): void => { + const mock = Mock(TestObjectOne); + assertEquals(mock.constructor.name, "MockBuilder"); + }, + ); + + describe(".create()", () => { + it( + "creates mock object", + (): void => { + const mock = Mock(TestObjectTwo).create(); + assertEquals(mock.name, undefined); + assertEquals(mock.is_mock, true); + }, + ); + }); + + describe(".withConstructorArgs(...)", () => { + it( + "can take 1 arg", + (): void => { + const mock = Mock(TestObjectTwo) + .withConstructorArgs("some name") + .create(); + assertEquals(mock.name, "some name"); + assertEquals(mock.is_mock, true); + }, + ); + + it( + "can take more than 1 arg", + (): void => { + const mock = Mock(TestObjectTwoMore) + .withConstructorArgs("some name", ["hello"]) + .create(); + assertEquals(mock.name, "some name"); + assertEquals(mock.array, ["hello"]); + assertEquals(mock.is_mock, true); + + const mockMathService = Mock(MathService) + .create(); + const mockTestObject = Mock(TestObjectLotsOfDataMembers) + .withConstructorArgs("has mocked math service", mockMathService) + .create(); + assertEquals(mockMathService.calls.add, 0); + mockTestObject.sum(1, 1); + assertEquals(mockMathService.calls.add, 1); + }, + ); + }); + + describe("method(...)", () => { + it( + "requires .willReturn(...) or .willThrow(...) to be chained", + (): void => { + const mock = Mock(TestObjectThree).create(); + assertEquals(mock.is_mock, true); + + // Original returns "World" + assertEquals(mock.test(), "World"); + + // Don't fully pre-program the method. This should cause an error during assertions. + mock.method("test"); + + try { + mock.test(); + } catch (error) { + assertEquals( + error.message, + `Pre-programmed method "test" does not have a return value.`, + ); + } + assertEquals(mock.calls.test, 2); + assertEquals(mock.calls.hello, 2); + }, + ); + + it( + ".willReturn(...) does not call original method and returns given value", + (): void => { + const mock = Mock(TestObjectThree).create(); + assertEquals(mock.is_mock, true); + + // Original returns "World" + assertEquals(mock.test(), "World"); + + // Change original to return "Hello" + mock.method("test").willReturn("Hello"); + + // Should output "Hello" and make the following calls + assertEquals(mock.test(), "Hello"); + assertEquals(mock.calls.test, 2); + assertEquals(mock.calls.hello, 2); + }, + ); + + it( + ".willReturn(...) can be performed more than once", + (): void => { + const mock = Mock(TestObjectThree).create(); + assertEquals(mock.is_mock, true); + + // Original returns "World" + assertEquals(mock.test(), "World"); + + // Change original to return "Hello" + mock.method("test").willReturn("Hello"); + mock.method("test").willReturn("Hello!"); + + assertEquals(mock.test(), "Hello!"); + assertEquals(mock.calls.test, 2); + assertEquals(mock.calls.hello, 2); + }, + ); + + it( + ".willReturn(...) returns specified value", + (): void => { + const mock = Mock(TestObjectThree).create(); + assertEquals(mock.is_mock, true); + + // Original returns "World" + assertEquals(mock.test(), "World"); + + // Don't fully pre-program the method. This should cause an error during + // assertions. + mock + .method("test") + .willReturn({ + "something": "something", + }); + + assertEquals(mock.test(), { "something": "something" }); + assertEquals(mock.calls.test, 2); + assertEquals(mock.calls.hello, 2); + }, + ); + + it( + ".willReturn(mock) returns the mock object (basic)", + (): void => { + const mock = Mock(TestObjectFourBuilder).create(); + assertEquals(mock.is_mock, true); + + mock + .method("someComplexMethod") + .willReturn(mock); + + assertEquals(mock.someComplexMethod(), mock); + assertEquals(mock.calls.someComplexMethod, 1); + }, + ); + + it( + ".willReturn(mock) returns the mock object (extra)", + (): void => { + // Assert that the original implementation sets properties + const mock1 = Mock(TestObjectFourBuilder).create(); + assertEquals(mock1.is_mock, true); + mock1.someComplexMethod(); + assertEquals(mock1.something_one, "one"); + assertEquals(mock1.something_two, "two"); + assertEquals(mock1.calls.someComplexMethod, 1); + + // Assert that the mock implementation will not set properties + const mock2 = Mock(TestObjectFourBuilder).create(); + assertEquals(mock2.is_mock, true); + mock2 + .method("someComplexMethod") + .willReturn(mock2); + + assertEquals(mock2.someComplexMethod(), mock2); + assertEquals(mock2.something_one, undefined); + assertEquals(mock2.something_two, undefined); + assertEquals(mock2.calls.someComplexMethod, 1); + + // Assert that we can also use setters + const mock3 = Mock(TestObjectFourBuilder).create(); + assertEquals(mock3.is_mock, true); + mock3.someComplexMethod(); + assertEquals(mock3.something_one, "one"); + assertEquals(mock3.something_two, "two"); + mock3.something_one = "you got changed"; + assertEquals(mock3.something_one, "you got changed"); + assertEquals(mock3.calls.someComplexMethod, 1); + }, + ); + + it( + ".willThrow() causes throwing RandomError (with constructor)", + (): void => { + const mock = Mock(TestObjectThree).create(); + assertEquals(mock.is_mock, true); + + // Original returns "World" + assertEquals(mock.test(), "World"); + + // Make the original method throw RandomError + mock + .method("test") + .willThrow(new RandomError("Random error message.")); + + assertThrows( + () => mock.test(), + RandomError, + "Random error message.", + ); + assertEquals(mock.calls.test, 2); + }, + ); + + it( + ".willThrow() causes throwing RandomError2 (no constructor)", + (): void => { + const mock = Mock(TestObjectThree).create(); + assertEquals(mock.is_mock, true); + + // Original returns "World" + assertEquals(mock.test(), "World"); + + // Make the original method throw RandomError + mock + .method("test") + .willThrow(new RandomError2()); + + assertThrows( + () => mock.test(), + RandomError2, + "Some message not by the constructor.", + ); + assertEquals(mock.calls.test, 2); + }, + ); + + it( + ".expects(...).toBeCalled(...)", + (): void => { + const mock = Mock(TestObjectThree).create(); + assertEquals(mock.is_mock, true); + + mock.expects("hello").toBeCalled(2); + mock.test(); + mock.verifyExpectations(); + }, + ); + }); + + // TODO(crookse) Put the below tests into one of the groups above this line + + describe("call count for outside nested function is increased", () => { + const mockMathService = Mock(MathService) + .create(); + const mockTestObject = Mock(TestObjectLotsOfDataMembers) + .withConstructorArgs("has mocked math service", mockMathService) + .create(); + assertEquals(mockMathService.calls.add, 0); + assertEquals(mockMathService.calls.nestedAdd, 0); + mockTestObject.sum(1, 1, true); + assertEquals(mockMathService.calls.add, 1); + assertEquals(mockMathService.calls.nestedAdd, 1); + }); + + describe("can mock getters and setters", () => { + const mock = Mock(TestObjectLotsOfDataMembers) + .create(); + mock.age = 999; + assertEquals(mock.age, 999); + }); + + describe("native request mock", () => { + it("handles async requests", async () => { + const router = Mock(TestRequestHandler).create(); + + const reqPost = new Request("https://google.com", { + method: "post", + headers: { + "content-type": "application/json", + }, + }); + assertEquals(router.calls.handle, 0); + assertEquals(await router.handle(reqPost), "posted"); + assertEquals(router.calls.handle, 1); + + const reqPostNotJson = new Request("https://google.com", { + method: "post", + }); + assertEquals(router.calls.handle, 1); + assertEquals( + await router.handle(reqPostNotJson), + "Content-Type is incorrect", + ); + assertEquals(router.calls.handle, 2); + + const reqGet = new Request("https://google.com", { + method: "get", + }); + + assertEquals(router.calls.handle, 2); + assertEquals( + await router.handle(reqGet), + "Method is not post", + ); + assertEquals(router.calls.handle, 3); + }); + }); + + describe("sets the default value for getters", () => { + class Game { + } + + class PlayersEngine { + private game = new Game(); + get Game() { + return this.game; + } + set Game(val: Game) { + this.game = val; + } + } + + const mock = Mock(PlayersEngine).create(); + assertEquals(mock.Game instanceof Game, true); + }); +}); diff --git a/tests/esm/unit/mod/stub.test.ts b/tests/esm/unit/mod/stub.test.ts new file mode 100644 index 00000000..1f9edd4b --- /dev/null +++ b/tests/esm/unit/mod/stub.test.ts @@ -0,0 +1,45 @@ +import { Stub } from "../../../../lib/esm/mod"; +import { assertEquals } from "../../jest_assertions"; + +class Server { + public greeting = "hello"; + + public methodThatLogs() { + return "server is running!"; + } +} + +describe("Stub()", () => { + it("can stub a class property", () => { + const server = new Server(); + assertEquals(server.greeting, "hello"); + Stub(server, "greeting", "you got changed"); + assertEquals(server.greeting, "you got changed"); + Stub(server, "greeting", null); + assertEquals(server.greeting, null); + Stub(server, "greeting", true); + assertEquals(server.greeting, true); + const obj = { test: "hello" }; + Stub(server, "greeting", obj); + assertEquals(server.greeting, obj); + }); + + it("can stub a class method", () => { + const server = new Server(); + assertEquals(server.methodThatLogs(), "server is running!"); + Stub(server, "methodThatLogs"); + assertEquals(server.methodThatLogs(), "stubbed"); + Stub(server, "methodThatLogs", null); + assertEquals(server.methodThatLogs(), null); + Stub(server, "methodThatLogs", true); + assertEquals(server.methodThatLogs(), true); + const obj = { test: "hello" }; + Stub(server, "methodThatLogs", obj); + assertEquals(server.methodThatLogs(), obj); + }); + + it("can return a stubbed function", () => { + const stub = Stub(); + assertEquals(stub(), "stubbed"); + }); +}); diff --git a/tests/integration/asserts_test.ts b/tests/integration/asserts_test.ts deleted file mode 100644 index 9a9f80b3..00000000 --- a/tests/integration/asserts_test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Rhum } from "../../mod.ts"; - -Rhum.testPlan("asserts_test.ts", () => { - Rhum.testSuite("Deno std asserts", () => { - Rhum.testCase("asserts", () => { - Rhum.asserts.assert(true); - }); - Rhum.testCase("assertEquals", () => { - Rhum.asserts.assertEquals(true, true); - }); - Rhum.testCase("assertNotEquals", () => { - Rhum.asserts.assertNotEquals(true, false); - Rhum.asserts.assertNotEquals("1", 1); - }); - Rhum.testCase("assertStrictEquals", () => { - Rhum.asserts.assertStrictEquals(true, true); - const a = {}; - const b = a; - Rhum.asserts.assertStrictEquals(a, b); - }); - Rhum.testCase("assertStringContains", () => { - Rhum.asserts.assertStringContains("Test hello", "hello"); - }); - Rhum.testCase("assertMatch", () => { - Rhum.asserts.assertMatch("Test hello", /hello/g); - }); - Rhum.testCase("assertArrayContains", () => { - Rhum.asserts.assertArrayContains(["t", "e", "s", "t"], ["t", "e"]); - }); - Rhum.testCase("assertThrows", () => { - Rhum.asserts.assertThrows(() => { - throw new Error("test"); - }); - }); - }); -}); - -Rhum.run(); diff --git a/tests/integration/hooks/after_all_test.ts b/tests/integration/hooks/after_all_test.ts deleted file mode 100644 index dbf97043..00000000 --- a/tests/integration/hooks/after_all_test.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Purpose of this test is to ensure a test using before_all succeeds - */ - -import { Rhum } from "../../../mod.ts"; - -let suite_val = 0; -let case_val = 0; -let async_case_val = 0; -Rhum.testPlan("after_all_test.ts", () => { - Rhum.afterAll(() => { - Rhum.asserts.assertEquals(suite_val, 3); - }); - // Run the first test suite - Rhum.testSuite("test suite 1", () => { - Rhum.afterAll(() => { - Rhum.asserts.assertEquals(case_val, 2); - case_val = 3; - }); - Rhum.testCase("hooks properly set suite_val and case_val", () => { - // Asserting that the suite val should not have been changed by the hook - Rhum.asserts.assertEquals(suite_val, 0); - suite_val = 1; - // Asserting that the case val should not have been changed by the hook - Rhum.asserts.assertEquals(case_val, 0); - case_val = 1; - }); - Rhum.testCase("hooks properly set case_val", () => { - // Asserting that the suite val should not have been changed by the hook - Rhum.asserts.assertEquals(suite_val, 1); - suite_val = 2; - // Asserting that the case val should not have been changed by the hook - Rhum.asserts.assertEquals(case_val, 1); - case_val = 2; - }); - }); - - // Run the second test suite - Rhum.testSuite("test suite 2", () => { - Rhum.afterAll(() => { - Rhum.asserts.assertEquals(case_val, 4); - }); - Rhum.testCase("hooks properly set suite_val and case_val", async () => { - // Asserting that the suite val should not have been changed by the hook - Rhum.asserts.assertEquals(suite_val, 2); - suite_val = 3; - // Asserting that the case val has been changed by the hook - Rhum.asserts.assertEquals(case_val, 3); - case_val = 4; - }); - }); - Rhum.testSuite("test suite 3", () => { - Rhum.afterAll(async () => { - Rhum.asserts.assertEquals(async_case_val, 1); - await new Promise((resolve) => { - setTimeout(() => resolve(async_case_val = 2), 1000); - }); - }); - Rhum.testCase("Async afterAll hook has no effect before case", () => { - Rhum.asserts.assertEquals(async_case_val, 0); - async_case_val = 1; - }); - }); - Rhum.testSuite("test suite 4", () => { - Rhum.testCase("Async afterAll hook has effect after case", () => { - Rhum.asserts.assertEquals(async_case_val, 2); - }); - }); -}); - -Rhum.run(); diff --git a/tests/integration/hooks/after_each_test.ts b/tests/integration/hooks/after_each_test.ts deleted file mode 100644 index 0a808bc5..00000000 --- a/tests/integration/hooks/after_each_test.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Purpose of this test is to ensure a test using before_each succeeds - */ - -import { Rhum } from "../../../mod.ts"; - -let suite_val = "Ed"; -let case_val = 22; - -Rhum.testPlan("after_each_test.ts", () => { - Rhum.afterEach(() => { - suite_val = "Eric"; - }); - // Run the first test suite - Rhum.testSuite("test suite 1", () => { - Rhum.afterEach(() => { - case_val = 2; - }); - Rhum.testCase("hooks properly set suite_val and case_val", () => { - // Asserting that the suite val should not have been changed by the hook (yet) - Rhum.asserts.assertEquals(suite_val, "Ed"); - // Revert it back for the next test that uses it - suite_val = "Ed"; - // Assert the value is kept the same (hook hasn't ran yet) - Rhum.asserts.assertEquals(case_val, 22); - }); - Rhum.testCase("Returns false", () => { - // Assert the value has changes as the hook should have ran - Rhum.asserts.assertEquals(case_val, 2); - }); - }); - - // Run the second test suite - Rhum.testSuite("test suite 2", () => { - Rhum.afterEach(() => { - case_val = 0; - }); - Rhum.testCase("hooks properly set suite_val and case_val", async () => { - // Asserting the hook should have replaced the name - Rhum.asserts.assertEquals(suite_val, "Eric"); - suite_val = "Ed"; - // Should be kept the same as the after each hook should have updated the value - Rhum.asserts.assertEquals(case_val, 2); - }); - }); - Rhum.testSuite("test suite 3", () => { - let async_case_val = 5; - Rhum.afterEach(async () => { - await new Promise((resolve) => { - setTimeout(() => resolve(async_case_val = 15), 1000); - }); - }); - Rhum.testCase("async afterEach hook has no effect before case", () => { - Rhum.asserts.assertEquals(async_case_val, 5); - }); - Rhum.testCase("async afterEach has effect after case", () => { - Rhum.asserts.assertEquals(async_case_val, 15); - }); - }); -}); - -Rhum.run(); diff --git a/tests/integration/hooks/before_all_test.ts b/tests/integration/hooks/before_all_test.ts deleted file mode 100644 index 821528a4..00000000 --- a/tests/integration/hooks/before_all_test.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Purpose of this test is to ensure a test using before_each succeeds - */ - -import { Rhum } from "../../../mod.ts"; - -let suite_val = "Ed"; -let case_val = 22; - -Rhum.testPlan("before_all_test.ts", () => { - Rhum.beforeAll(() => { - suite_val = "Eric"; - }); - - // Run the first test suite - Rhum.testSuite("test suite 1", () => { - Rhum.beforeAll(() => { - case_val = 2; - }); - - Rhum.testCase("hooks properly set suite_val and case_val", () => { - // Asserting the value has changed - Rhum.asserts.assertEquals(suite_val, "Eric"); - // Revert it back for the next test that uses it - suite_val = "Ed"; - // Assert the value has changed from 22 to 2 - Rhum.asserts.assertEquals(case_val, 2); - case_val = 22; - }); - Rhum.testCase("hooks properly set case_val", () => { - // Assert the value has changed from 22 to 2 (after setting it to 22 in the above case) - Rhum.asserts.assertEquals(case_val, 22); - Rhum.asserts.assertEquals(suite_val, "Ed"); - }); - }); - - // Run the second test suite - Rhum.testSuite("test suite 2", () => { - Rhum.beforeAll(() => { - case_val = 0; - }); - Rhum.testCase("hooks properly set suite_val and case_val", async () => { - // Asserting the hook should have replaced the name - Rhum.asserts.assertEquals(suite_val, "Ed"); - Rhum.asserts.assertEquals(case_val, 0); - }); - }); - - Rhum.testSuite("test suite 3", () => { - let async_case_val = 5; - Rhum.beforeAll(async () => { - await new Promise((resolve) => { - setTimeout(() => resolve(async_case_val = 15), 1000); - }); - }); - Rhum.testCase("beforeAll hook can be async", () => { - Rhum.asserts.assertEquals(suite_val, "Ed"); - Rhum.asserts.assertEquals(async_case_val, 15); - }); - }); -}); - -Rhum.run(); diff --git a/tests/integration/hooks/before_each_test.ts b/tests/integration/hooks/before_each_test.ts deleted file mode 100644 index 6cb2d858..00000000 --- a/tests/integration/hooks/before_each_test.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Purpose of this test is to ensure a test using before_each succeeds - */ - -import { Rhum } from "../../../mod.ts"; - -let suite_val = "Ed"; -let case_val = 22; - -Rhum.testPlan("before_each_test.ts", () => { - Rhum.beforeEach(() => { - suite_val = "Eric"; - }); - - // Run the first test suite - Rhum.testSuite("test suite 1", () => { - Rhum.beforeEach(() => { - case_val = 2; - }); - - Rhum.testCase("suite_val is Eric", () => { - Rhum.asserts.assertEquals(suite_val, "Eric"); - suite_val = "Ed"; - }); - Rhum.testCase("suite_val is Ed", () => { - Rhum.asserts.assertEquals(suite_val, "Ed"); - }); - Rhum.testCase("case_val is 2", () => { - Rhum.asserts.assertEquals(case_val, 2); - case_val = 22; - }); - Rhum.testCase("case_val is still 2", () => { - Rhum.asserts.assertEquals(case_val, 2); - }); - }); - - // Run the second test suite - Rhum.testSuite("test suite 2", () => { - Rhum.beforeEach(() => { - case_val = 0; - }); - - Rhum.testCase("suite_val is Eric", async () => { - Rhum.asserts.assertEquals(suite_val, "Eric"); - suite_val = "Ed"; - }); - Rhum.testCase("suite_val is Ed", async () => { - Rhum.asserts.assertEquals(suite_val, "Ed"); - case_val = 1; - }); - Rhum.testCase("case_val is 0", () => { - Rhum.asserts.assertEquals(case_val, 0); - }); - }); - - Rhum.testSuite("test suite 3", () => { - let async_case_val = 5; - - Rhum.beforeEach(async () => { - await new Promise((resolve) => { - setTimeout(() => resolve(async_case_val = 15), 1000); - }); - }); - - Rhum.testCase("beforeEach hook can be async", () => { - Rhum.asserts.assertEquals(async_case_val, 15); - }); - }); -}); - -Rhum.run(); diff --git a/tests/integration/mock_test.ts b/tests/integration/mock_test.ts deleted file mode 100644 index 93e4a3e8..00000000 --- a/tests/integration/mock_test.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { Rhum } from "../../mod.ts"; -import { assertEquals } from "../deps.ts"; - -class MathService { - public add( - num1: number, - num2: number, - useNestedAdd: boolean = false, - ): number { - if (useNestedAdd) { - return this.nestedAdd(num1, num2); - } - return num1 + num2; - } - - public nestedAdd(num1: number, num2: number): number { - return num1 + num2; - } -} - -class TestObject { - public name: string; - #age = 0; - protected math_service: MathService; - protected protected_property = "I AM PROTECTED PROPERTY."; - constructor(name: string, mathService: MathService) { - this.math_service = mathService; - this.name = name; - } - public sum( - num1: number, - num2: number, - useNestedAdd: boolean = false, - ): number { - const sum = this.math_service.add(num1, num2, useNestedAdd); - return sum; - } - protected protectedMethod() { - return "I AM A PROTECTED METHOD."; - } - - public get age() { - return this.#age; - } - - public set age(val: number) { - this.#age = val; - } -} - -class TestRequestHandler { - async handle(request: Request): Promise { - const method = request.method.toLowerCase(); - const contentType = request.headers.get("Content-Type"); - - if (method !== "post") { - return "Method is not post"; - } - - if (contentType !== "application/json") { - return "Content-Type is incorrect"; - } - - return "posted"; - } -} - -Rhum.testPlan("mock_test.ts", () => { - Rhum.testSuite("mock()", () => { - Rhum.testCase("can mock an object", () => { - const mock = Rhum - .mock(TestObject) - .create(); - Rhum.asserts.assertEquals(mock.constructor.name, "TestObject"); - Rhum.asserts.assertEquals(mock.is_mock, true); - }); - - Rhum.testCase("can mock an object with constructor args", () => { - const mock = Rhum - .mock(TestObject) - .withConstructorArgs("my server", new MathService()) - .create(); - Rhum.asserts.assertEquals(mock.constructor.name, "TestObject"); - Rhum.asserts.assertEquals(mock.is_mock, true); - Rhum.asserts.assertEquals(mock.name, "my server"); - }); - - Rhum.testCase("can access protected property", () => { - const mock = Rhum - .mock(TestObject) - .create(); - Rhum.asserts.assertEquals( - (mock as unknown as { [key: string]: string }).protected_property, - "I AM PROTECTED PROPERTY.", - ); - }); - - Rhum.testCase("can access protected method", () => { - const mock = Rhum - .mock(TestObject) - .create(); - Rhum.asserts.assertEquals( - (mock as unknown as { [key: string]: () => string }).protectedMethod(), - "I AM A PROTECTED METHOD.", - ); - }); - - Rhum.testCase("call count for outside function is increased", () => { - const mockMathService = Rhum - .mock(MathService) - .create(); - const mockTestObject = Rhum - .mock(TestObject) - .withConstructorArgs("has mocked math service", mockMathService) - .create(); - Rhum.asserts.assertEquals(mockMathService.calls.add, 0); - mockTestObject.sum(1, 1); - Rhum.asserts.assertEquals(mockMathService.calls.add, 1); - }); - - Rhum.testCase("call count for outside nested function is increased", () => { - const mockMathService = Rhum - .mock(MathService) - .create(); - const mockTestObject = Rhum - .mock(TestObject) - .withConstructorArgs("has mocked math service", mockMathService) - .create(); - Rhum.asserts.assertEquals(mockMathService.calls.add, 0); - Rhum.asserts.assertEquals(mockMathService.calls.nestedAdd, 0); - mockTestObject.sum(1, 1, true); - Rhum.asserts.assertEquals(mockMathService.calls.add, 1); - Rhum.asserts.assertEquals(mockMathService.calls.nestedAdd, 1); - }); - - Rhum.testCase("can mock getters and setters", () => { - const mock = Rhum - .mock(TestObject) - .create(); - mock.age = 999; - Rhum.asserts.assertEquals(mock.age, 999); - }); - - Rhum.testCase("Native Request mock", async () => { - const router = Rhum.mock(TestRequestHandler).create(); - - const reqPost = new Request("https://google.com", { - method: "post", - headers: { - "content-type": "application/json", - }, - }); - Rhum.asserts.assertEquals(router.calls.handle, 0); - Rhum.asserts.assertEquals(await router.handle(reqPost), "posted"); - Rhum.asserts.assertEquals(router.calls.handle, 1); - - const reqPostNotJson = new Request("https://google.com", { - method: "post", - }); - Rhum.asserts.assertEquals(router.calls.handle, 1); - Rhum.asserts.assertEquals( - await router.handle(reqPostNotJson), - "Content-Type is incorrect", - ); - Rhum.asserts.assertEquals(router.calls.handle, 2); - - const reqGet = new Request("https://google.com", { - method: "get", - }); - Rhum.asserts.assertEquals(router.calls.handle, 2); - Rhum.asserts.assertEquals( - await router.handle(reqGet), - "Method is not post", - ); - Rhum.asserts.assertEquals(router.calls.handle, 3); - }); - Rhum.testCase("Sets the default value for getters", () => { - class Game { - } - class PlayersEngine { - private game = new Game(); - get Game() { - return this.game; - } - set Game(val: Game) { - this.game = val; - } - } - const mock = Rhum.mock(PlayersEngine).create(); - Rhum.asserts.assertEquals(mock.Game instanceof Game, true); - }); - }); -}); - -Rhum.run(); diff --git a/tests/integration/stub_test.ts b/tests/integration/stub_test.ts deleted file mode 100644 index d9996d99..00000000 --- a/tests/integration/stub_test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Rhum } from "../../mod.ts"; - -class Server { - public greeting = "hello"; - - public methodThatLogs() { - console.log("Server running."); - } - - public methodThatThrows() { - } -} - -Rhum.testPlan("stub_test.ts", () => { - Rhum.testSuite("stub()", () => { - Rhum.testCase("can stub a property", () => { - const server = Rhum.stubbed(new Server()); - server.stub("greeting", "you got changed"); - Rhum.asserts.assertEquals(server.greeting, "you got changed"); - }); - - Rhum.testCase("can stub a method", () => { - const server = Rhum.stubbed(new Server()); - server.stub("methodThatLogs", () => { - return "don't run the console.log()"; - }); - Rhum.asserts.assertEquals( - server.methodThatLogs(), - "don't run the console.log()", - ); - Rhum.asserts.assertEquals( - server.is_stubbed, - true, - ); - }); - }); -}); - -Rhum.run(); diff --git a/tests/unit/mock_builder_test.ts b/tests/unit/mock_builder_test.ts deleted file mode 100644 index cf1d81d8..00000000 --- a/tests/unit/mock_builder_test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { StdAsserts as asserts } from "../../deps.ts"; -import { MockBuilder } from "../../src/mock_builder.ts"; - -Deno.test({ - name: "Unit | MockBuilder | constructor() | returns a MockBuilder object", - fn(): void { - const mock = new MockBuilder(TestObjectOne); - asserts.assertEquals(mock.constructor.name, "MockBuilder"); - }, -}); - -Deno.test({ - name: "Unit | MockBuilder | create() | creates mock without constructor args", - fn(): void { - const mock = new MockBuilder(TestObjectTwo) - .create(); - asserts.assertEquals(mock.name, undefined); - }, -}); - -Deno.test({ - name: "Unit | MockBuilder | create() | creates mock with constructor args", - fn(): void { - const mock = new MockBuilder(TestObjectTwo) - .withConstructorArgs("some name") - .create(); - asserts.assertEquals(mock.name, "some name"); - }, -}); - -// TODO(crookse) Write this test when we can invoke non-public members. -// Deno.test({ -// name: "Unit | MockBuilder | getAllProperties() | gets all properties", -// fn(): void { -// }, -// }); - -// TODO(crookse) Write this test when we can invoke non-public members. -// Deno.test({ -// name: "Unit | MockBuilder | getAllFunctions() | gets all functions", -// fn(): void { -// }, -// }); - -// FILE MARKER - DATA ////////////////////////////////////////////////////////// - -class TestObjectOne { -} - -class TestObjectTwo { - public name: string; - constructor(name: string) { - this.name = name; - } -} diff --git a/tests/unit/mod_test.ts b/tests/unit/mod_test.ts deleted file mode 100644 index aa38d34f..00000000 --- a/tests/unit/mod_test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { StdAsserts as asserts } from "../../deps.ts"; -import { Rhum } from "../../mod.ts"; - -// TODO(any) When this feature is properly implemented -// Deno.test({ -// name: "Unit | Rhum | SetUp() | Assigns the correct callback", -// async fn(): Promise { -// function cb () { -// return "Hello world!" -// } -// Rhum.SetUp(cb) -// asserts.assertEquals(typeof Rhum.set_up_hook, "function") -// if (Rhum.set_up_hook) { -// asserts.assertEquals(Rhum.set_up_hook(), "Hello world!") -// } else { -// throw new Error("Rhum.set_up_hook should be defined") -// } -// } -// }) - -// TODO(any) When this feature is properly implemented -// Deno.test({ -// name: "Unit | Rhum | Skip() | Correctly skips the test", -// async fn(): Promise { -// -// } -// }) - -// TODO(any) When this feature is properly implemented -// Deno.test({ -// name: "Unit | Rhum | TearDown() | Correctly tears down", -// async fn(): Promise { -// -// } -// }) - -Deno.test({ - name: "Unit | Rhum | testCase() | Runs a test without failing", - async fn(): Promise { - Rhum.testPlan("test plan", () => { - Rhum.testSuite("test suite", () => { - Rhum.testCase("Testing testCase", () => { - console.log("Running!"); - }); - }); - }); - }, -}); - -Deno.test({ - name: "Unit | Rhum | testPlan() | Registers the test plan name", - async fn(): Promise { - let functionWasCalled = false; - function testFn() { - functionWasCalled = true; - return "Hello world!"; - } - const copyTestFn = testFn; - Rhum.testPlan("Testing testCase", copyTestFn); - Rhum.asserts.assert(functionWasCalled); - }, -}); - -Deno.test({ - name: "Unit | Rhum | testSuite() | Registers the test suite name", - async fn(): Promise { - let functionWasCalled = false; - function testFn() { - functionWasCalled = true; - return "Hello world!"; - } - const copyTestFn = testFn; - Rhum.testSuite("Testing testCase", copyTestFn); - asserts.assertEquals(functionWasCalled, true); - }, -}); diff --git a/tests/unit/quick_start.ts b/tests/unit/quick_start.ts deleted file mode 100644 index 561566a8..00000000 --- a/tests/unit/quick_start.ts +++ /dev/null @@ -1,33 +0,0 @@ -// File: app_test.ts - -import { Rhum } from "../../mod.ts"; - -let value = false; - -function run() { - return true; -} - -async function close() { - value = true; - return value; -} - -Rhum.testPlan("app_test.ts", () => { - // Run the first test suite - Rhum.testSuite("run()", () => { - Rhum.testCase("Returns true", () => { - const result = run(); - Rhum.asserts.assertEquals(true, result); - }); - }); - // Run the second test suite - Rhum.testSuite("close()", () => { - Rhum.testCase("Returns true", async () => { - const result = await close(); - Rhum.asserts.assertEquals(true, result); - }); - }); -}); - -Rhum.run(); diff --git a/tests/unit/test_case_test.ts b/tests/unit/test_case_test.ts deleted file mode 100644 index f8eaecde..00000000 --- a/tests/unit/test_case_test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { StdAsserts as asserts } from "../../deps.ts"; -import { TestCase } from "../../src/test_case.ts"; -import { ITestPlan } from "../../src/interfaces.ts"; - -Deno.test({ - name: "Unit | TestCase | run() | Runs the test without failing", - async fn(): Promise { - const plan: ITestPlan = { - suites: { - "suite_1": { - cases: [{ - name: "case_1", - new_name: "123 case_1", - testFn: function () { - asserts.assertEquals(true, true); - }, - }], - }, - }, - }; - const testCase = new TestCase(plan); - await testCase.run(); - }, -}); diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json new file mode 100644 index 00000000..d944d7cf --- /dev/null +++ b/tsconfig.cjs.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "./lib/cjs" + } +} diff --git a/tsconfig.esm.json b/tsconfig.esm.json new file mode 100644 index 00000000..d2489cd0 --- /dev/null +++ b/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "es2015", + "outDir": "./lib/esm" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..e4b8592b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2015", + "rootDir": "./tmp/conversion_workspace", + "declaration": true, + "sourceMap": true, + "esModuleInterop": true, + "moduleResolution": "node" + }, + "include": ["./tmp/conversion_workspace/**/*.ts"], + "exclude": ["./tests/**/*"] +}