UNPKG

@pmoo/testy

Version:

A minimal testing framework, for educational purposes.

203 lines (163 loc) 4.43 kB
import { TestResult } from './test_result.js'; import { isFunction, isStringWithContent, isUndefined, notNullOrUndefined } from '../utils/index.js'; import { I18nMessage } from '../i18n/i18n_messages.js'; /** * I am an executable test, part of a [test suite]{@link TestSuite} and executed by a [test runner]{@link TestRunner}. * After the execution, I know the [result]{@link TestResult}. Tests must contain at least one assertion. * See {@link Asserter} and {@link Assertion} for more details on how to write those. */ export class Test { #name; #body; #callbacks; #result; #isMarkedAsExclusiveForRun; constructor(name, body, callbacks) { this.#initializeName(name); this.#initializeBody(body); this.#callbacks = callbacks; this.#result = undefined; this.#isMarkedAsExclusiveForRun = false; } // Execution async run(context) { const state = { isRunning: true }; await Promise.race([ this.#timeoutThreshold(context.testExecutionTimeoutMs, state), TestResult .evaluate(this, context) // eslint-disable-next-line no-return-assign .then(_result => state.isRunning = false), ]); } async evaluate(context) { await this.#evaluateHook(context.hooks.before); try { await this.#body.call(); } catch (error) { this.setResult(TestResult.error(error)); } finally { await this.#evaluateHook(context.hooks.after); } } markPending(pendingResult) { this.setResult(pendingResult); this.finishWithPendingStatus(); } skip() { this.markExplicitlySkipped(TestResult.explicitlySkip()); } markSkipped(skippedResult) { this.setResult(skippedResult); this.finishWithSkippedStatus(); } markExplicitlySkipped(explicitlySkippedResult) { this.setResult(explicitlySkippedResult); } only() { this.#isMarkedAsExclusiveForRun = true; } isMarkedAsExclusiveForRun() { return this.#isMarkedAsExclusiveForRun; } setResult(result) { if (this.hasNoResult() || this.isSuccess()) { this.#result = result; } } finishWithSuccess() { this.#callbacks.whenSuccess(this); } finishWithFailure() { this.#callbacks.whenFailed(this); } finishWithError() { this.#callbacks.whenErrored(this); } finishWithPendingStatus() { this.#callbacks.whenPending(this); } finishWithSkippedStatus() { this.#callbacks.whenSkipped(this); } // Testing hasDefinition() { return !isUndefined(this.#body); } hasNoResult() { return isUndefined(this.result()); } hasResult() { return notNullOrUndefined(this.result()); } isSuccess() { return this.result().isSuccess(); } isPending() { return this.result().isPending(); } isExplicitlyMarkedPending() { return this.result().isExplicitlyMarkedPending(); } isSkipped() { return this.result().isSkipped(); } isExplicitlySkipped() { return this.hasResult() && this.result().isExplicitlySkipped(); } isError() { return this.result().isError(); } isFailure() { return this.result().isFailure(); } // Accessing name() { return this.#name; } result() { return this.#result; } // Private #initializeName(name) { this.#ensureNameIsValid(name); this.#name = name; } #initializeBody(body) { this.#ensureBodyIsValid(body); this.#body = body; } #ensureNameIsValid(name) { if (!isStringWithContent(name)) { throw new Error('Test does not have a valid name'); } } #ensureBodyIsValid(body) { if (!isUndefined(body) && !isFunction(body)) { throw new Error('Test does not have a valid body'); } } async #evaluateHook(hook) { if (isUndefined(hook)) { return; } try { await hook.call(); } catch (error) { this.setResult(TestResult.error(error)); } } #timeoutThreshold(timeoutMs, state) { return new Promise(resolve => { setTimeout(() => { if (state.isRunning) { this.setResult(TestResult.error(I18nMessage.of('reached_timeout_error', timeoutMs))); this.finishWithError(); } // This is resolve() and not reject() because we want other tests to be executed. // There was an error in terms of the test itself, but there was not an error in terms of Testy's execution. resolve(); }, timeoutMs); }); } }