【Golang】【wasm】テストの仕方と注意点 @ Go 1.16+

## WebAssembly 用のアプリでもテストがしたい

> Go 言語（以下 Golang）で組んだ WebAssembly（`wasm`）が動きはするものの、`go test` でテストを実行すると `exec format error` や `imports syscall/js` エラーが出る。
>
> ```text
> fork/exec /tmp/go-buildxxxxxxxxx/xxxx/my_pkg_hoge.test: exec format error
> ```
> ```text
> imports syscall/js: build constraints exclude all Go files in /usr/local/go/src/syscall/js
> ```

- 検証環境:
    -  Go 1.15.6（`golang:1.15-alpine` @ Docker）
    -  Go 1.16.5（`tinygo/tinygo:latest` @ Docker）
    -  Go 1.16.6（`golang:alpine` @ Docker）
    -  Go 1.21.5（@ macOS 12.7.2）

## TL; DR <sub><sup>（今北産業）</sup></sub>

1. ローカル／実行環境に [Node.js](https://ja.wikipedia.org/wiki/Node.js) をインストールする。
1. `wasm` は実行環境（`GOARCH`）なので、テストであってもアプリの実行/ビルド時に OS とアーキテクチャを指定する必要がある（VSCode ユーザーは下記「エラー時の切り分け」参照）。

    ```shell
    GOOS=js GOARCH=wasm go test ./...
    ```
    ```shell
    GOOS=js GOARCH=wasm go build ./... -o myWasm
    ```

1. 環境変数の `PATH` に `go_js_wasm_exec` のあるディレクトリを追加する。

    :::note info
    **これがないと `exec format error` が発生する**。
    一般的に `$(go env GOROOT)/misc/wasm`
    
    - 設定例：`export PATH="$(go env GOROOT)/misc/wasm:${PATH}"`
    - `go_js_wasm_exec` の場所が分からない場合は `find / -name go_js_wasm_exec` などで検索する。
:::

- 参考文献: https://golang.org/src/syscall/js/js_test.go | js | syscall | src @ golang.org

### エラー時の切り分け

<details><summary>【VSCode ユーザー必見】<br> 「<code>could not import "syscall/js"</code>」と出てエラーる。もしくは、エラーがうざい時。</summary><br>

リポジトリの `./.vscode/settings.json` に、以下の VSCode の追加設定を追記し、VSCode を再起動します。

```json:.vscode/settings.json
{
    "go": {
        "toolsEnvVars": {
            "GOARCH": "wasm",
            "GOOS": "js"
        },
        "installDependenciesWhenBuilding": false
    }
}
```

`"installDependenciesWhenBuilding": false` がないと、VSCode からのテストは実行できるものの、[ソースコード上のエラー表記が消えません](https://github.com/microsoft/vscode-go/issues/1874#issuecomment-419773367)。

というのも、VSCode は `gopls` を使って、適宜ソースコードに問題がないか解析しています。

TS; DR でも説明していますが、GOOS が "js"、GOARCH が "wasm" でないと `syscall/js` パッケージは読み込めず、ローカルの環境変数に `$(go env GOROOT)/misc/wasm` を追加しただけでは `syscall/js` のパッケージを読み込めません。`import "syscall/js"` の箇所に以下のエラーが発生します。

```text:エラー内容
could not import syscall/js (cannot find package "syscall/js" in any of 
	/usr/local/Cellar/go/1.**.**/libexec/src/syscall/js (from $GOROOT)
	/Users/admin/go/src/syscall/js (from $GOPATH))compilerBrokenImport
error while importing syscall/js: build constraints exclude all Go files in
	/usr/local/Cellar/go/1.**.**/libexec/src/syscall/jscompiler
```

```text:エラー内容
could not import syscall/js (no required module provides package "syscall/js")
 (compilerBrokenImport)
error while importing syscall/js: build constraints exclude all Go files in
 /usr/local/Cellar/go/1.**.**/libexec/src/syscall/jscompiler
```

</details>

<details><summary>VSCode で「<code>exec: node: not found</code>」と出てテストが実行できない場合</summary><br>

VSCode上からテストを実行すると `exec: node: not found` が出る場合は、ローカルに [Node.js](https://ja.wikipedia.org/wiki/Node.js) がインストールされていません。別途インストールする必要があります。

```shell-session:エラー内容
Running tool: /usr/local/Cellar/go/1.xx.x/libexec/bin/go test -timeout 30s -run ^TestAdd$ github.com/YOURNAME/my-sample-wasm
/usr/local/Cellar/go/1.xx.x/libexec/misc/wasm/go_js_wasm_exec: line 14: exec: node: not found
FAIL	github.com/YOURNAME/my-sample-wasm	0.010s
FAIL
```

</details>

<details><summary>ビルド済みの <code>wasm</code> バイナリを Golang 内から呼び出して使いたい</summary><br>

ビルドされた `wasm` バイナリを実行するには、通常ブラウザを経由するか [Node.js](https://ja.wikipedia.org/wiki/Node.js) が必要です。

しかし、`wasm` アプリの作成ではなく、`wasm` バイナリを Golang 内で読み込んで外部モジュールのように利用したい場合があります。

例えば「[Rust](https://ja.wikipedia.org/wiki/Rust_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E)) などの他言語で作成された `wasm` バイナリを Go で使いたい」といったニッチなケースです。

その場合、Golang で実装されたランタイムに [wasmer-go](https://github.com/wasmerio/wasmer-go) があり、これを Golang のソースから使えるようにバインドした [gowasmer](https://github.com/mattn/gowasmer) を使うことができます。Javascript で言うところの `node.js` と `wasm_exec.js` の Golang 版が `wasmer-go` と `gowasmer` です。

- [wasmer で Go の WASM を実行できるパッケージを作った](https://zenn.dev/mattn/articles/0f702d5da03b15) @ zenn.dev
- [github.com/mattn/gowasmer](https://github.com/mattn/gowasmer) @ GitHub

テストの実行に `wasmer-go` を `node.js` の代わりに使いたいところですが、`wasmer-go` や `gowasmer` は「ビルド済み」の `wasm` ファイルを利用することを前提としています。[カバレッジ](https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%BC%E3%83%89%E7%B6%B2%E7%BE%85%E7%8E%87)などのことも考えると、ローカルに `node.js` 環境を入れた方がメンテナンス性が高まると思います。

</details>

## TS; DR <sub><sup>（Go で wasm のテストを完全に理解した気になっている俺様プラクティス）</sup></sub>

- WebAssembly 用のソースとはいえ、テストの書き方は通常でおk。
- 問題は `syscall/js` パッケージ。OS を`js`、アーキテクチャを `wasm` でテストビルドする必要がある。<sub><sup>（`GOOS=js GOARCH=wasm go test` など）</sup></sub>
- `js, wasm` 指定でビルドするだけでなく [Node.js](https://ja.wikipedia.org/wiki/Node.js) も必要。

Golang で **WebAssembly 用の開発をする場合でもテストは行いたいもの**です。Golang 初心者ゆえ、ユニット・テストを書くだけで最低限のことをしている気になれるので、安心できるのです。

しかし、<ruby>ふいんき<rt>・・・・</rt></ruby>で Golang を触っているものだから、コンパイルしたものは動くものの、いつも通りの Golang のテストを書いてもエラーが出るのです。

後述するように、落ち着いて考えればわかることなのですが、Golang 初心者に輪をかけて、せっかちなものだから「（えっ？あっ！WebAssembly だからブラウザでテストしないといけないの？）」と焦ってしまいました。

さらに、ガバガバなくせに Go でのカバレッジ（テストの網羅率）が脳裏をよぎります。

3 歩ですぐ忘れるのですが、おそらく大事なのは `wasm` を提供する側でのテストと、それを利用する側でのテストは別物であるということです。つまり、Golang 側でのテストと、Javascript 側でのテストです。

この記事では（自分への戒めも含めて）Golang 側でのテストに注力し、気づいた点や学んだ点を<ruby>反芻<rt>はんすう</rt></ruby>するためと、未来の自分のために残したいと思います。

### テストが実行される流れを思い出す。ゆっくりと

アホみたいな話しなのですが、Golang はコンパイル型の言語です。

そのため、テストの場合でもソースから `temp` ディレクトリに<ruby>作成<rt>ビルド</rt></ruby>します。そしてビルドされた `temp` 内のバイナリをデバッガーと一緒に実行し、各種テストの測定を行います。

また、Golang の大きな特徴として OS と CPU の種類を指定すると、それに対応したバイナリを作成してくれます。つまり、Windows で macOS や Linux 向けのバイナリが、実行できなくてもビルドはできる、ということです。クロス困憊るってヤツです。

このことは「知ってはいる」のですが、`wasm` という目新しい用語に翻弄されて「`wasm` 用のソースは、ローカルでテストできない」と早とちりしてしまいました。

気づきのポイントは「`go test` がビルドしたバイナリを『誰が』『どこで』実行するのか」ということです。

この「誰」とはカーネル（OS）のことで、「どこで」はアーキテクチャ（CPU）のことです。

`go env` の環境変数が `GOOS=darwin` `GOARCH=amd64` の場合は `go test ./...` すると、OS が macOS で CPU が AMD/Intel 64bit 向けのバイナリがビルドされます。

この場合、Intel Mac の macOS 上であれば、テストが実行されます。

しかし、macOS 上で `GOOS=darwin GOARCH=amd64 go test ./...` しても M1/M2 Mac だとエラーがでます。OS（カーネル）が合っていても、CPU（アーキテクチャ）が違うためです。

同様に、macOS 上で `GOOS=linux GOARCH=amd64 go test ./...` すると `exec format error` が出ます。当然なのですが、CPU は合っていても OS が違うためです。

「あっ！`wasm` を作るときは `GOOS=js GOARCH=wasm go build` とするから、このマシン（Mac）ではテストできないんジャマイカ！<sup>[🔈](https://ja-audio.howtopronounce.com/162289022760bb56f3620a1.mp3)</sup>」と、よく考えもせず慌ててしまったのです。

しかし、すでにお気づきの方も多いと思います。正しくは「ジャメイカ<sup>[🔈](https://upload.wikimedia.org/wikipedia/commons/0/0a/En-us-Jamaica.ogg)</sup>」です。

### Golang で WebAssembly （`wasm`）を考える場合の 2 つのポイント

1. `syscall/js` パッケージの存在
1. `wasm` バイナリの実行者

Golang で WebAssembly 用の "Hello, world!" が動いたので、喜んでいたのも<ruby>束<rt>つか</rt></ruby>の間。既存の自作アプリを WebAssembly 対応させようと、サイトを練り歩いたりサンプル・プログラムを見ていると `syscall/js` のパッケージを `import` しないといけないことに気づきます。

- [Hello, world! の WASM サンプル](https://github.com/KEINOS/go-wasm-example) @ GitHub

この `syscall/js.go` パッケージは、ブラウザの Javascript が `wasm` バイナリにアクセスした際のブリッジ（橋渡し）もしくはゲート（出入口）となるパッケージです。

Javascript の型と Golang 用の型を変換してくれたり、Javascript 側（ブラウザ側）からのリクエスト（処理依頼）を受け取ったり、返したりなど、交換手のような仕組みを提供してくれています。

**問題は、この `syscall/js.go` パッケージは `GOOS=js GOARCH=wasm` でないと動かない**ことです。

VSCode の [Go 拡張機能](https://marketplace.visualstudio.com/items?itemName=golang.Go)を入れていると `import "syscall/js"` の箇所で以下のようなエラー・メッセージが出ていると思います。

```
could not import syscall/js (no required module provides package "syscall/js") compiler (BrokenImport)
error while importing syscall/js: build constraints exclude all Go files in /usr/local/go/src/syscall/jscompiler
```

エラー内容としては、コードの静的解析時に `syscall/js` モジュールの読み込みに失敗した（存在しなかった）という内容です。

このエラー自体は、ソースの上部に以下のコメントをつけると解除されます。

```go
//go:build js && wasm
// +build js,wasm
```

では、なぜ読み込みに失敗したかと言うと、`syscall/js.go` パッケージのソース・コードのヘッダを見ると、上記と同じ `//go:build js && wasm` が指定されているためです。

- [js.go](https://golang.org/src/syscall/js/js.go) | js | syscall | src @ golang.org

#### `//go:build` はたまた `// +build` の復習

Golang では、ソースコードの `package` 宣言の前の**コメント行に `//go:build` という記載があった場合は、マッチした `GOOS` や `GOARCH` の場合のみビルドの対象にする**ことができます


```go
//go:build js && wasm
// +build js,wasm
```
そのため、<font color=darkorange>**`syscall/js.go` パッケージは、`GOOS/GOARCH` が `js/wasm` の場合のみ読み込まれる**</font>ということです（ちなみに `// +build` の記法は Go 1.17 より前のバージョンの記法で同じ意味を持ちます。下位互換のため記載したりもできます）。

これが、通常通り `go test ./...` と実行しても `imports syscall/js` エラー（パッケージがないエラー）が出る原因です。

この `//go:build ...` や `// +build ...` を使った仕組みは 1 つのリポジトリで複数 OS に対応したい場合に力を発揮します。特にシステム・コールがらみ（OS 独自の API やコマンドを利用）の場合などです。

例えば、とある関数のコードを「Windows 用」「Linux 用」「macOS（darwin）用」と分けておき、各々のソースコードのヘッダに `# +build darwin` などと限定させることで、他のパッケージは同じ関数名を使うことができます。

> <sub><sup>🐒 &nbsp;&nbsp;この OS 互換について、以下のパッケージのリポジトリが、とても参考になります。ターミナル／コマンドプロンプトからの入力を OS ごとにわけることで一元化してくれる、CUI アプリを作る場合に、たいへんありがたいパッケージです。</sup></sub>
>
> - [go-tty](https://github.com/mattn/go-tty) | [mattn](https://github.com/mattn) @ GitHub

ここで少し整理します。

1. `syscall/js` パッケージを使う場合は `GOOS=js GOARCH=wasm` でテストやビルドを実行しないとパッケージ足らずでエラーになる。
1. `GOOS=js GOARCH=wasm` でテストやビルドすると、`GOOS` と `GOARCH` が実行環境と合わないためエラーになる。

この 2 つの矛盾を何とかしないといけません。

### ビルドした `wasm` を Web サーバに設置した時のことを思い出す

`Hello, world!` をブラウザで動かした時のことを思い出しましょう。必要なファイルは以下のような感じだったと思います。

```shell-session
$ ./build.sh
**snip**

$ tree ./html
./html
├── index.html # <- 元 wasm_exec.html
├── test.wasm
└── wasm_exec.js
```

- [Hello, world! の WASM サンプル](https://github.com/KEINOS/go-wasm-example) @ GitHub

ここで重要なのが `wasm_exec.js` です。Javascript が `*.wasm` バイナリを読み込んで関数を実行するのに必要なものだからです。

つまり、Golang 側で言うところの `syscall/js` と似た役割を、Javascript 側では `wasm_exec.js` が<ruby>担<rt>にな</rt></ruby>ってくれているのです。

`wasm_exec.js` の注意点として、`*.wasm` を作った（ビルドした）コンパイラによって中身が変わることです。

つまり、<font color="darkorange">**コンパイラが変わった場合は、`wasm_exec.js` もコンパイラに合ったものに変えないといけません**</font>。

そして、肝心の `wasm_exec.js` は `go` と一緒に同梱されています。公式リポジトリの Wiki ではシンプルに「ローカルからコピーする」と書いてあります。

> Copy the JavaScript support file:
>
> ```shell-session
> $ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
> ```
> <sub><sup>（[WebAssembly](https://github.com/golang/go/wiki/WebAssembly) | Wiki | go | golang @ GitHub より）</sup></sub>

これを読み違えたのか、ネットの記事では「公式リポジトリから最新版をダウンロードしろ」的なことも書いてあったりします。案の定、ネット記事をベースに試して動かないと Issue を立てた人がいて、「ドキュメント嫁」から叱られてました。

>> I then copied the js / html fiels for running wasm in browser via:
>> https://github.com/golang/go/tree/master/misc/wasm
>
> Do not copy from master branch. Use the files from your distribution as mentioned in the wiki page.
>
> `$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .` and same for the html file.
> <sub><sup>（[issue コメント](https://github.com/golang/go/issues/29827#issuecomment-455747409) | Issue #29827 | go | golang @ GitHub より）</sup></sub>
>
> 【以下、筆者訳】
>>
>> 次に、wasm を走らせる js と html ファイルをブラウザ経由で以下からピーコピーコしてきました。
>> https://github.com/golang/go/tree/master/misc/wasm
>
> マスターブランチからコピペピピックしないでください。Wiki にもあるように、自分に配布された（Go にバンドルしてきた）ものを使ってください。html ファイルについても同様です。
>
> `$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .`

他人の嫁ながら、自分も叱られた思いがします。独身だけど。

つまり、**`wasm` をコンパイルしたものが提供している `wasm_exec.*` を利用しないといけない**ということです。

例えば、Go の標準コンパイラより小さいバイナリが作成できる [TinyGo](https://tinygo.org/) コンパイラを使う場合も同様です。

この場合は、TinyGo と同じ環境にある Go が提供しているものではなく、実際にコンパイルする TinyGo が提供している `wasm_exec.js` を使います。具体的には、TinyGo の Docker の場合は以下のディレクトリにあります。

- `wasm_exec.js`: `/usr/local/tinygo/targets/wasm_exec.js`
- `wasm_exec.html`: `/usr/local/tinygo/src/examples/wasm/main/index.html`

さて、ブラウザ実行の場合は、コンパイラに合った `wasm_exec.js` を使うことは理解できました。違うものだと Javascript がコンパイラの API を理解できないためです。

では、本題の「Golang 側で `wasm` バイナリをテストする」にはどうすればいいでしょう。

### Node.js は OS みたいなもの

復習ですが、`wasm` のバイナリは OS が `js`（Javascript）、アーキテクチャが `wasm` で<ruby>ビルド<rt>コンパイル</rt></ruby>されています。（`GOOS=js GOARCH=wasm go build ./main.go -o ./doc/mywasm.wasm`）

そして、実行する時は Javascript 環境で `wasm` の構文を解釈できるプロセッサー（処理系統）でないといけません。つまり、Javascript + WebAssembly 対応のブラウザなら実行できるということです。

問題は「テストを実行する場合はどうなるのか」というところです。

まず、アーキテクチャの問題ですが、先の「`wasm_exec.js` はコンパイラと合ったものを使う」ことで Javascript 側から `wasm` を処理することができます。となると、残る問題は OS である `js`（Javascript）をどうするかです。

もぅ、すでにお気づきだと思いますが、**[Node.js](https://ja.wikipedia.org/wiki/Node.js) を介して実行すればいい**のです。

Node.js（`node`）は、Javascript のランタイムです。つまり、`php ./index.php` や `python ./index.py` のように、`node ./index.js` と実行すると Javascript を実行できるのです。

Golang のテスト（`go test`）の場合、まずテスト用にバイナリをビルドしてから、直接バイナリを実行して、その結果を取得することでテストを行います。

つまり、`go` が `$ /path/to/mybuiltapp_test ...（テスト用の引数）...` のように実行するのです。

となると、この時に `go` が `$ node /path/to/mybuiltapp_test ...（テスト用の引数）...` と実行出来れば、その実行結果をテストに反映できることになります。

何か見えてきたでしょうか。

そう、**コンパイルは `go` にさせて、バイナリの実行は `node` にまかせればいい**のです。

具体的には `go run` の `-exec` オプションが使われます。

ここで、`-exec` オプションの詳細の前に確認したいことがあります。

先の「ドキュメント嫁に叱られる」件で「配布されたものを使え」と `cp $(go env GOROOT)/misc/wasm/wasm_exec.js` 光線を浴びました。そのディレクトリにあるファイルを見てみましょう。

```shell-session
$ ls -lah "$(go env GOROOT)/misc/wasm"
total 36K    
drwxr-xr-x    2 root     root        4.0K Dec  4  2020 .
drwxr-xr-x   12 root     root        4.0K Dec  4  2020 ..
-rwxr-xr-x    1 root     root         441 Dec  4  2020 go_js_wasm_exec
-rw-r--r--    1 root     root        1.3K Dec  4  2020 wasm_exec.html
-rw-r--r--    1 root     root       16.7K Dec  4  2020 wasm_exec.js
```

「変なところからコピペピピックせずに、ここからコピーしろ」と言われた `wasm_exec.js` と `wasm_exec.html` が確認できます。

ここで**注目してもらいたいのが `go_js_wasm_exec` ファイル**です。初めて出て来た名前ですが、これが `-exec` に使われます。

Golang は、`go run` 実行時に `-exec` オプションが指定されて<ruby>いない<rt>・・・</rt></ruby>場合、`GOOS` と `GOARCH` が現在の環境とマッチしているか否かで挙動が変わります。

マッチしている場合は、テスト用に作成されたバイナリはそのまま実行されます。

```shell-session
$ ./path/to/mybuiltapp_test ...（テスト用の引数）...
```

マッチしない（異なる）場合は `go_<GOOS>_<GOARCH>_exec` という書式のファイルを、パス（環境変数の `PATH` にあるディレクトリ）から探し、それを使ってビルドされたテスト用バイナリを実行します。

この時、`go run -exec=go_<GOOS>_<GOARCH>_exec ./...` として実行するのと同じ挙動になります。

つまり `GOOS=js` `GOARCH=wasm` の場合、<ruby>ビルド<rt>コンパイル</rt></ruby>後、`go_js_wasm_exec` を探して以下のようにテスト用に作成されたバイナリを実行します。

```shell-session
$ go_js_wasm_exec /path/to/mybuiltapp_test ...（テスト用の引数）...
```

そして、`go_js_wasm_exec` の中身ですが、同ディレクトリにある `wasm_exec.js` を使った Javascript で書かれたテストを作成して `node` に渡し、その実行結果を Go が解釈できる形で返します。パーサーのような役割をします。

ここで<font color="darkorange">**大事なのが `go_js_wasm_exec` を見つけられるようにパスを通す必要がある**</font>ということと、`node` がインストールされていないといけないことです。

### まとめ

1. HTML で使う `wasm_exec.js` は、コンパイラー（Go もしくは TinyGo）が提供しているものを使う。
1. `wasm_exec.js` などがあったディレクトリを、OS の検索パス（環境変数 `PATH` など）に追加する。
1. `node` コマンドが実行できるように Node.js を入れておく<sub><sup>（`npm` などのパッケージ・マネージャーは基本的に不要）</sup></sub>
1. `go test ./...` する時に `GOOS=js GOARCH=wasm go test ./...` と OS とアーキテクチャを指定する。