Skip to content

Commit

Permalink
Add Jest environment docs
Browse files Browse the repository at this point in the history
  • Loading branch information
mrbbot committed Nov 12, 2021
1 parent 9359109 commit c28c916
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 2 deletions.
4 changes: 2 additions & 2 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ module.exports = {
"link",
{
rel: "icon",
href:
"data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔥</text></svg>",
href: "data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔥</text></svg>",
},
],
["meta", { property: "og:description", content: pkg.description }],
Expand Down Expand Up @@ -49,6 +48,7 @@ module.exports = {
{ text: "🗺 Source Maps", link: "/source-maps.html" },
{ text: "🕸 Web Standards", link: "/standards.html" },
{ text: "📄 HTMLRewriter", link: "/html-rewriter.html" },
{ text: "🤹 Jest Environment", link: "/jest.html" },
],
},
{
Expand Down
229 changes: 229 additions & 0 deletions docs/jest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# 🤹 Jest Environment

Miniflare includes a custom Jest environment that allows you to run your unit
tests within the Miniflare sandbox. Note that Jest &ge; 27 is required.

## Setup

The Miniflare environment isn't installed by default, install it and Jest with:

```shell
$ npm install -D jest-environment-miniflare jest
```

In the following examples, we'll assume your `package.json` contains
`"type": "module"`, and that you're using a tool to bundle your worker. See
[⚡️ Developing with esbuild](/esbuild.html) for an example.

To enable the Miniflare environment, set the
[`testEnvironment` option](https://jestjs.io/docs/configuration#testenvironment-string)
in your Jest configuration:

```js
// jest.config.js
export default {
testEnvironment: "miniflare",
// Configuration is automatically loaded from `.env`, `package.json` and
// `wrangler.toml` files by default, but you can pass any additional Miniflare
// API options here:
testEnvironmentOptions: {
bindings: { KEY: "value" },
kvNamespaces: ["TEST_NAMESPACE"],
},
};
```

## Writing and Running Tests

The Miniflare environment lets us import our worker's functions with regular
`import` syntax. We can write a test the following worker like so:

```js
// src/index.js
addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event.request));
});

// Assuming you've got a build tool that removes `export`s when you actually
// deploy your worker
export async function handleRequest(request) {
return new Response(`URL: ${request.url} KEY: ${KEY}`);
}
```

```js
// test/index.spec.js
import { handleRequest } from "../src/index.js";

test("responds with url", async () => {
const req = new Request("http://localhost/");
const res = await handleRequest(req);
expect(await res.text()).toBe("URL: http://localhost/ KEY: value");
});
```

Modules support is still experimental in Jest and requires the
`--experimental-vm-modules` flag. To run this test:

```shell
$ NODE_OPTIONS=--experimental-vm-modules npx jest
```

## Isolated Storage

The Miniflare environment will use isolated storage for KV namespaces, caches,
and Durable Objects in each test. This essentially means any changes you make in
a test or `describe`-block are automatically undone afterwards. The isolated
storage is copied from the parent `describe`-block, allowing you to seed data in
`beforeAll` hooks.

As an example, consider the following tests:

```js
// Gets the array
async function get() {
const jsonValue = await TEST_NAMESPACE.get("array");
return JSON.parse(jsonValue ?? "[]");
}

// Pushes an item onto the end of the array
async function push(item) {
const value = await get();
value.push(item);
await TEST_NAMESPACE.put("array", JSON.stringify(value));
}

beforeAll(async () => {
await push("beforeAll");
});

beforeEach(async () => {
// This runs in each tests' isolated storage environment
await push("beforeEach");
});

test("test 1", async () => {
// This push(1) will only mutate the isolated environment
await push(1);
expect(await get()).toEqual(["beforeAll", "beforeEach", 1]);
});

test("test 2", async () => {
await push(2);
// Note that push(1) from the previous test has been "undone"
expect(await get()).toEqual(["beforeAll", "beforeEach", 2]);
});

describe("describe", () => {
beforeAll(async () => {
await push("describe: beforeAll");
});

beforeEach(async () => {
await push("describe: beforeEach");
});

test("test 3", async () => {
await push(3);
expect(await get()).toEqual([
// All beforeAll's run before beforeEach's
"beforeAll",
"describe: beforeAll",
"beforeEach",
"describe: beforeEach",
3,
]);
});

test("test 4", async () => {
await push(4);
expect(await get()).toEqual([
"beforeAll",
"describe: beforeAll",
"beforeEach",
"describe: beforeEach",
4,
]);
});
});
```
Note that bindings (e.g. variables, KV namespaces, etc) are only included in the
global scope when you're using a `service-worker` format worker. In `modules`
mode, you can use the `getMiniflareBindings` global method:
```js
const { TEST_NAMESPACE } = getMiniflareBindings();
```
Note also that storage persistence options (`kvPersist`, `cachePersist`, and
`durableObjectsPersist`) are ignored by the Miniflare Jest environment.
## Durable Objects
When testing Durable Objects, Miniflare needs to run your script itself to
extract exported Durable Object classes. Miniflare should be able to auto-detect
your script from your `package.json` or `wrangler.toml` file, but you can also
set it manually in Jest configuration:
```js
// src/index.mjs
export class TestObject {
constructor(state) {
this.storage = state.storage;
}

async fetch() {
const count = (await this.storage.get("count")) + 1;
this.storage.put("count", count);
return new Response(count.toString());
}
}
```
```js
// jest.config.js
export default {
testEnvironment: "miniflare",
testEnvironmentOptions: {
modules: true,
scriptPath: "./src/index.mjs",
durableObjects: {
TEST_OBJECT: "TestObject",
},
},
};
```
To access Durable Object storage in tests, use the
`getMiniflareDurableObjectStorage` global method:
```js
test("increments count", async () => {
// Durable Objects requires modules mode so bindings aren't accessible via the
// global scope
const { TEST_OBJECT } = getMiniflareBindings();
const id = TEST_OBJECT.newUniqueId();

// Seed Durable Object storage (isolated storage rules from above also apply)
const storage = await getMiniflareDurableObjectStorage(id);
await storage.put("count", 3);

// Increment the count
const stub = TEST_OBJECT.get(id);
const res = await stub.fetch("http://localhost/");
expect(await res.text()).toBe("4");

// Check storage updated
expect(await storage.get("count")).toBe(4);
});
```
<!--prettier-ignore-start-->
::: warning
Note that if you also import `../src/index.mjs` in your test, your script will
be run twice, as Miniflare and Jest don't share a module cache. This usually
won't be a problem, but be aware you may have issues with unique `Symbol()`s or
`instanceof`s.
:::
<!--prettier-ignore-end-->

0 comments on commit c28c916

Please sign in to comment.