diff --git a/example_tests/test.ts b/example_tests/test.ts new file mode 100644 index 00000000..04809b50 --- /dev/null +++ b/example_tests/test.ts @@ -0,0 +1,69 @@ +import { Rhum } from "../mod.ts"; + +Rhum.testPlan(() => { + Rhum.skip("testSuite 1: skipped", () => { + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.skip("testSuite 2: skipped", () => { + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.testSuite("testSuite 3", () => { + Rhum.testCase("testCase", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.testSuite("testSuite 4", () => { + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.testCase("testCase", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.testSuite("testSuite 5", () => { + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.testCase("testCase", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.skip("testSuite 6: skipped", () => { + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.testSuite("testSuite 7", () => { + Rhum.testCase("testCase", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.testCase("fail", () => { + Rhum.asserts.assertEquals(false, true); + }); + }); +}); diff --git a/mod.ts b/mod.ts index a781db66..917b6546 100644 --- a/mod.ts +++ b/mod.ts @@ -1,20 +1,17 @@ import { assertions, asserts } from "./src/rhum_asserts.ts"; -import type { ICase, IPlan, IStats } from "./src/interfaces.ts"; +import type { + ITestCase, + ITestPlan, + ITestPlanResults, +} from "./src/interfaces.ts"; import type { Constructor, Stubbed } from "./src/types.ts"; import { MockBuilder } from "./src/mock_builder.ts"; -import { green, red } from "https://deno.land/std@0.74.0/fmt/colors.ts"; +import { green, red, yellow } from "https://deno.land/std@0.74.0/fmt/colors.ts"; export const version = "v1.1.4"; const encoder = new TextEncoder(); -const stats: IStats = { - passed: 0, - failed: 0, - skipped: 0, - errors: "", -}; - export type { Constructor, Stubbed } from "./src/types.ts"; export { MockBuilder } from "./src/mock_builder.ts"; @@ -50,39 +47,105 @@ export { MockBuilder } from "./src/mock_builder.ts"; */ export class RhumRunner { /** - * The asserts module from https://deno.land/std/testing, but attached to Rhum - * for accessibility. + * The asserts module from Deno Standard Module's testing module, 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 + // Reason for this is ignore: 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 + // deno-lint-ignore ban-types public asserts: { [key in assertions]: Function } = asserts; - protected plan: IPlan = { + /** + * A property to hold the currently running test suite. + */ + protected current_test_suite = ""; + + /** + * A property to hold how many test cases are in a test suite. We use this + * data to keep track of how many test cases have been set up in a test suite. + */ + protected current_test_suite_num_test_cases = 0; + + /** + * A property to hold the test plan which contains the test suites and test + * cases as well as any hooks that need to be executed in each. + */ + protected test_plan: ITestPlan = { suites: {}, + skip: false, + name: "", }; - protected current_test_suite = ""; + /** + * A property to hold the output results of the test plan. + */ + protected test_plan_results: ITestPlanResults = { + passed: 0, + failed: 0, + skipped: 0, + errors: "", + }; // 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. + * Add a test suite to the test plan. + * + * @param suiteName - The name of the test suite. + * @param skip - Are we skipping this test suite? + * @param testCases - The callback function containing the test cases that + * will be executed. + */ + public addTestSuiteToTestPlan( + suiteName: string, + skip: boolean, + testCases: () => void, + ): void { + // Set the name of the currently running test suite so that other methods + // know what test suite is running + this.setCurrentTestSuite(suiteName); + + // If the test suite and its test cases are not yet tracked in the plan + // object, then add the required data so we can track the test suite and its + // test cases. We use this data when we call runTestPlan(). + if (!this.test_plan.suites[suiteName]) { + this.test_plan.suites[suiteName] = { + cases: [], + skip: skip, + }; + } + + // Count how many test cases are in this test suite. We use this data in + // .skip() to make sure we don't accidentally execute test case .skip() + // logic on a test suite. We want to make sure we execute test suite .skip() + // logic on a test suite. + const matches = testCases.toString().match(/Rhum\.(skip|testCase)/g); + if (matches && matches.length) { + this.setCurrentTestSuiteNumTestCases(matches.length); + } + } + + /** + * 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, before each test suite or case. + * need to do what you want, after all test suites or cases. * - * Rhum.testPlan("My Plan", () => { - * Rhum.beforeEach(() => { - * // Runs before each test suite in this test plan + * Rhum.testPlan(() => { + * Rhum.afterAll(() => { + * // Runs once after all test suites in this test plan * }); * Rhum.testSuite("My Suite 1", () => { - * Rhum.beforeEach(() => { - * // Runs before each test case in this test suite + * Rhum.afterAll(() => { + * // Runs once after all test cases in this test suite * }); * Rhum.testCase("My Test Case 1", () => { * ... @@ -90,14 +153,15 @@ export class RhumRunner { * }); * }); */ - public beforeEach(cb: () => void): void { + public afterAll(cb: () => void): void { // Check if the hook is for test cases inside of a suite - if (this.current_test_suite != "") { - // is a before each inside a suite for every test case - this.plan.suites![this.current_test_suite].before_each_case_hook = cb; - } else if (this.current_test_suite == "") { - // before each hooks for the suites - this.plan.before_each_suite_hook = cb; + if (this.getCurrentTestSuite() != "") { + // is a before all inside a suite for every test case + this.test_plan.suites[this.getCurrentTestSuite()].after_all_cases_hook = + cb; + } else if (this.getCurrentTestSuite() == "") { + // before all hooks for the suites + this.test_plan.after_all_suites_hook = cb; } } @@ -110,7 +174,7 @@ export class RhumRunner { * @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.testPlan(() => { * Rhum.afterEach(() => { * // Runs after each test suite in this test plan * }); @@ -126,31 +190,32 @@ export class RhumRunner { */ public afterEach(cb: () => void): void { // Check if the hook is for test cases inside of a suite - if (this.current_test_suite != "") { + if (this.getCurrentTestSuite() != "") { // is a after each inside a suite for every test case - this.plan.suites![this.current_test_suite].after_each_case_hook = cb; - } else if (this.current_test_suite == "") { + this.test_plan.suites![this.getCurrentTestSuite()].after_each_case_hook = + cb; + } else if (this.getCurrentTestSuite() == "") { // after each hooks for the suites - this.plan.after_each_suite_hook = cb; + this.test_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 + * 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 after all test cases. + * execute before 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. + * need to do what you want, before all test suites or cases. * - * Rhum.testPlan("My Plan", () => { - * Rhum.afterAll(() => { - * // Runs once after all test suites in this test plan + * Rhum.testPlan(() => { + * Rhum.beforeAll(() => { + * // Runs once before 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.beforeAll(() => { + * // Runs once before all test cases in this test suite * }); * Rhum.testCase("My Test Case 1", () => { * ... @@ -158,33 +223,34 @@ export class RhumRunner { * }); * }); */ - public afterAll(cb: () => void): void { + public beforeAll(cb: () => void): void { // Check if the hook is for test cases inside of a suite - if (this.current_test_suite != "") { + if (this.getCurrentTestSuite() != "") { // is a before all inside a suite for every test case - this.plan.suites[this.current_test_suite].after_all_cases_hook = cb; - } else if (this.current_test_suite == "") { + this.test_plan.suites[this.getCurrentTestSuite()].before_all_cases_hook = + cb; + } else if (this.getCurrentTestSuite() == "") { // before all hooks for the suites - this.plan.after_all_suites_hook = cb; + this.test_plan.before_all_suites_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. + * 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 all test suites or cases. + * need to do what you want, before each test suite or case. * - * Rhum.testPlan("My Plan", () => { - * Rhum.beforeAll(() => { - * // Runs once before all test suites in this test plan + * Rhum.testPlan(() => { + * Rhum.beforeEach(() => { + * // Runs before each test suite in this test plan * }); * Rhum.testSuite("My Suite 1", () => { - * Rhum.beforeAll(() => { - * // Runs once before all test cases in this test suite + * Rhum.beforeEach(() => { + * // Runs before each test case in this test suite * }); * Rhum.testCase("My Test Case 1", () => { * ... @@ -192,14 +258,15 @@ export class RhumRunner { * }); * }); */ - public beforeAll(cb: () => void): void { + public beforeEach(cb: () => void): void { // Check if the hook is for test cases inside of a suite - if (this.current_test_suite != "") { - // is a before all inside a suite for every test case - this.plan.suites[this.current_test_suite].before_all_cases_hook = cb; - } else if (this.current_test_suite == "") { - // before all hooks for the suites - this.plan.before_all_suites_hook = cb; + if (this.getCurrentTestSuite() != "") { + // is a before each inside a suite for every test case + this.test_plan.suites![this.getCurrentTestSuite()].before_each_case_hook = + cb; + } else if (this.getCurrentTestSuite() == "") { + // before each hooks for the suites + this.test_plan.before_each_suite_hook = cb; } } @@ -207,29 +274,133 @@ export class RhumRunner { // // Do something // } + /** + * Get the current test suite. + * + * @returns The current test suite. + */ + public getCurrentTestSuite(): string { + return this.current_test_suite; + } + + /** + * Get the current number of test cases in the current test suite. + * + * @returns The number of test cases in the current test suite. + */ + public getCurrentTestSuiteNumTestCases(): number { + return this.current_test_suite_num_test_cases; + } + + /** + * Get the current test plan. + * + * @returns The test plan. + */ + public getTestPlan(): ITestPlan { + return this.test_plan; + } + + /** + * 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); + } + + /** + * Run the test plan. + */ + public async runTestPlan(): Promise { + const filters = Deno.args; + const filterTestCase = filters[0]; + const filterTestSuite = filters[1]; + + if (filterTestCase != "undefined") { + return await this.runCaseFiltered(filterTestCase); + } + + if (filterTestSuite != "undefined") { + return await this.runSuiteFiltered(filterTestSuite); + } + + await this.runAllSuitesAndCases(); + } + + /** + * Set the current test suite. + * + * @param val - The name of the test suite. + */ + public setCurrentTestSuite(val: string): void { + this.current_test_suite = val; + } + + /** + * Set the current test suites number of test cases. + * + * @param val - The number of test cases. + */ + public setCurrentTestSuiteNumTestCases(val: number): void { + this.current_test_suite_num_test_cases = val; + } + /** * Allows a test plan, suite, or case to be skipped when the tests run. * - * Rhum.testPlan("My Plan", () => { + * Rhum.testPlan(() => { * Rhum.skip("My Suite 1", () => { // will not run this block - * Rhum.testCase("My Test Case In Suite 1", () => { - * ... - * }); + * 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 - * ... - * }); + * Rhum.testCase("My Test Case In Suite 2", () => {}); + * Rhum.skip("My Other Test Case In Suite 2", () => {}); // will not run this block + * }); + * }); + * + * Rhum.skip(() => { // will not run this block + * Rhum.testSuite("My SUite 1", () => { + * Rhum.testCase("My Test Case In Suite 1", () => {}); * }); * }); */ - 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 + public skip(name: string | (() => void), cb: () => void = () => {}): void { + // If the name is a function, then we know we're skipping an entire test + // plan + if (typeof name == "function") { + // Execute the test plan's test suites, which is the `name` param + this.skipTestPlan(name); + return; + } + + // If there is no current test suite, then we know we know this .skip() call + // is attached to a test suite + if (this.getCurrentTestSuite() == "") { + this.skipTestSuite(name, cb); + // Otherwise, if there is a current test suite, then we know this .skip() + // call is attached to a test case + } else { + // If all of the test cases in a test suite have been set up, then this + // .skip() call is for a test suite. Soooo recursion... + if (this.getCurrentTestSuiteNumTestCases() <= 0) { + this.setCurrentTestSuite(""); + this.skip(name, cb); + return; + } + + this.skipTestCase(name, cb); + } } /** @@ -248,15 +419,18 @@ export class RhumRunner { * } * * // Define the object that will have stubbed members as a stubbed object - * const myStubbedObject = Rhum.stubbed(new MyObject()); + * const myStubbedObject = Rhum.stub(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"); + * Rhum.asserts.assertEquals( + * myStubbedObject.some_property, + * "this property is now stubbed" + * ); */ - public stubbed(obj: T): Stubbed { + public stub(obj: T): Stubbed { (obj as unknown as { [key: string]: boolean }).is_stubbed = true; (obj as unknown as { [key: string]: (property: string, value: unknown) => void; @@ -272,24 +446,6 @@ export class RhumRunner { 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. @@ -299,7 +455,7 @@ export class RhumRunner { * @param name - The name of the test case. * @param testFn - The test to execute. * - * Rhum.testPlan("My Plan", () => { + * Rhum.testPlan(() => { * Rhum.testSuite("My Suite 1", () => { * Rhum.testCase("My Test Case 1", () => { * Rhum.assert.assertEquals(something, true); @@ -311,10 +467,13 @@ export class RhumRunner { * }); */ public testCase(name: string, testFn: () => void): void { - this.plan.suites[this.current_test_suite].cases.push({ + this.addTestCaseToTestSuite( name, - test_fn: testFn, - }); + testFn, + this.test_plan.skip || + this.test_plan.suites[this.getCurrentTestSuite()].skip, + this.getCurrentTestSuite(), + ); } /** @@ -325,11 +484,12 @@ export class RhumRunner { * @param name - The name of the test plan. * @param testSuites - The test suites to execute. * - * Rhum.testPlan("My Plan", () => { + * Rhum.testPlan(() => { * ... * }); */ public async testPlan(testSuites: () => void): Promise { + this.test_plan.name = Deno.args[2]; await testSuites(); await this.runTestPlan(); } @@ -340,9 +500,10 @@ export class RhumRunner { * 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. + * @param testCases - The callback function containing the test cases that + * will be executed. * - * Rhum.testPlan("My Plan", () => { + * Rhum.testPlan(() => { * Rhum.testSuite("My Suite 1", () => { * ... * }); @@ -352,48 +513,43 @@ export class RhumRunner { * }); */ public async testSuite(name: string, testCases: () => void): Promise { - this.current_test_suite = name; - - if (!this.plan.suites[name]) { - this.plan.suites[name] = { - cases: [], - }; - } + this.addTestSuiteToTestPlan( + name, + this.test_plan.skip, + testCases, + ); + // Execute the test cases in this test suite await testCases(); - - this.current_test_suite = ""; } - /** - * Run the test plan. - */ - public async runTestPlan(): Promise { - const filters = Deno.args; - const filterTestCase = filters[0]; - const filterTestSuite = filters[1]; - - if (filterTestCase != "undefined") { - return await this.runCaseFiltered(filterTestCase); - } - - if (filterTestSuite != "undefined") { - return await this.runSuiteFiltered(filterTestSuite); - } - - await this.runAllSuitesAndCases(); - } + // FILE MARKER - METHODS - PROTECTED ///////////////////////////////////////// /** - * Run all test suites and test cases in the test plan. + * Add a test case to a test suite. + * + * @param caseName - The name of the test case. + * @param testFn - The test to execute. + * @param skip - Are we skipping this test? + * @param suiteName - The name of the test suite this test case belongs to. */ - public async runAllSuitesAndCases(): Promise { - await this.runHooksBeforeSuites(); - for (const suiteName in this.plan.suites) { - await this.runSuite(suiteName); + protected addTestCaseToTestSuite( + caseName: string, + testFn: () => void, + skip: boolean, + suiteName: string, + ): void { + this.test_plan.suites[suiteName].cases.push({ + name: caseName, + test_fn: testFn, + skip: skip, + }); + + // Track that this case is ready to be executed + const numTestCases = this.getCurrentTestSuiteNumTestCases(); + if (numTestCases > 0) { + this.setCurrentTestSuiteNumTestCases(numTestCases - 1); } - await this.runHooksAfterSuites(); - this.sendStats(); } /** @@ -403,10 +559,21 @@ export class RhumRunner { * and the test function to execute. * @param suiteName - The name of the test suite this test case belongs to. */ - public async runCase(testCase: ICase, suiteName: string): Promise { + public async runCase(testCase: ITestCase, suiteName: string): Promise { + // SKIP THAT SHIT + if (testCase.skip) { + Deno.stdout.writeSync( + encoder.encode( + " " + yellow("SKIP") + " " + testCase.name + "\n", + ), + ); + this.test_plan_results.skipped++; + return; + } + // Execute .beforeEach() hook before each test case if it exists - if (this.plan.suites[suiteName].before_each_case_hook) { - await this.plan.suites[suiteName].before_each_case_hook!(); + if (this.test_plan.suites[suiteName].before_each_case_hook) { + await this.test_plan.suites[suiteName].before_each_case_hook!(); } // Execute the test @@ -417,20 +584,20 @@ export class RhumRunner { " " + green("PASS") + " " + testCase.name + "\n", ), ); - stats.passed++; + this.test_plan_results.passed++; } catch (error) { Deno.stdout.writeSync( encoder.encode( " " + red("FAIL") + " " + testCase.name + "\n", ), ); - stats.failed++; - stats.errors += ("\n" + error.stack + "\n"); + this.test_plan_results.failed++; + this.test_plan_results.errors += ("\n" + error.stack + "\n"); } // Execute .afterEach() hook after each test case if it exists - if (this.plan.suites[suiteName].after_each_case_hook) { - await this.plan.suites[suiteName].after_each_case_hook!(); + if (this.test_plan.suites[suiteName].after_each_case_hook) { + await this.test_plan.suites[suiteName].after_each_case_hook!(); } } @@ -442,14 +609,90 @@ export class RhumRunner { public async runCaseFiltered(filterVal: string): Promise { await this.runHooksBeforeSuites(); - for (const suiteName in this.plan.suites) { + for (const suiteName in this.test_plan.suites) { await this.runHooksBeforeSuitesAndCases(suiteName); await this.runSuite(suiteName, filterVal); await this.runHooksAfterSuitesAndCases(suiteName); } await this.runHooksAfterSuites(); - this.sendStats(); + this.outputResults(); + } + + /** + * Output the test plan's results. This data is used by the + * /path/to/rhum/src/test_runner.ts. + */ + protected outputResults(): void { + Deno.stdout.writeSync( + encoder.encode(JSON.stringify(this.test_plan_results)), + ); + } + + /** + * Run all test suites and test cases in the test plan. + */ + public async runAllSuitesAndCases(): Promise { + await this.runHooksBeforeSuites(); + for (const suiteName in this.test_plan.suites) { + await this.runSuite(suiteName); + } + await this.runHooksAfterSuites(); + this.outputResults(); + } + + /** + * Run hooks after all test suites. + */ + protected async runHooksAfterSuites(): Promise { + // Execute .afterAll() hook after all test suites + if (this.test_plan.after_all_suites_hook) { + await this.test_plan.after_all_suites_hook(); + } + } + + /** + * Run hooks after all test suites and test cases. + */ + protected async runHooksAfterSuitesAndCases( + suiteName: string, + ): Promise { + // Execute .afterAll() hook after all test cases + if (this.test_plan.suites[suiteName].after_all_cases_hook) { + await this.test_plan.suites[suiteName].after_all_cases_hook!(); + } + + // Execute .afterEach() hook after each test suite if it exists + if (this.test_plan.after_each_suite_hook) { + await this.test_plan.after_each_suite_hook(); + } + } + + /** + * Run hooks before all test suites. + */ + protected async runHooksBeforeSuites(): Promise { + // Execute .beforeAll() hook before all test suites + if (this.test_plan.before_all_suites_hook) { + await this.test_plan.before_all_suites_hook(); + } + } + + /** + * Run hooks before each test suite or before each test case. + */ + protected async runHooksBeforeSuitesAndCases( + suiteName: string, + ): Promise { + // Execute .beforeEach() hook before each test suite if it exists + if (this.test_plan.before_each_suite_hook) { + await this.test_plan.before_each_suite_hook(); + } + + // Execute .beforeAll() hook before all test cases + if (this.test_plan.suites[suiteName].before_all_cases_hook) { + await this.test_plan.suites[suiteName].before_all_cases_hook!(); + } } /** @@ -468,7 +711,7 @@ export class RhumRunner { await this.runHooksBeforeSuitesAndCases(suiteName); - for (const testCase of this.plan.suites[suiteName].cases) { + for (const testCase of this.test_plan.suites[suiteName].cases) { if (filterValTestCase) { if (testCase.name != filterValTestCase) { continue; @@ -489,64 +732,50 @@ export class RhumRunner { public async runSuiteFiltered(filterVal: string): Promise { await this.runHooksBeforeSuites(); - for (const suiteName in this.plan.suites) { + for (const suiteName in this.test_plan.suites) { if (suiteName == filterVal) { await this.runSuite(suiteName); } } await this.runHooksAfterSuites(); - this.sendStats(); - } - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - METHODS - PROTECTED ///////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - protected async runHooksAfterSuites(): Promise { - // Execute .afterAll() hook after all test suites - if (this.plan.after_all_suites_hook) { - await this.plan.after_all_suites_hook(); - } + this.outputResults(); } - protected async runHooksAfterSuitesAndCases( - suiteName: string, - ): Promise { - // Execute .afterAll() hook after all test cases - if (this.plan.suites[suiteName].after_all_cases_hook) { - await this.plan.suites[suiteName].after_all_cases_hook!(); - } - - // Execute .afterEach() hook after each test suite if it exists - if (this.plan.after_each_suite_hook) { - await this.plan.after_each_suite_hook(); - } + /** + * Skip a test case. + * + * @param name - The name of the test case. + * @param testFn - The test to execute. + */ + protected skipTestCase( + name: string, + testFn: () => void, + ): void { + this.addTestCaseToTestSuite(name, testFn, true, this.getCurrentTestSuite()); } - protected async runHooksBeforeSuites(): Promise { - // Execute .beforeAll() hook before all test suites - if (this.plan.before_all_suites_hook) { - await this.plan.before_all_suites_hook(); - } + protected async skipTestPlan(testSuites: () => void): Promise { + this.test_plan.skip = true; + await testSuites(); + await this.runTestPlan(); } - protected async runHooksBeforeSuitesAndCases( - suiteName: string, + /** + * Skip a test suite. + * + * @param name - The name of the test suite. + * @param testCases - The callback function containing the test cases that + * will be executed. + */ + protected async skipTestSuite( + name: string, + testCases: () => void, ): Promise { - // Execute .beforeEach() hook before each test suite if it exists - if (this.plan.before_each_suite_hook) { - await this.plan.before_each_suite_hook(); - } + this.addTestSuiteToTestPlan(name, true, testCases); - // Execute .beforeAll() hook before all test cases - if (this.plan.suites[suiteName].before_all_cases_hook) { - await this.plan.suites[suiteName].before_all_cases_hook!(); - } - } - - protected sendStats(): void { - Deno.stdout.writeSync(encoder.encode(JSON.stringify(stats))); + // Execute the test cases in this test suite + await testCases(); } } diff --git a/src/interfaces.ts b/src/interfaces.ts index acf74b6b..c8b31d8a 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,6 +1,6 @@ /** * cases? - * An array of objects matching the ICase interface. + * An array of objects matching the ITestCase interface. * * after_all_cases_hook? * A callback function to execute after all test cases. @@ -14,12 +14,13 @@ * before_each_case_hook? * A callback function to execute before each test case. */ -export interface ISuite { - cases: ICase[]; +export interface ITestSuite { + cases: ITestCase[]; after_all_cases_hook?: () => void; after_each_case_hook?: () => void; before_all_cases_hook?: () => void; before_each_case_hook?: () => void; + skip: boolean; } /** @@ -30,29 +31,25 @@ export interface ISuite { * The test function. Ultimately, this gets passed as the second * argument of Deno.test(). */ -export interface ICase { +export interface ITestCase { name: string; test_fn: () => void; + skip: boolean; } -export interface IPlan { +export interface ITestPlan { suites: { - [key: string]: ISuite; + [key: string]: ITestSuite; }; after_all_suites_hook?: () => void; after_each_suite_hook?: () => void; before_all_suites_hook?: () => void; before_each_suite_hook?: () => void; -} - -export interface ICaseResult { + skip: boolean; name: string; - pass: boolean; - suite: string; - errors?: string; } -export interface IStats { +export interface ITestPlanResults { passed: number; failed: number; skipped: number; diff --git a/src/test_runner.ts b/src/test_runner.ts index 28352688..22f0a77b 100644 --- a/src/test_runner.ts +++ b/src/test_runner.ts @@ -1,11 +1,6 @@ import { walkSync } from "https://deno.land/std@0.74.0/fs/walk.ts"; -import { - blue, - green, - red, - yellow, -} from "https://deno.land/std@0.74.0/fmt/colors.ts"; -import { IFilters, IStats } from "./interfaces.ts"; +import { colors } from "../deps.ts"; +import { IFilters, ITestPlanResults } from "./interfaces.ts"; const decoder = new TextDecoder(); @@ -19,9 +14,8 @@ export async function runTests( console.log(); logInfo("Starting Rhum"); - // Define the object that will keep a running total of all the stats we care - // about. - const stats: IStats = { + // Define the variable that will keep track of all tests' results + const stats: ITestPlanResults = { passed: 0, failed: 0, skipped: 0, @@ -51,6 +45,7 @@ export async function runTests( Deno.realPathSync("./" + path), filters.test_case as string, filters.test_suite as string, + path, ], stdout: "piped", stderr: "piped", @@ -58,14 +53,26 @@ export async function runTests( const stderr = decoder.decode(await p.stderrOutput()); if (stderr) { - // Output the error, but remove the "Check file:///" line, and replace it - // with the test file being run - const stderrFormatted = stderr - .replace(/.+/, "\r") - .replace(/\n|\r|\r\n|\n\r/g, ""); - console.log(stderrFormatted.replace("", path)); + if ( + stderr.includes(colors.red("error")) || + stderr.includes("Expected") || + stderr.includes("Expected") || + stderr.includes("Unexpected") || + stderr.includes("Uncaught") || + stderr.includes("TypeError") || + stderr.match(/TS[0-9].+\[/) // e.g., TS2345 [ERROR] + ) { + console.log(stderr); + } else { + // Output the error, but remove the "Check file:///" line, and replace it + // with the test file being run + const stderrFormatted = stderr + .replace(/.+/, "\r") + .replace(/\n|\r|\r\n|\n\r/g, ""); + console.log(stderrFormatted.replace("", path)); + } } else { - // Otherwise, just output the test file being run + // Output the file being tested console.log(path); } @@ -79,11 +86,15 @@ export async function runTests( // Store the results from the test file in the stats. We want to keep track // of these stats because we want to display the overall test results when // Rhum is done running through all of the tests. - const testPlanResults = JSON.parse(stdout.match(statsString)![0]); - stats.passed += testPlanResults.passed; - stats.failed += testPlanResults.failed; - stats.skipped += testPlanResults.skipped; - stats.errors += testPlanResults.errors; + try { + const testPlanResults = JSON.parse(stdout.match(statsString)![0]); + stats.passed += testPlanResults.passed; + stats.failed += testPlanResults.failed; + stats.skipped += testPlanResults.skipped; + stats.errors += testPlanResults.errors; + } catch (error) { + logError("An error ocurred while executing the tests.\n\n" + error.stack); + } } // Output the errors @@ -91,9 +102,9 @@ export async function runTests( // Output the overall results console.log( - `\nTest Results: ${green(stats.passed.toString())} passed; ${ - red(stats.failed.toString()) - } failed; ${yellow(stats.skipped.toString())} skipped`, + `\nTest Results: ${colors.green(stats.passed.toString())} passed; ${ + colors.red(stats.failed.toString()) + } failed; ${colors.yellow(stats.skipped.toString())} skipped`, ); } @@ -127,13 +138,22 @@ function getTestFiles(dirOrFile: string): string[] { return testFiles; } +/** + * Log a debug message. + * + * @param message The message to log. + */ +export function logDebug(message: string): void { + console.log(colors.green("DEBUG") + " " + message); +} + /** * Log an error message. * * @param message The message to log. */ export function logError(message: string): void { - console.log(red("ERROR") + " " + message); + console.log(colors.red("ERROR") + " " + message); } /** @@ -142,5 +162,5 @@ export function logError(message: string): void { * @param message The message to log. */ export function logInfo(message: string): void { - console.log(blue("INFO") + " " + message); + console.log(colors.blue("INFO") + " " + message); } diff --git a/tests/integration/output_test.ts b/tests/integration/output_test.ts new file mode 100644 index 00000000..884fbf08 --- /dev/null +++ b/tests/integration/output_test.ts @@ -0,0 +1,80 @@ +import { Rhum } from "../../mod.ts"; +import { colors } from "../../deps.ts"; + +Rhum.testPlan(() => { + Rhum.testSuite("output", () => { + Rhum.testCase("output shows as expected", async () => { + const p = Deno.run({ + cmd: [ + "rhum", "example_tests/test.ts" + ], + stdout: "piped" + }); + const stdout = new TextDecoder().decode(await p.output()); + + // When we assert the output below, we want to strip out anything that is + // filesystem related. For example, we want to strip the following out: + // + // at Object.assertEquals (asserts.ts:196:9) + // at Object.test_fn (file:///private/var/src/drashland/rhum/example_tests/test.ts:43:12) + // at RhumRunner.runCase (file:///private/var/src/drashland/rhum/mod.ts:172:3) + // at RhumRunner.runSuite (file:///private/var/src/drashland/rhum/mod.ts:248:8) + // at async RhumRunner.runAllSuitesAndCases (file:///private/var/src/drashland/rhum/mod.ts:202:51) + // at async RhumRunner.runTestPlan (file:///private/var/src/drashland/rhum/mod.ts:97:3) + // at async RhumRunner.testPlan (file:///private/var/src/drashland/rhum/mod.ts:139:3) + // + // This information would be difficult to manage between a CI environment + // and a local environment. Therefore, we strip it out of the tests + // beacuse we don't really care about that stuff. We care about the + // output: PASS, FAIL, SKIP, and the Actual / Expected data. + Rhum.asserts.assertEquals( + stdout.replace(/\n.+at.*\)/g, ""), + output + ); + }); + }); +}); + +const output = ` +${colors.blue("INFO")} Starting Rhum +${colors.blue("INFO")} Checking test file(s) +${colors.blue("INFO")} Running test(s) + +example_tests/test.ts + testSuite 1: skipped + ${colors.yellow("SKIP")} skipped + ${colors.yellow("SKIP")} skipped + testSuite 2: skipped + ${colors.yellow("SKIP")} skipped + testSuite 3 + ${colors.green("PASS")} testCase + ${colors.yellow("SKIP")} skipped + ${colors.yellow("SKIP")} skipped + testSuite 4 + ${colors.yellow("SKIP")} skipped + ${colors.green("PASS")} testCase + testSuite 5 + ${colors.yellow("SKIP")} skipped + ${colors.yellow("SKIP")} skipped + ${colors.green("PASS")} testCase + testSuite 6: skipped + ${colors.yellow("SKIP")} skipped + testSuite 7 + ${colors.green("PASS")} testCase + ${colors.yellow("SKIP")} skipped + ${colors.red("FAIL")} fail + + +AssertionError: Values are not equal: + + + ${colors.gray(colors.bold("[Diff]"))} ${colors.red(colors.bold("Actual"))} / ${colors.green(colors.bold("Expected"))} + + +${colors.red(colors.bold("- false"))} +${colors.green(colors.bold("+ true"))} + + + +Test Results: ${colors.green("4")} passed; ${colors.red("1")} failed; ${colors.yellow("10")} skipped +`; diff --git a/tests/integration/skip_random_test.ts b/tests/integration/skip_random_test.ts new file mode 100644 index 00000000..6ccde6a6 --- /dev/null +++ b/tests/integration/skip_random_test.ts @@ -0,0 +1,73 @@ +import { Rhum } from "../../mod.ts"; + +// This file should have 5 passed tests +// This file should have 0 failed tests +// This file should have 10 skipped tests + +Rhum.testPlan(() => { + Rhum.skip("testSuite skipped 1", () => { + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.skip("testSuite skipped 2", () => { + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.testSuite("testSuite not skipped 3", () => { + Rhum.testCase("testCase", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.testSuite("testSuite not skipped 4", () => { + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.testCase("testCase", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.testSuite("testSuite not skipped 5", () => { + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.testCase("testCase", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.skip("testSuite skipped 6", () => { + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.testSuite("testSuite not skipped 7", () => { + Rhum.testCase("testCase", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.testCase("testCase", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); +}); diff --git a/tests/integration/skip_test_case_test.ts b/tests/integration/skip_test_case_test.ts new file mode 100644 index 00000000..50a25578 --- /dev/null +++ b/tests/integration/skip_test_case_test.ts @@ -0,0 +1,12 @@ +import { Rhum } from "../../mod.ts"; + +Rhum.testPlan(() => { + Rhum.testSuite("testSuite", () => { + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.testCase("testCase", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); +}); diff --git a/tests/integration/skip_test_plan_test.ts b/tests/integration/skip_test_plan_test.ts new file mode 100644 index 00000000..b498ed49 --- /dev/null +++ b/tests/integration/skip_test_plan_test.ts @@ -0,0 +1,69 @@ +import { Rhum } from "../../mod.ts"; + +Rhum.skip(() => { + Rhum.skip("testSuite skipped 1", () => { + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.skip("testSuite skipped 2", () => { + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.testSuite("testSuite skipped 3", () => { + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.testSuite("testSuite skipped 4", () => { + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.testSuite("testSuite skipped 5", () => { + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.skip("testSuite skipped 6", () => { + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); + + Rhum.testSuite("testSuite skipped 7", () => { + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.skip("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); +}); diff --git a/tests/integration/skip_test_suite_test.ts b/tests/integration/skip_test_suite_test.ts new file mode 100644 index 00000000..e9e7a3dd --- /dev/null +++ b/tests/integration/skip_test_suite_test.ts @@ -0,0 +1,12 @@ +import { Rhum } from "../../mod.ts"; + +Rhum.testPlan(() => { + Rhum.skip("skipped", () => { + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + Rhum.testCase("skipped", () => { + Rhum.asserts.assertEquals(true, true); + }); + }); +}); diff --git a/tests/integration/stub_test.ts b/tests/integration/stub_test.ts index 1989063c..f8e11c08 100644 --- a/tests/integration/stub_test.ts +++ b/tests/integration/stub_test.ts @@ -14,13 +14,13 @@ class Server { Rhum.testPlan(() => { Rhum.testSuite("stub()", () => { Rhum.testCase("can stub a property", () => { - const server = Rhum.stubbed(new Server()); + const server = Rhum.stub(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()); + const server = Rhum.stub(new Server()); server.stub("methodThatLogs", () => { return "don't run the console.log()"; }); diff --git a/tests/unit/mod_test.ts b/tests/unit/mod_test.ts new file mode 100644 index 00000000..be690953 --- /dev/null +++ b/tests/unit/mod_test.ts @@ -0,0 +1,382 @@ +import { Rhum } from "../../mod.ts"; +const decoder = new TextDecoder(); +import { colors } from "../../deps.ts"; + +Rhum.testPlan(() => { + Rhum.testSuite("addTestSuiteToTestPlan", () => { + Rhum.testCase("plan.suites['myRandomTestSuite'] is set", () => { + Rhum.addTestSuiteToTestPlan("myRandomTestSuite", false, () => {}); + Rhum.asserts.assertEquals( + typeof Rhum.getTestPlan().suites["myRandomTestSuite"], + "object", + ); + Rhum.asserts.assertEquals( + Rhum.getTestPlan().suites["myRandomTestSuite"].skip, + false, + ); + }); + }); + + Rhum.testSuite("beforeAll", () => { + Rhum.testCase("before_all_suites_hook is set", () => { + Rhum.setCurrentTestSuite(""); + Rhum.beforeAll(() => {}); + Rhum.asserts.assertEquals( + typeof Rhum.getTestPlan().before_all_suites_hook, + "function", + ); + }); + + Rhum.testCase("before_all_cases_hook is set", () => { + Rhum.setCurrentTestSuite("some test suite"); + Rhum.addTestSuiteToTestPlan( + "some test suite", + false, + () => {}, + ); + Rhum.beforeAll(() => {}); + Rhum.asserts.assertEquals( + typeof Rhum.getTestPlan().suites["some test suite"] + .before_all_cases_hook, + "function", + ); + }); + }); + + Rhum.testSuite("beforeEach", () => { + Rhum.testCase("before_each_suite_hook is set", () => { + Rhum.setCurrentTestSuite(""); + Rhum.beforeEach(() => {}); + Rhum.asserts.assertEquals(Rhum.getCurrentTestSuite(), ""); + const plan = Rhum.getTestPlan(); + Rhum.asserts.assertEquals( + typeof plan.before_each_suite_hook, + "function", + ); + }); + + Rhum.testCase("before_each_case_hook is set", () => { + Rhum.setCurrentTestSuite("some test suite"); + Rhum.addTestSuiteToTestPlan("some test suite", false, () => {}); + Rhum.beforeEach(() => {}); + Rhum.asserts.assertEquals(Rhum.getCurrentTestSuite(), "some test suite"); + Rhum.asserts.assertEquals( + typeof Rhum.getTestPlan().suites["some test suite"] + .before_each_case_hook, + "function", + ); + }); + }); + + Rhum.testSuite("afterEach", () => { + Rhum.testCase("after_each_suite_hook is set", () => { + Rhum.setCurrentTestSuite(""); + Rhum.afterEach(() => {}); + Rhum.asserts.assertEquals(Rhum.getCurrentTestSuite(), ""); + Rhum.asserts.assertEquals( + typeof Rhum.getTestPlan().after_each_suite_hook, + "function", + ); + }); + + Rhum.testCase("after_each_case_hook is set", () => { + Rhum.setCurrentTestSuite("some test test"); + Rhum.addTestSuiteToTestPlan( + "some test suite", + false, + () => {}, + ); + Rhum.afterEach(() => {}); + Rhum.asserts.assertEquals( + Rhum.getCurrentTestSuite(), + "some test suite", + ); + Rhum.asserts.assertEquals( + typeof Rhum.getTestPlan().suites["some test suite"] + .after_each_case_hook, + "function", + ); + }); + }); + + Rhum.testSuite("afterAll", () => { + Rhum.testCase("after_all_suites_hook is set", () => { + Rhum.setCurrentTestSuite(""); + Rhum.afterAll(() => {}); + Rhum.asserts.assertEquals( + typeof Rhum.getTestPlan().after_all_suites_hook, + "function", + ); + }); + + Rhum.testCase("after_all_cases_hook is set", () => { + Rhum.setCurrentTestSuite("some test suite"); + Rhum.afterAll(() => {}); + Rhum.addTestSuiteToTestPlan( + "some test suite", + false, + () => {}, + ); + Rhum.asserts.assertEquals( + typeof Rhum.getTestPlan().suites["some test suite"] + .after_all_cases_hook, + "function", + ); + }); + }); + + Rhum.testSuite("getCurrentTestSuite", () => { + Rhum.testCase("current_test_suite is set to ''", () => { + Rhum.setCurrentTestSuite(""); + Rhum.asserts.assertEquals(Rhum.getCurrentTestSuite(), ""); + }); + Rhum.testCase("current_test_suite is set to 'some test suite'", () => { + Rhum.setCurrentTestSuite("some test suite"); + Rhum.asserts.assertEquals(Rhum.getCurrentTestSuite(), "some test suite"); + }); + }); + + Rhum.testSuite("getCurrentTestSuiteNumTestCases", () => { + Rhum.testCase("current_test_suite_num_test_cases defaults to 0", () => { + Rhum.asserts.assertEquals(Rhum.getCurrentTestSuiteNumTestCases(), 0); + }); + Rhum.testCase("current_test_suite_num_test_cases defaults to 0", () => { + Rhum.setCurrentTestSuiteNumTestCases(1); + Rhum.asserts.assertEquals(Rhum.getCurrentTestSuiteNumTestCases(), 1); + }); + }); + + Rhum.testSuite("getTestPlan", () => { + Rhum.testCase("test_plan is set", () => { + Rhum.asserts.assertEquals( + Object.keys(Rhum.getTestPlan())[0], + "suites", + ); + }); + }); + + Rhum.testSuite("setCurrentTestSuite", () => { + Rhum.testCase("current_test_suite is set to 'yo them shits stank'", () => { + Rhum.setCurrentTestSuite("yo them shits stank"); + Rhum.asserts.assertEquals( + Rhum.getCurrentTestSuite(), + "yo them shits stank", + ); + }); + }); + + Rhum.testSuite("setCurrentTestSuiteNumTestCases", () => { + Rhum.testCase("current_test_suite_num_test_cases is set to 1337", () => { + Rhum.setCurrentTestSuiteNumTestCases(1337); + Rhum.asserts.assertEquals(Rhum.getCurrentTestSuiteNumTestCases(), 1337); + }); + }); + + Rhum.testSuite("skip", () => { + Rhum.testCase("can skip a test plan", async () => { + const p = Deno.run({ + cmd: [ + "rhum", + "tests/integration/skip_test_plan_test.ts", + ], + stdout: "piped", + }); + const stdout = decoder.decode(await p.output()); + Rhum.asserts.assertEquals( + stdout, + data_skipTestPlan, + ); + }); + + Rhum.testCase("can skip a test suite", async () => { + const p = Deno.run({ + cmd: [ + "rhum", + "tests/integration/skip_test_suite_test.ts", + ], + stdout: "piped", + }); + const stdout = decoder.decode(await p.output()); + Rhum.asserts.assertEquals( + stdout, + data_skipTestSuite, + ); + }); + + Rhum.testCase("can skip a test case", async () => { + const p = Deno.run({ + cmd: [ + "rhum", + "tests/integration/skip_test_case_test.ts", + ], + stdout: "piped", + }); + const stdout = decoder.decode(await p.output()); + Rhum.asserts.assertEquals( + stdout, + data_skipTestCase, + ); + }); + }); + + Rhum.testSuite("stub", () => { + Rhum.testCase("can stub an object", () => { + class MyObj { + public hello() { + return false; + } + } + const stubbed = Rhum.stub(new MyObj()); + Rhum.asserts.assertEquals( + stubbed.is_stubbed, + true, + ); + Rhum.asserts.assertEquals( + stubbed.hello(), + false, + ); + stubbed.stub("hello", () => { + return true; + }); + Rhum.asserts.assertEquals( + stubbed.hello(), + true, + ); + }); + }); + + Rhum.testSuite("mock", () => { + Rhum.testCase("can mock an object", () => { + class MyObj {} + const mock = Rhum.mock(MyObj) + .create(); + Rhum.asserts.assertEquals( + mock.is_mock, + true, + ); + }); + + Rhum.testCase("can mock an object with constructor args", () => { + class MyObj { + public arg_1 = "some value"; + constructor(arg1: string) { + this.arg_1 = arg1; + } + } + const mock = Rhum.mock(MyObj) + .withConstructorArgs("some new value") + .create(); + Rhum.asserts.assertEquals( + mock.is_mock, + true, + ); + Rhum.asserts.assertEquals( + mock.arg_1, + "some new value", + ); + }); + }); + + Rhum.testSuite("testCase", () => { + Rhum.testCase("adds a test case to a test suite", () => { + const cases = Rhum.getTestPlan().suites["testCase"].cases.length; + Rhum.asserts.assertEquals( + cases, + 1, + ); + }); + }); + + Rhum.testSuite("testPlan", () => { + Rhum.testCase("sets the test_plan by name", () => { + Rhum.asserts.assertEquals( + Rhum.getTestPlan().name, + // The test plan name is the name of the test file being tested. In this + // case, its this file. + "tests/unit/mod_test.ts", + ); + }); + }); + + Rhum.testSuite("testSuite", () => { + Rhum.testCase("adds a test suite to the test plan", () => { + Rhum.asserts.assert( + Rhum.getTestPlan().suites["testSuite"], + ); + }); + }); +}); + +//////////////////////////////////////////////////////////////////////////////// +// DATA PROVIDERS ////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +const data_skipTestPlan = ` +${colors.blue("INFO")} Starting Rhum +${colors.blue("INFO")} Checking test file(s) +${colors.blue("INFO")} Running test(s) + +tests/integration/skip_test_plan_test.ts + testSuite skipped 1 + ${colors.yellow("SKIP")} skipped + ${colors.yellow("SKIP")} skipped + testSuite skipped 2 + ${colors.yellow("SKIP")} skipped + testSuite skipped 3 + ${colors.yellow("SKIP")} skipped + ${colors.yellow("SKIP")} skipped + ${colors.yellow("SKIP")} skipped + testSuite skipped 4 + ${colors.yellow("SKIP")} skipped + ${colors.yellow("SKIP")} skipped + testSuite skipped 5 + ${colors.yellow("SKIP")} skipped + ${colors.yellow("SKIP")} skipped + ${colors.yellow("SKIP")} skipped + testSuite skipped 6 + ${colors.yellow("SKIP")} skipped + testSuite skipped 7 + ${colors.yellow("SKIP")} skipped + ${colors.yellow("SKIP")} skipped + ${colors.yellow("SKIP")} skipped + + + +Test Results: ${colors.green("0")} passed; ${colors.red("0")} failed; ${ + colors.yellow("15") +} skipped +`; + +const data_skipTestSuite = ` +${colors.blue("INFO")} Starting Rhum +${colors.blue("INFO")} Checking test file(s) +${colors.blue("INFO")} Running test(s) + +tests/integration/skip_test_suite_test.ts + skipped + ${colors.yellow("SKIP")} skipped + ${colors.yellow("SKIP")} skipped + + + +Test Results: ${colors.green("0")} passed; ${colors.red("0")} failed; ${ + colors.yellow("2") +} skipped +`; + +const data_skipTestCase = ` +${colors.blue("INFO")} Starting Rhum +${colors.blue("INFO")} Checking test file(s) +${colors.blue("INFO")} Running test(s) + +tests/integration/skip_test_case_test.ts + testSuite + ${colors.yellow("SKIP")} skipped + ${colors.green("PASS")} testCase + + + +Test Results: ${colors.green("1")} passed; ${colors.red("0")} failed; ${ + colors.yellow("1") +} skipped +`; diff --git a/tests/unit/quick_start.ts b/tests/unit/quick_start_test.ts similarity index 100% rename from tests/unit/quick_start.ts rename to tests/unit/quick_start_test.ts