diff --git a/assembly/env.ts b/assembly/env.ts index f2a2e2a..656058b 100644 --- a/assembly/env.ts +++ b/assembly/env.ts @@ -12,6 +12,14 @@ export namespace assertResult { export declare function registerTestFunction(index: u32): void; + @external("__unittest_framework_env","registerBeforeEachFunction") + export declare function registerBeforeEachFunction(index: u32): boolean; + + + @external("__unittest_framework_env","registerAfterEachFunction") + export declare function registerAfterEachFunction(index: u32): boolean; + + @external("__unittest_framework_env","collectCheckResult") export declare function collectCheckResult( result: bool, diff --git a/assembly/implement.ts b/assembly/implement.ts index 82ad50a..a7448ac 100644 --- a/assembly/implement.ts +++ b/assembly/implement.ts @@ -15,9 +15,15 @@ export function testImpl(name: string, testFunction: () => void): void { assertResult.removeDescription(); } -export function beforeEachImpl(func: () => void): void {} +export function beforeEachImpl(func: () => void): void { + const result = assertResult.registerBeforeEachFunction(func.index); + assert(result, "register setup function failed"); +} -export function afterEachImpl(func: () => void): void {} +export function afterEachImpl(func: () => void): void { + const result = assertResult.registerAfterEachFunction(func.index); + assert(result, "register teardown function failed"); +} export function mockImpl( originalFunction: T, diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 3d4b610..7986464 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -32,6 +32,7 @@ export default defineConfig({ { text: "Configuration", link: "/api-documents/configuration" }, { text: "Options", link: "/api-documents/options" }, { text: "Matchers", link: "/api-documents/matchers" }, + { text: "Setup Teardown", link: "/api-documents/setup-teardown" }, { text: "Mock Function", link: "/api-documents/mock-function" }, { text: "Report", link: "/api-documents/coverage-report" }, { text: "Return Code", link: "/api-documents/return-code.md" }, diff --git a/docs/api-documents/setup-teardown.md b/docs/api-documents/setup-teardown.md new file mode 100644 index 0000000..833fca1 --- /dev/null +++ b/docs/api-documents/setup-teardown.md @@ -0,0 +1,36 @@ +## Setup And Teardown + +Often while writing tests you have some setup work that needs to happen before tests run, and you have some finishing work that needs to happen after tests run. unittest framework provides helper functions to handle this. + +If you have some work you need to do repeatedly for many tests, you can use `beforeEach` and `afterEach` hooks. + +::: info +`beforeEach` and `afterEach` can only work inside describe which will limit its scope +::: + +### How to Use + +```ts +let setup = 0; +describe("setup", () => { + // effect for the whole describe including sub-describe + beforeEach(() => { + setup = 10; + }); + test("1st", () => { + expect(setup).equal(10); + setup = 100; + }); + test("2nd", () => { + expect(setup).equal(10); + setup = 100; + }); + test("3nd", () => { + expect(setup).equal(10); + }); +}); +``` + +:::info +If multiple `beforeEach` or `afterEach` is registered, they will be call in order. +::: diff --git a/docs/release-note.md b/docs/release-note.md index c1d4330..781de7b 100644 --- a/docs/release-note.md +++ b/docs/release-note.md @@ -6,6 +6,7 @@ - Improved the as-test performances. - Introduce new features `isolated: false` to significantly reduce test execution time in large projects. ([#73](https://github.com/wasm-ecosystem/assemblyscript-unittest-framework/pull/73)) +- Introduce setup and teardown API. ([#77](https://github.com/wasm-ecosystem/assemblyscript-unittest-framework/pull/77)) ## 1.3.1 diff --git a/src/core/execute.ts b/src/core/execute.ts index d2534da..f28e002 100644 --- a/src/core/execute.ts +++ b/src/core/execute.ts @@ -69,30 +69,36 @@ async function nodeExecutor( throw new Error("node executor abort"); }; - try { - executionRecorder.startTestFunction(`${instrumentResult.baseName} - init`); - wasi.start(ins); - } catch (error) { - await exceptionHandler(error); - } - executionRecorder.finishTestFunction(); + await executionRecorder.runTestFunction( + `${instrumentResult.baseName} - init`, + () => { + wasi.start(ins); + }, + exceptionHandler + ); - const execTestFunction = ins.exports["executeTestFunction"]; + const execTestFunction = ins.exports["executeTestFunction"] as (a: number) => void; assert(typeof execTestFunction === "function"); for (const testCase of executionRecorder.testCases) { if (isCrashed) { break; } - const { fullName, functionIndex } = testCase; + const { fullName, functionIndex, setupFunctions, teardownFunctions } = testCase; if (matchedTestNames.length === 0 || matchedTestNames.includes(fullName)) { - executionRecorder.startTestFunction(fullName); - try { - (execTestFunction as (a: number) => void)(functionIndex); - } catch (error) { - await exceptionHandler(error); - } - executionRecorder.finishTestFunction(); + await executionRecorder.runTestFunction( + fullName, + () => { + for (const setupFuncIndex of setupFunctions) { + execTestFunction(setupFuncIndex); + } + execTestFunction(functionIndex); + for (const teardownFuncIndex of teardownFunctions) { + execTestFunction(teardownFuncIndex); + } + }, + exceptionHandler + ); mockStatusRecorder.clear(); } } diff --git a/src/core/executionRecorder.ts b/src/core/executionRecorder.ts index c6412f1..2c61903 100644 --- a/src/core/executionRecorder.ts +++ b/src/core/executionRecorder.ts @@ -46,15 +46,21 @@ export class ExecutionResult implements IExecutionResult { class TestBlock { constructor(public description: string) {} + setupFunctions: number[] = []; + teardownFunctions: number[] = []; } -class TestCase { +export class TestCase { fullName: string; + setupFunctions: number[]; + teardownFunctions: number[]; constructor( testBlockStack: TestBlock[], public functionIndex: number ) { this.fullName = testBlockStack.map((block) => block.description).join(" "); + this.setupFunctions = testBlockStack.flatMap((block) => block.setupFunctions); + this.teardownFunctions = testBlockStack.flatMap((block) => block.teardownFunctions); } } @@ -73,15 +79,39 @@ export class ExecutionRecorder implements UnitTestFramework { _removeDescription(): void { this.testBlockStack.pop(); } + + get lastTestBlock(): TestBlock | undefined { + return this.testBlockStack.at(-1); + } + // return false if error + _registerSetup(functionIndex: number): boolean { + const lastTestBlock = this.lastTestBlock; + if (lastTestBlock === undefined) { + return false; + } else { + lastTestBlock.setupFunctions.push(functionIndex); + return true; + } + } + // return false if error + _registerTeardown(functionIndex: number): boolean { + const lastTestBlock = this.lastTestBlock; + if (lastTestBlock === undefined) { + return false; + } else { + lastTestBlock.teardownFunctions.push(functionIndex); + return true; + } + } _addTestCase(functionIndex: number): void { this.testCases.push(new TestCase(this.testBlockStack, functionIndex)); } - startTestFunction(testCaseFullName: string): void { - this.currentExecutedTestCaseFullName = testCaseFullName; + _startTestFunction(fullName: string): void { + this.currentExecutedTestCaseFullName = fullName; this.logRecorder.reset(); } - finishTestFunction(): void { + _finishTestFunction(): void { const logMessages: string[] | null = this.logRecorder.onFinishTest(); if (logMessages !== null) { this.result.failedLogMessages[this.currentExecutedTestCaseFullName] = ( @@ -89,6 +119,22 @@ export class ExecutionRecorder implements UnitTestFramework { ).concat(logMessages); } } + async runTestFunction( + fullName: string, + runner: () => Promise | void, + exceptionHandler: (error: unknown) => Promise + ) { + this._startTestFunction(fullName); + try { + const r = runner(); + if (r instanceof Promise) { + await r; + } + } catch (error) { + await exceptionHandler(error); + } + this._finishTestFunction(); + } notifyTestCrash(error: ExecutionError): void { this.logRecorder.addLog(`Reason: ${chalk.red(error.message)}`); @@ -128,6 +174,12 @@ export class ExecutionRecorder implements UnitTestFramework { registerTestFunction: (index: number): void => { this._addTestCase(index); }, + registerBeforeEachFunction: (index: number): boolean => { + return this._registerSetup(index); + }, + registerAfterEachFunction: (index: number): boolean => { + return this._registerTeardown(index); + }, collectCheckResult: (result: number, codeInfoIndex: number, actualValue: number, expectValue: number): void => { this.collectCheckResult( result !== 0, diff --git a/tests/e2e/run.js b/tests/e2e/run.js index 015c28f..7b15221 100644 --- a/tests/e2e/run.js +++ b/tests/e2e/run.js @@ -55,13 +55,17 @@ runEndToEndTest("compilationFailed", "", (error, stdout, stderr) => { assert(error.code === 2); }); +runEndToEndTest("isolated-cli", "--isolated false", (error, stdout, stderr) => {}); +runEndToEndTest("isolated-false", "", (error, stdout, stderr) => {}); +runEndToEndTest("isolated-true", "", (error, stdout, stderr) => {}); + runEndToEndTest("printLogInFailedInfo", "", (error, stdout, stderr) => { assert(error.code === 1); }); -runEndToEndTest("isolated-true", "", (error, stdout, stderr) => {}); -runEndToEndTest("isolated-false", "", (error, stdout, stderr) => {}); -runEndToEndTest("isolated-cli", "--isolated false", (error, stdout, stderr) => {}); +runEndToEndTest("setup-teardown", "", (error, stdout, stderr) => { + assert(error.code === 1); +}); runEndToEndTest( "testFiles", diff --git a/tests/e2e/setup-teardown/as-test.config.js b/tests/e2e/setup-teardown/as-test.config.js new file mode 100644 index 0000000..44a0d6f --- /dev/null +++ b/tests/e2e/setup-teardown/as-test.config.js @@ -0,0 +1,14 @@ +import path from "node:path"; + +const __dirname = path.dirname(new URL(import.meta.url).pathname); + +/** + * @type {import("../../../config.d.ts").Config} + */ +export default { + include: [__dirname], + temp: path.join(__dirname, "tmp"), + output: path.join(__dirname, "tmp"), + mode: [], + isolated: true, +}; diff --git a/tests/e2e/setup-teardown/nest.test.ts b/tests/e2e/setup-teardown/nest.test.ts new file mode 100644 index 0000000..8613d50 --- /dev/null +++ b/tests/e2e/setup-teardown/nest.test.ts @@ -0,0 +1,28 @@ +import { test, expect, describe, beforeEach } from "../../../assembly"; + +let setup0 = 0; +let setup1 = 0; +describe("setup", () => { + beforeEach(() => { + setup0 = 10; + setup1 = 20; + }); + describe("nested", () => { + test("1st", () => { + expect(setup0).equal(10); + expect(setup1).equal(20); + setup0 = 100; + setup1 = 200; + }); + test("2nd", () => { + expect(setup0).equal(10); + expect(setup1).equal(20); + setup0 = 100; + setup1 = 200; + }); + }); + test("3nd", () => { + expect(setup0).equal(10); + expect(setup1).equal(20); + }); +}); diff --git a/tests/e2e/setup-teardown/setup.test.ts b/tests/e2e/setup-teardown/setup.test.ts new file mode 100644 index 0000000..8f5e27e --- /dev/null +++ b/tests/e2e/setup-teardown/setup.test.ts @@ -0,0 +1,26 @@ +import { test, expect, describe, beforeEach } from "../../../assembly"; + +let setup0 = 0; +let setup1 = 0; +describe("setup", () => { + beforeEach(() => { + setup0 = 10; + setup1 = 20; + }); + test("1st", () => { + expect(setup0).equal(10); + expect(setup1).equal(20); + setup0 = 100; + setup1 = 200; + }); + test("2nd", () => { + expect(setup0).equal(10); + expect(setup1).equal(20); + setup0 = 100; + setup1 = 200; + }); + test("3nd", () => { + expect(setup0).equal(10); + expect(setup1).equal(20); + }); +}); diff --git a/tests/e2e/setup-teardown/setup_out_of_block.test.ts b/tests/e2e/setup-teardown/setup_out_of_block.test.ts new file mode 100644 index 0000000..2cc5674 --- /dev/null +++ b/tests/e2e/setup-teardown/setup_out_of_block.test.ts @@ -0,0 +1,3 @@ +import { beforeEach } from "../../../assembly"; + +beforeEach(() => {}); diff --git a/tests/e2e/setup-teardown/stdout.txt b/tests/e2e/setup-teardown/stdout.txt new file mode 100644 index 0000000..0c675e0 --- /dev/null +++ b/tests/e2e/setup-teardown/stdout.txt @@ -0,0 +1,16 @@ +code analysis: OK +compile test files: OK +instrument: OK +execute test files: OK + +test case: 18/19 (success/total) + +Error Message: + tests/e2e/setup-teardown/tmp/setup_out_of_block.test - init: + Test Crashed! +Reason: unreachable + at assembly/implement/beforeEachImpl (assembly/implement.ts:20:2) + at assembly/index/beforeEach (assembly/index.ts:47:2) + at start:tests/e2e/setup-teardown/setup_out_of_block.test (tests/e2e/setup-teardown/tmp/setup_out_of_block.test.instrumented.wasm:1:547) + at ~start (tests/e2e/setup-teardown/tmp/setup_out_of_block.test.instrumented.wasm:1:325) + diff --git a/tests/e2e/setup-teardown/teardown.test.ts b/tests/e2e/setup-teardown/teardown.test.ts new file mode 100644 index 0000000..6d47e29 --- /dev/null +++ b/tests/e2e/setup-teardown/teardown.test.ts @@ -0,0 +1,26 @@ +import { test, expect, describe, afterEach } from "../../../assembly"; + +let teardown0 = 0; +let teardown1 = 0; +describe("teardown", () => { + afterEach(() => { + teardown0 = 10; + teardown1 = 20; + }); + test("1st", () => { + expect(teardown0).equal(0); + expect(teardown1).equal(0); + teardown0 = 100; + teardown1 = 200; + }); + test("2nd", () => { + expect(teardown0).equal(10); + expect(teardown1).equal(20); + teardown0 = 100; + teardown1 = 200; + }); + test("3nd", () => { + expect(teardown0).equal(10); + expect(teardown1).equal(20); + }); +}); diff --git a/tests/e2e/setup-teardown/tsconfig.json b/tests/e2e/setup-teardown/tsconfig.json new file mode 100644 index 0000000..798b474 --- /dev/null +++ b/tests/e2e/setup-teardown/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "assemblyscript/std/assembly.json", + "include": ["./**/*.ts"] +} diff --git a/tests/ts/test/core/executionRecorder.test.ts b/tests/ts/test/core/executionRecorder.test.ts index e4f6099..542e2bc 100644 --- a/tests/ts/test/core/executionRecorder.test.ts +++ b/tests/ts/test/core/executionRecorder.test.ts @@ -6,10 +6,9 @@ describe("execution recorder", () => { const recorder = new ExecutionRecorder(); recorder._addDescription("description"); recorder._addTestCase(1); - console.log(recorder.testCases); expect(recorder.testCases).toMatchObject([{ functionIndex: 1, fullName: "description" }]); - recorder.startTestFunction("description"); + recorder._startTestFunction("description"); recorder.collectCheckResult(false, 0, "", ""); expect(recorder.result.failedInfo).toHaveProperty("description"); }); @@ -20,7 +19,7 @@ describe("execution recorder", () => { recorder._addTestCase(1); expect(recorder.testCases).toMatchObject([{ functionIndex: 1, fullName: "description1 description2" }]); - recorder.startTestFunction("description1 description2"); + recorder._startTestFunction("description1 description2"); recorder.collectCheckResult(false, 0, "", ""); expect(recorder.result.failedInfo).toHaveProperty("description1 description2"); }); @@ -33,12 +32,45 @@ describe("execution recorder", () => { recorder._addTestCase(1); expect(recorder.testCases).toMatchObject([{ functionIndex: 1, fullName: "description1" }]); - recorder.startTestFunction("description1"); + recorder._startTestFunction("description1"); recorder.collectCheckResult(false, 0, "", ""); expect(recorder.result.failedInfo).toHaveProperty("description1"); }); }); + describe("setup and teardown", () => { + test("base", () => { + const recorder = new ExecutionRecorder(); + recorder._addDescription("description"); + recorder._registerSetup(10); + recorder._registerSetup(11); + recorder._registerTeardown(20); + recorder._registerTeardown(21); + recorder._addTestCase(1); + expect(recorder.testCases).toMatchObject([{ setupFunctions: [10, 11], teardownFunctions: [20, 21] }]); + }); + test("pop", () => { + const recorder = new ExecutionRecorder(); + recorder._addDescription("description 1"); + recorder._registerSetup(10); + recorder._registerTeardown(20); + + recorder._addDescription("description 2"); + recorder._registerSetup(11); + recorder._registerTeardown(21); + recorder._removeDescription(); + + recorder._addTestCase(1); + expect(recorder.testCases).toMatchObject([{ setupFunctions: [10], teardownFunctions: [20] }]); + }); + + test("out of block", () => { + const recorder = new ExecutionRecorder(); + expect(recorder._registerSetup(10)).toBe(false); + expect(recorder._registerTeardown(20)).toBe(false); + }); + }); + describe("collectCheckResult", () => { test("collect false result", () => { const recorder = new ExecutionRecorder();