TypeScript Decorators Based Interface for Mocha
It will read your @suite
, @test
, @timeout
, @slow
, @only
and @skip
decorators
and call the appopriate describe
, it
, timeout
, slow
, it.only
or it.skip
.
- A class decorated with
@suite
is considered suite. - Static
before
and after
methods will be called before and after all tests. - A class instance will be created for each test.
- Instance
before
and after
methods will be called before and after each test. - A method decorated with
@test
is considered a test. - Methods accepting
done
callback or returning a Promise
instance are considered async.
Thanks to
Haringat for the async support in before and after methods.
Test Watcher
There is a watcher script in the package, that runs tsc -w
process and watches its output for successful compilation, upon compilation runs a mocha
process.
You will need a tsconfig.json
, and at least test.ts
mocha entrypoint.
Install mocha
, typescript
and mocha-typescript
as dev dependencies (required):
npm install typescript --save-dev
npm install mocha --save-dev
npm install mocha-typescript --save-dev
Add the following npm script to package.json
:
"scripts": {
"dev-test-watch": "mocha-typescript-watch"
},
And run the typescript mocha watcher from the terminal using npm run dev-test-watch
.
You can use the watcher with plain describe
, it
functions. The decorator based interface is not required for use with the watcher.
Test Interface
The standard mocha interface (arrow functions are discouraged because this is messed up, so we use function):
describe("Hello", function() {
it("world", function() {});
})
Becomes:
@suite class Hello {
@test "world"() { }
}
Scafolding
In the terminal run:
mkdir mocha-ts-use
cd mocha-ts-use
npm init
# then multiple enter hits
npm install typescript --save-dev
npm install mocha --save-dev
npm install mocha-typescript --save-dev
In the package.json
add:
"scripts": {
"test": "tsc -p . && mocha",
"prepublish": "tsc -p ."
},
Create tsconfig.json
file near the package.json
like that:
{
"compilerOptions": {
"module": "commonjs",
"removeComments": false,
"preserveConstEnums": true,
"sourceMap": true,
"experimentalDecorators": true,
"declaration": true
}
}
Add a test.ts
file:
import { suite, test, slow, timeout, skip, only } from "mocha-typescript";
@suite class Hello {
@test "world"() { }
}
Back to the terminal:
npm test
Now you have tests for the code you are about to write.
Mind adding .npmignore
and .gitignore
to keep .ts
files in git
,
and .js
and .d.ts
files in npm
.
More Example Code
Check the playground:
import { suite, test, slow, timeout, skip, only } from "mocha-typescript";
declare var Promise: any;
@suite("mocha typescript")
class Basic {
@test("should pass when asserts are fine")
asserts_pass() {
}
@test("should fail when asserts are broken")
asserts_fail() {
var error = new Error("Assert failed");
(<any>error).expected = "expected";
(<any>error).actual = "to fail";
throw error;
}
@test("should pass async tests")
assert_pass_async(done: Function) {
setTimeout(() => done(), 1);
}
@test("should fail async when given error")
assert_fail_async(done: Function) {
setTimeout(() => done(new Error("Oops...")), 1);
}
@test("should fail async when callback not called")
@timeout(100)
assert_fail_async_no_callback(done: Function) {
}
@test("should pass when promise resolved")
promise_pass_resolved() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(), 1);
});
}
@test("should fail when promise rejected")
promise_fail_rejected() {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Ooopsss...")), 1);
});
}
}
@suite class CuteSyntax {
@test testNamedAsMethod() {
}
@test "can have non verbose syntax for fancy named tests"() {
}
@test "and they can be async too"(done) {
done();
}
}
@suite class LifeCycle {
static tokens = 0;
token: number;
constructor() {
console.log(" - new LifeCycle");
}
before() {
this.token = LifeCycle.tokens++;
console.log(" - Before each test " + this.token);
}
after() {
console.log(" - After each test " + this.token);
}
static before() {
console.log(" - Before the suite: " + ++this.tokens);
}
static after() {
console.log(" - After the suite" + ++this.tokens);
}
@test one() {
console.log(" - Run one: " + this.token);
}
@test two() {
console.log(" - Run two: " + this.token);
}
}
@suite class PassingAsyncLifeCycle {
constructor() {
}
before(done) {
setTimeout(done, 100);
}
after() {
return new Promise((resolve, reject) => resolve());
}
static before() {
return new Promise((resolve, reject) => resolve());
}
static after(done) {
setTimeout(done, 300);
}
@test one() {
}
@test two() {
}
}
@suite class Times {
@test @slow(10) "when fast is normal"(done) {
setTimeout(done, 0);
}
@test @slow(15) "when average is yellow-ish"(done) {
setTimeout(done, 10);
}
@test @slow(15) "when slow is red-ish"(done) {
setTimeout(done, 20);
}
@test @timeout(10) "when faster than timeout passes"(done) {
setTimeout(done, 0);
}
@test @timeout(10) "when slower than timeout fails"(done) {
setTimeout(done, 20);
}
}
@suite class ExecutionControl {
@skip @test "this won't run"() {
}
@test "this however will"() {
}
@test "add @only to run just this test"() {
}
}
class ServerTests {
connect() {
console.log(" connect(" + ServerTests.connection + ")");
}
disconnect() {
console.log(" disconnect(" + ServerTests.connection + ")");
}
static connection: string;
static connectionId: number = 0;
static before() {
ServerTests.connection = "shader connection " + ++ServerTests.connectionId;
console.log(" boot up server.");
}
static after() {
ServerTests.connection = undefined;
console.log(" tear down server.");
}
}
@suite class MobileClient extends ServerTests {
@test "client can connect"() { this.connect(); }
@test "client can disconnect"() { this.disconnect(); }
}
@suite class WebClient extends ServerTests {
@test "web can connect"() { this.connect(); }
@test "web can disconnect"() { this.disconnect(); }
}
declare var describe, it;
describe("outer suite", () => {
@suite class TestClass {
@test method() {
}
}
});