GitHub Actionsでthree.jsの単体テスト

## はじめに

この記事は、GitHub Actions上でthree.jsの単体テスト方法を記録、共有するためのものです。

node.jsでWebGLを利用したコードを単体テストし、GitHub Actions上で処理します。ヘッドレスブラウザやelectronは使用しません。

### 想定する読者

この記事は、以下の読者を想定して書かれています。

-   JavaScriptの開発経験がある
-   Jestを使ったことがある
-   three.jsを使ったことがある
-   リポジトリをGitHubで管理している / 管理したい

JavaScriptの解説やJestのインストールガイドはこの記事には含まれません。

### 想定する環境

この記事は、以下の環境を想定して書かれています。記事を読む前に、お手元の環境をご確認ください。

```yml
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18.x, 20.x]
```

また、テスティングフレームワークはJestを利用します。

```json
"devDependencies": {
  "jest": "^29.3.1",
}
```

## 先に結論だけ

https://github.com/MasatoMakino/minimal-test-environment-jest-threejs

GitHub Actionsでthree.jsを単体テストする最小限のリポジトリを作りました。

## WebGLを利用した単体テスト

[Jest](https://jestjs.io/ja/)はnode.jsで稼働するJavaScriptテスティングフレームワークです。Jestは[jsdom](https://github.com/jsdom/jsdom)という、JavaScriptで記述されたDOM実装と連携します。このjsdomを使ってCanvasを作成し、WebGLを利用するコードをテストします。

### Jestでjsdomを利用する

Jestでjsdomを利用するには、まずJestと`jest-environment-jsdom`をインストールします。

```bash
npm i -D jest jest-environment-jsdom
```

次に[設定ファイル](https://jestjs.io/ja/docs/configuration)を作成し環境に`jsdom`を指定します。

_▼jest.config.js_

```js
module.exports = {
  testEnvironment: "jest-environment-jsdom",
};
```

もしくは、jestコマンドに`--env=jest-environment-jsdom`オプションを追加します。

_▼package.json_

```json
"scripts": {
  "test": "jest --env=jest-environment-jsdom"
}
```

### 課題

jsdomは軽量化のため、標準設定では[Canvas要素をdiv要素で代用します](https://github.com/jsdom/jsdom#canvas-support)。そのため、CanvasRenderingContext2DやWebGLRenderingContextはテストで使えません。

- Canvas要素のサポート
- WebGLRenderingContextのサポート

jsdomでWebGLをテストするためには、この2つの課題を解決しなければなりません。

### Canvas要素のサポート

jsdomでCanvas要素をサポートするために、[node-canvas](https://github.com/Automattic/node-canvas)を利用します。

```shell
npm install canvas --save-dev
```

jsdomは`node-canvas`のインストールを自動で検知します。ユーザーがなにか設定をする必要はありません。

```js
const canvas = document.createElement("canvas");
```

テストコード上でdocument.createElementを呼び出すと、`node-canvas`からCanvas要素が返されます。

### WebGLRenderingContextのサポート

jsdomでWebGLRenderingContextをサポートするために、[headless-gl](https://github.com/stackgl/headless-gl)を利用します。headless-glはnode.js上でWebGLRenderingContextを作成するモジュールです。

```shell
npm install gl --save-dev
```

テストコードでは、以下のようにWebGLRenderingContextを生成します。

```js
const gl = require('gl')(width, height)
```

width, heightには好きなサイズを指定できますが、THREE.WebGLRendererのsetSize関数で変更されてしまうため、最小限のサイズを指定します。

```js
const gl = require('gl')(1, 1)
```

### テストコードの作成　

`node-canvas`と`headless-gl`を使ってテストを作成します。

_▼./\_\_test\_\_/Test.spec.js_

```js
const THREE = require("three");

jest.spyOn(console, "error").mockImplementation()

describe("Test", () => {
    const canvas = document.createElement("canvas");

    test("2d context should be exist", () => {
        const context = canvas.getContext("2d");
        expect(context).toBeTruthy();
    })

    test("generate webgl context", ()=>{
        const gl = require("gl")(1,1);
        expect(gl).toBeTruthy();
    })

    test("generate webgl renderer", ()=>{
        const gl = require("gl")(1,1);
        const renderer = new THREE.WebGLRenderer({context:gl, canvas:canvas});
        renderer.setSize(640, 480);

        expect(renderer.getContext()).toBeTruthy();
        expect(renderer.getContext()).toBe(gl);
        expect(renderer.domElement.width).toBe(640);
    })
})
```

このテストで、以下の課題2点が確認できました。

- Canvas要素のサポート
- WebGLコンテキストのサポート

また、CanvasとWebGLコンテキストを利用してWebGLRendererが生成できました。

## GitHub Actionsで実行する

ここまでで作成したテストは、そのままGitHub Actionsで動作しません。GitHub Actionsの仮想環境`ubuntu-latest`には、headless-glが依存するいくつかのパッケージが含まれていないためです。

headless-glはビルド済みのバイナリを配布していますが、2024/01/05の時点では、node.js v20向けのバイナリは配布されていません。node.js v20環境でテストを行う場合は、依存パッケージをaptからインストールし、ビルドしなければなりません。

```yml
    steps:
      - uses: actions/checkout@v4
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: "npm"
      - run: sudo apt-get update
      - name: Install dependencies for headless-gl
        run: sudo apt-get install -y build-essential libxi-dev libglu1-mesa-dev libglew-dev pkg-config
      # npm ciの途中で、headless-glのビルドが始まる
      - run: npm ci
      # npm testの実行直前に xvfb-run --auto-servernum で仮想フレームバッファーXvfbを起動
      - run: xvfb-run --auto-servernum npm test
```

各OSで必要とされるパッケージは、[headless-glのREADME](https://github.com/stackgl/headless-gl?tab=readme-ov-file#system-dependencies)を参照してください。

</details>

## 参考記事

https://qiita.com/agehama_/items/5e3dedea73a2a1e16dc4

https://github.com/juliangruber/browser-run/issues/147?utm_source=pocket_mylist#issuecomment-564553618


以上、ありがとうございました。