mocha + jsdom で HTMLElement の単体テストを実行する

変更履歴

* `node-canvas`を追加

# やりたいこと

* [`mocha`](https://mochajs.org/) で HTML DOM 操作のあるモジュールのユニットテストをしたい

# 問題点

* HTML DOM を操作するオブジェクト（[`Document`](https://developer.mozilla.org/ja/docs/Web/API/Document), [HTMLCanvasElement](https://developer.mozilla.org/ja/docs/Web/API/HTMLCanvasElement) etc...)は`Node.js`では標準実装されていない為、テストコードを実行すると必ずエラーになる。

```javascript: エラーになる例
module.exports = {
    setup(): () => {
        const container = document.querySelector(".container"); // <- この行でエラー
        
        if(!container){
            throw new DOMException(`".container" is undefined.`);  // <- この行でエラー
        }

        const canvas = document.querySelector("canvas");

        const ctx2d = canvas.getContext("2d"); // <- この行でエラー
        // ... 後続処理 ...
    }
};
```

```log:エラーの例 (document が無い)
ReferenceError: document is not defined
```

```log:エラーの例 (DOMException が無い)
ReferenceError: DOMException is not defined
```

※ 他にもありそう

# 結論

[`jsdom` | Github](https://github.com/jsdom/jsdom)を活用する

# 前提

* Node.js が正しくインストールされていること
* mocha でテストコードが実行可能な事

# 環境

* Node.js - v16.14.2
* mocha - v10.2.0
* jsdom - v22.1.0
* canvas - v2.11.2

# 1. `jsdom`をインストール

```bash:bash
npm install --save-dev jsdom @types/jsdom
```

# 2. テストコードに組み込む

想定する構成

* :open_file_folder: src
    * :notebook: sample.js
* :open_file_folder: test
    * :notebook: sample.spec.js
    * :open_file_folder: mocks
        * :notebook: sample.mock.js

## 2.1. 足りないオブジェクトの`fake`を作成する

### 2.1.1. `node-canvas`をインストール

別稿を参照

https://qiita.com/ryo2020/items/fd310b487bc2ae194d07

### 2.1.2. fakeを作成する

```javascript:./test/mock/sample.mock.js
import jsdom from "jsdom";

const { JSDOM } = jsdom;
const fakedom = new JSDOM();
const { window } = fakedom;

// fake objects
// Node.js の globalThis に宣言がなければ参照を追加
globalThis.DOMException ??= window.DOMException; // Node.js v18 以降では不要
globalThis.document ??= window.document;
globalThis.HTMLCanvasElement ??= window.HTMLCanvasElement;

// ... 他のオブジェクトも、足りないものがあれば参照を追加していく ...
```

## 2.3. `jsdom`で`document`の`fake`を作成し、テスト対象関数の引数とする

`fake object`も`import`しておく

```diff_javascript:テストコード
/**
 * ./test/sample.spec.js
 */

import { assert } from "chai";

// テストの対象モジュール
import {setup} from "@/dom.js";

+ // fake object をインポート
+ import "@/../test/mocks/sample.mock";

+ // @see https://github.com/jsdom/jsdom#basic-usage
+ import jsdom from "jsdom";
+ const { JSDOM } = jsdom;

describe("dom のテスト", () => {
+   const fakedom = new JSDOM(`<!DOCTYPE html>`);
+   const { document } = fakedom.window;

    it("setup", () => {
-       setup();
+       setup(document);
    });
});
```

## 2.3. `document`を引数で受け取れる様に変更する。

※実環境への影響は最小限にする

```diff_javascript:テスト対象のモジュール
/**
 * ./src/dom.js
 */

module.exports = {
-   setup: () => {
-       const container = document.querySelector(".container");
+   setup: (document) => {
+       const doc = document || self.document;
+       const container = doc.querySelector(".container");
        
        if(!container){
            throw new DOMException(`".container" is undefined`);
        }

        // ... 後続処理 ...
    },
};
```

## 2.4. テストを実行

```bash:bash
npm run test

... 中略 ...

> mocha test/**/*.spec.ts -R spec



dom のテスト
    ✔ setup

  1 passing (1ms)
```

了