mirror of
https://github.com/denoland/deno.git
synced 2024-12-24 16:19:12 -05:00
feat(cli/js/testing): Add TestDefinition::skip (#4351)
This commit is contained in:
parent
a159165fe5
commit
64a35acd64
6 changed files with 104 additions and 105 deletions
13
cli/js/lib.deno.ns.d.ts
vendored
13
cli/js/lib.deno.ns.d.ts
vendored
|
@ -17,6 +17,7 @@ declare namespace Deno {
|
||||||
export interface TestDefinition {
|
export interface TestDefinition {
|
||||||
fn: TestFunction;
|
fn: TestFunction;
|
||||||
name: string;
|
name: string;
|
||||||
|
skip?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Register a test which will be run when `deno test` is used on the command
|
/** Register a test which will be run when `deno test` is used on the command
|
||||||
|
@ -32,12 +33,16 @@ declare namespace Deno {
|
||||||
* when `Deno.runTests` is used */
|
* when `Deno.runTests` is used */
|
||||||
export function test(name: string, fn: TestFunction): void;
|
export function test(name: string, fn: TestFunction): void;
|
||||||
|
|
||||||
|
enum TestStatus {
|
||||||
|
Passed = "passed",
|
||||||
|
Failed = "failed",
|
||||||
|
Skipped = "skipped"
|
||||||
|
}
|
||||||
|
|
||||||
interface TestResult {
|
interface TestResult {
|
||||||
passed: boolean;
|
|
||||||
name: string;
|
name: string;
|
||||||
skipped: boolean;
|
status: TestStatus;
|
||||||
hasRun: boolean;
|
duration?: number;
|
||||||
duration: number;
|
|
||||||
error?: Error;
|
error?: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
import { red, green, bgRed, gray, italic } from "./colors.ts";
|
import { bgRed, gray, green, italic, red, yellow } from "./colors.ts";
|
||||||
import { exit } from "./ops/os.ts";
|
import { exit } from "./ops/os.ts";
|
||||||
import { Console } from "./web/console.ts";
|
import { Console } from "./web/console.ts";
|
||||||
|
|
||||||
const RED_FAILED = red("FAILED");
|
const RED_FAILED = red("FAILED");
|
||||||
const GREEN_OK = green("OK");
|
const GREEN_OK = green("OK");
|
||||||
|
const YELLOW_SKIPPED = yellow("SKIPPED");
|
||||||
const RED_BG_FAIL = bgRed(" FAIL ");
|
const RED_BG_FAIL = bgRed(" FAIL ");
|
||||||
const disabledConsole = new Console((_x: string, _isErr?: boolean): void => {});
|
const disabledConsole = new Console((_x: string, _isErr?: boolean): void => {});
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ export type TestFunction = () => void | Promise<void>;
|
||||||
export interface TestDefinition {
|
export interface TestDefinition {
|
||||||
fn: TestFunction;
|
fn: TestFunction;
|
||||||
name: string;
|
name: string;
|
||||||
|
skip?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEST_REGISTRY: TestDefinition[] = [];
|
const TEST_REGISTRY: TestDefinition[] = [];
|
||||||
|
@ -31,34 +33,32 @@ export function test(
|
||||||
t: string | TestDefinition | TestFunction,
|
t: string | TestDefinition | TestFunction,
|
||||||
fn?: TestFunction
|
fn?: TestFunction
|
||||||
): void {
|
): void {
|
||||||
let name: string;
|
let testDef: TestDefinition;
|
||||||
|
|
||||||
if (typeof t === "string") {
|
if (typeof t === "string") {
|
||||||
if (!fn) {
|
if (!fn || typeof fn != "function") {
|
||||||
throw new Error("Missing test function");
|
throw new TypeError("Missing test function");
|
||||||
}
|
}
|
||||||
name = t;
|
if (!t) {
|
||||||
if (!name) {
|
throw new TypeError("The test name can't be empty");
|
||||||
throw new Error("The name of test case can't be empty");
|
|
||||||
}
|
}
|
||||||
|
testDef = { fn: fn as TestFunction, name: t, skip: false };
|
||||||
} else if (typeof t === "function") {
|
} else if (typeof t === "function") {
|
||||||
fn = t;
|
if (!t.name) {
|
||||||
name = t.name;
|
throw new TypeError("The test function can't be anonymous");
|
||||||
if (!name) {
|
|
||||||
throw new Error("Test function can't be anonymous");
|
|
||||||
}
|
}
|
||||||
|
testDef = { fn: t, name: t.name, skip: false };
|
||||||
} else {
|
} else {
|
||||||
fn = t.fn;
|
if (!t.fn) {
|
||||||
if (!fn) {
|
throw new TypeError("Missing test function");
|
||||||
throw new Error("Missing test function");
|
|
||||||
}
|
}
|
||||||
name = t.name;
|
if (!t.name) {
|
||||||
if (!name) {
|
throw new TypeError("The test name can't be empty");
|
||||||
throw new Error("The name of test case can't be empty");
|
|
||||||
}
|
}
|
||||||
|
testDef = { fn: t.fn, name: t.name, skip: Boolean(t.skip) };
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_REGISTRY.push({ fn, name });
|
TEST_REGISTRY.push(testDef);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TestStats {
|
interface TestStats {
|
||||||
|
@ -78,18 +78,17 @@ export interface RunTestsOptions {
|
||||||
reporter?: TestReporter;
|
reporter?: TestReporter;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TestResult {
|
enum TestStatus {
|
||||||
passed: boolean;
|
Passed = "passed",
|
||||||
name: string;
|
Failed = "failed",
|
||||||
skipped: boolean;
|
Skipped = "skipped"
|
||||||
hasRun: boolean;
|
|
||||||
duration: number;
|
|
||||||
error?: Error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TestCase {
|
interface TestResult {
|
||||||
result: TestResult;
|
name: string;
|
||||||
fn: TestFunction;
|
status: TestStatus;
|
||||||
|
duration?: number;
|
||||||
|
error?: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TestEvent {
|
export enum TestEvent {
|
||||||
|
@ -115,24 +114,10 @@ interface TestEventEnd {
|
||||||
results: TestResult[];
|
results: TestResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function testDefinitionToTestCase(def: TestDefinition): TestCase {
|
|
||||||
return {
|
|
||||||
fn: def.fn,
|
|
||||||
result: {
|
|
||||||
name: def.name,
|
|
||||||
passed: false,
|
|
||||||
skipped: false,
|
|
||||||
hasRun: false,
|
|
||||||
duration: 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: already implements AsyncGenerator<RunTestsMessage>, but add as "implements to class"
|
// TODO: already implements AsyncGenerator<RunTestsMessage>, but add as "implements to class"
|
||||||
// TODO: implements PromiseLike<TestsResult>
|
// TODO: implements PromiseLike<TestsResult>
|
||||||
class TestApi {
|
class TestApi {
|
||||||
readonly testsToRun: TestDefinition[];
|
readonly testsToRun: TestDefinition[];
|
||||||
readonly testCases: TestCase[];
|
|
||||||
readonly stats: TestStats = {
|
readonly stats: TestStats = {
|
||||||
filtered: 0,
|
filtered: 0,
|
||||||
ignored: 0,
|
ignored: 0,
|
||||||
|
@ -148,7 +133,6 @@ class TestApi {
|
||||||
) {
|
) {
|
||||||
this.testsToRun = tests.filter(filterFn);
|
this.testsToRun = tests.filter(filterFn);
|
||||||
this.stats.filtered = tests.length - this.testsToRun.length;
|
this.stats.filtered = tests.length - this.testsToRun.length;
|
||||||
this.testCases = this.testsToRun.map(testDefinitionToTestCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async *[Symbol.asyncIterator](): AsyncIterator<
|
async *[Symbol.asyncIterator](): AsyncIterator<
|
||||||
|
@ -159,32 +143,35 @@ class TestApi {
|
||||||
tests: this.testsToRun.length
|
tests: this.testsToRun.length
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const results: TestResult[] = [];
|
||||||
const suiteStart = +new Date();
|
const suiteStart = +new Date();
|
||||||
for (const testCase of this.testCases) {
|
for (const { name, fn, skip } of this.testsToRun) {
|
||||||
const { fn, result } = testCase;
|
const result: Partial<TestResult> = { name };
|
||||||
let shouldBreak = false;
|
if (skip) {
|
||||||
try {
|
result.status = TestStatus.Skipped;
|
||||||
|
this.stats.ignored++;
|
||||||
|
} else {
|
||||||
const start = +new Date();
|
const start = +new Date();
|
||||||
|
try {
|
||||||
await fn();
|
await fn();
|
||||||
result.duration = +new Date() - start;
|
result.duration = +new Date() - start;
|
||||||
result.passed = true;
|
result.status = TestStatus.Passed;
|
||||||
this.stats.passed++;
|
this.stats.passed++;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
result.passed = false;
|
result.duration = +new Date() - start;
|
||||||
|
result.status = TestStatus.Failed;
|
||||||
result.error = err;
|
result.error = err;
|
||||||
this.stats.failed++;
|
this.stats.failed++;
|
||||||
shouldBreak = this.failFast;
|
|
||||||
} finally {
|
|
||||||
result.hasRun = true;
|
|
||||||
yield { kind: TestEvent.Result, result };
|
|
||||||
if (shouldBreak) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
yield { kind: TestEvent.Result, result: result as TestResult };
|
||||||
|
results.push(result as TestResult);
|
||||||
|
if (this.failFast && result.error != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const duration = +new Date() - suiteStart;
|
const duration = +new Date() - suiteStart;
|
||||||
const results = this.testCases.map(r => r.result);
|
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
kind: TestEvent.End,
|
kind: TestEvent.End,
|
||||||
|
@ -241,13 +228,21 @@ export class ConsoleTestReporter implements TestReporter {
|
||||||
async result(event: TestEventResult): Promise<void> {
|
async result(event: TestEventResult): Promise<void> {
|
||||||
const { result } = event;
|
const { result } = event;
|
||||||
|
|
||||||
if (result.passed) {
|
switch (result.status) {
|
||||||
|
case TestStatus.Passed:
|
||||||
this.console.log(
|
this.console.log(
|
||||||
`${GREEN_OK} ${result.name} ${formatDuration(result.duration)}`
|
`${GREEN_OK} ${result.name} ${formatDuration(result.duration!)}`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case TestStatus.Failed:
|
||||||
|
this.console.log(
|
||||||
|
`${RED_FAILED} ${result.name} ${formatDuration(result.duration!)}`
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
this.console.log(`${RED_FAILED} ${result.name}`);
|
|
||||||
this.console.log(result.error!);
|
this.console.log(result.error!);
|
||||||
|
break;
|
||||||
|
case TestStatus.Skipped:
|
||||||
|
this.console.log(`${YELLOW_SKIPPED} ${result.name}`);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,8 @@ unitTest(function nameOfTestCaseCantBeEmpty(): void {
|
||||||
() => {
|
() => {
|
||||||
Deno.test("", () => {});
|
Deno.test("", () => {});
|
||||||
},
|
},
|
||||||
Error,
|
TypeError,
|
||||||
"The name of test case can't be empty"
|
"The test name can't be empty"
|
||||||
);
|
);
|
||||||
assertThrows(
|
assertThrows(
|
||||||
() => {
|
() => {
|
||||||
|
@ -21,8 +21,8 @@ unitTest(function nameOfTestCaseCantBeEmpty(): void {
|
||||||
fn: () => {}
|
fn: () => {}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
Error,
|
TypeError,
|
||||||
"The name of test case can't be empty"
|
"The test name can't be empty"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ unitTest(function testFnCantBeAnonymous(): void {
|
||||||
() => {
|
() => {
|
||||||
Deno.test(function() {});
|
Deno.test(function() {});
|
||||||
},
|
},
|
||||||
Error,
|
TypeError,
|
||||||
"Test function can't be anonymous"
|
"The test function can't be anonymous"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,4 +13,3 @@ OK bundleApiJsModules [WILDCARD]
|
||||||
OK diagnosticsTest [WILDCARD]
|
OK diagnosticsTest [WILDCARD]
|
||||||
|
|
||||||
test result: OK 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out [WILDCARD]
|
test result: OK 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out [WILDCARD]
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue