Skip to content

Commit

Permalink
feat(testing): Add behavior-driven development
Browse files Browse the repository at this point in the history
  • Loading branch information
KyleJune committed Mar 27, 2022
1 parent 7246c77 commit 2d5e8d8
Show file tree
Hide file tree
Showing 9 changed files with 2,854 additions and 0 deletions.
296 changes: 296 additions & 0 deletions testing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,3 +277,299 @@ with the `BenchmarkRunOptions.silent` flag.
Clears all registered benchmarks, so calling `runBenchmarks()` after it wont run
them. Filtering can be applied by setting `BenchmarkRunOptions.only` and/or
`BenchmarkRunOptions.skip` to regular expressions matching benchmark names.

## Behavior-driven development

With the `bdd.ts` module you can write your tests in a familiar format for
grouping tests and adding setup/teardown hooks used by other JavaScript testing
frameworks like Jasmine, Jest, and Mocha.

The `describe` function creates a block that groups together several related
tests. The `it` function registers an individual test case. The `describe` and
`it` functions have similar call signatures to `Deno.test`, making it easy to
migrate from using `Deno.test`.

### Hooks

There are 4 types of hooks available for test suites. A test suite can have
multiples of each type of hook, they will be called in the order that they are
registered. The `afterEach` and `afterAll` hooks will be called whether or not
the test case passes. The all hooks will be called once for the whole group
while the each hooks will be called for each individual test case.

- `beforeAll`: Runs before all of the tests in the test suite.
- `afterAll`: Runs after all of the tests in the test suite finish.
- `beforeEach`: Runs before each of the individual test cases in the test suite.
- `afterEach`: Runs after each of the individual test cases in the test suite.

If a hook is registered at the top level, a global test suite will be registered
and all tests will belong to it. Hooks registered at the top level must be
registered before any individual test cases or test suites.

### Focusing tests

If you would like to only run specific individual test cases, you can do so by
calling `it.only` instead of `it`. If you would like to only run specific test
suites, you can do so by calling `describe.only` instead of `describe`.

There is one limitation to this when the individual test cases or test suites
belong to another test suite, they will be the only ones to run within the top
level test suite.

### Ignoring tests

If you would like to not run specific individual test cases, you can do so by
calling `it.ignore` instead of `it`. If you would like to only run specific test
suites, you can do so by calling `describe.ignore` instead of `describe`.

### Sanitization options

Like `Deno.TestDefinition`, the `DescribeDefinition` and `ItDefinition` have
sanitization options. They work in the same way.

- sanitizeExit: Ensure the test case does not prematurely cause the process to
exit, for example via a call to Deno.exit. Defaults to true.
- sanitizeOps: Check that the number of async completed ops after the test is
the same as number of dispatched ops. Defaults to true.
- sanitizeResources: Ensure the test case does not "leak" resources - ie. the
resource table after the test has exactly the same contents as before the
test. Defaults to true.

### Permissions option

Like `Deno.TestDefinition`, the `DescribeDefintion` and `ItDefinition` have a
permissions option. They specify the permissions that should be used to run an
individual test case or test suite. Set this to "inherit" to keep the calling
thread's permissions. Set this to "none" to revoke all permissions.

Defaults to "inherit".

There is currently one limitation to this, you cannot use the permissions option
on an individual test case or test suite that belongs to another test suite.

### Migrating and usage

To migrate from `Deno.test`, all you have to do is replace `Deno.test` with
`it`. If you are using the step API, you will need to replace `Deno.test` with
describe and steps with `describe` or `it`. The callback for individual test
cases can be syncronous or asyncronous.

Below is an example of a test file using `Deno.test` and `t.step`. In the
following sections there are examples of how it can be converted to using nested
test grouping, flat test grouping, and a mix of both.

```ts
import {
assertEquals,
assertStrictEquals,
assertThrows,
} from "https://deno.land/std@$STD_VERSION/testing/asserts.ts";
import { User } from "https://deno.land/std@$STD_VERSION/testing/bdd_examples/user.ts";

Deno.test("User.users initially empty", () => {
assertEquals(User.users.size, 0);
});

Deno.test("User constructor", () => {
try {
const user = new User("Kyle");
assertEquals(user.name, "Kyle");
assertStrictEquals(User.users.get("Kyle"), user);
} finally {
User.users.clear();
}
});

Deno.test("User age", async (t) => {
const user = new User("Kyle");

await t.step("getAge", () => {
assertThrows(() => user.getAge(), Error, "Age unknown");
user.age = 18;
assertEquals(user.getAge(), 18);
});

await t.step("setAge", () => {
user.setAge(18);
assertEquals(user.getAge(), 18);
});
});
```

#### Nested test grouping

Tests created within the callback of a `describe` function call will belong to
the new test suite it creates. The hooks can be created within it or be added to
the options argument for describe.

```ts
import {
assertEquals,
assertStrictEquals,
assertThrows,
} from "https://deno.land/std@$STD_VERSION/testing/asserts.ts";
import {
afterEach,
beforeEach,
describe,
it,
} from "https://deno.land/std@$STD_VERSION/testing/bdd.ts";
import { User } from "https://deno.land/std@$STD_VERSION/testing/bdd_examples/user.ts";

describe("User", () => {
it("users initially empty", () => {
assertEquals(User.users.size, 0);
});

it("constructor", () => {
try {
const user = new User("Kyle");
assertEquals(user.name, "Kyle");
assertStrictEquals(User.users.get("Kyle"), user);
} finally {
User.users.clear();
}
});

describe("age", () => {
let user: User;

beforeEach(() => {
user = new User("Kyle");
});

afterEach(() => {
User.users.clear();
});

it("getAge", function () {
assertThrows(() => user.getAge(), Error, "Age unknown");
user.age = 18;
assertEquals(user.getAge(), 18);
});

it("setAge", function () {
user.setAge(18);
assertEquals(user.getAge(), 18);
});
});
});
```

#### Flat test grouping

The `describe` function returns a unique symbol that can be used to reference
the test suite for adding tests to it without having to create them within a
callback. The gives you the ability to have test grouping without any extra
indentation in front of the grouped tests.

```ts
import {
assertEquals,
assertStrictEquals,
assertThrows,
} from "https://deno.land/std@$STD_VERSION/testing/asserts.ts";
import {
describe,
it,
} from "https://deno.land/std@$STD_VERSION/testing/bdd.ts";
import { User } from "https://deno.land/std@$STD_VERSION/testing/bdd_examples/user.ts";

const userTests = describe("User");

it(userTests, "users initially empty", () => {
assertEquals(User.users.size, 0);
});

it(userTests, "constructor", () => {
try {
const user = new User("Kyle");
assertEquals(user.name, "Kyle");
assertStrictEquals(User.users.get("Kyle"), user);
} finally {
User.users.clear();
}
});

const ageTests = describe({
name: "age",
suite: userTests,
beforeEach(this: { user: User }) {
this.user = new User("Kyle");
},
afterEach() {
User.users.clear();
},
});

it(ageTests, "getAge", function () {
const { user } = this;
assertThrows(() => user.getAge(), Error, "Age unknown");
user.age = 18;
assertEquals(user.getAge(), 18);
});

it(ageTests, "setAge", function () {
const { user } = this;
user.setAge(18);
assertEquals(user.getAge(), 18);
});
```

#### Mixed test grouping

Both nested test grouping and flat test grouping can be used together. This can
be useful if you'd like to create deep groupings without all the extra
indentation in front of each line.

```ts
import {
assertEquals,
assertStrictEquals,
assertThrows,
} from "https://deno.land/std@$STD_VERSION/testing/asserts.ts";
import {
describe,
it,
} from "https://deno.land/std@$STD_VERSION/testing/bdd.ts";
import { User } from "https://deno.land/std@$STD_VERSION/testing/bdd_examples/user.ts";

describe("User", () => {
it("users initially empty", () => {
assertEquals(User.users.size, 0);
});

it("constructor", () => {
try {
const user = new User("Kyle");
assertEquals(user.name, "Kyle");
assertStrictEquals(User.users.get("Kyle"), user);
} finally {
User.users.clear();
}
});

const ageTests = describe({
name: "age",
beforeEach(this: { user: User }) {
this.user = new User("Kyle");
},
afterEach() {
User.users.clear();
},
});

it(ageTests, "getAge", function () {
const { user } = this;
assertThrows(() => user.getAge(), Error, "Age unknown");
user.age = 18;
assertEquals(user.getAge(), 18);
});

it(ageTests, "setAge", function () {
const { user } = this;
user.setAge(18);
assertEquals(user.getAge(), 18);
});
});
```
Loading

0 comments on commit 2d5e8d8

Please sign in to comment.