ruby.wasm のアプリのテストはこれまで JavaScript で書かなければなりませんでしたが、Ruby のテストコードは RSpec で書きたいと思い、ruby.wasm で RSpec を実行するテストランナー rspec-wasm を作りましたので、使い方を紹介します。
これまでの ruby.wasm のテストコード
ruby.wasm でアプリを作って、テストを作ろうと思うと、JavaScript のヒアドキュメント(テンプレート文字列)に Ruby を書くようなテストコードになってしまいます。
こんな感じです。
import { test } from "node:test";
import assert from "node:assert";
test("初期状態では空の盤面とプレイヤーXで始まること", async () => {
// JavaScript のヒアドキュメントの中に Ruby コードを書く必要があった…
const resultJson = await vm.eval(`
require_relative "../lib/tic_tac_toe"
game = TicTacToe.new
# JavaScript側に値を渡すためにJSON化などの工夫が必要
{ current_player: game.current_player, winner: game.winner }.to_json
`);
const data = JSON.parse(resultJson.toString());
// アサーションも JavaScript 側のライブラリ(assert 等)を使うので RSpec の記法が使えない…
assert.strictEqual(data.current_player, "X");
assert.strictEqual(data.winner, null);
});
テストコード中のコメントにも書きましたが、WASMのテストは JavaScript で書くことになるので、ruby.wasm のテストをするための Ruby のコードは JavaScript のヒアドキュメントの中に書きます。この書き方では、IDEやエディタのシンタックスハイライトや補完が効きません。
そして、テストの結果は JavaScript に渡さなければならないので、JSON化などを考えます。アサーションも JavaScript 側のライブラリに合わせます。
普段 Ruby で開発して RSpec でテストを書いている人から考えると、なかなかストレスがたまる状況でした。
rspec-wasm を使うとどうなるか
rspec-wasm は、ruby.wasm 上で RSpec を実行するテストランナーです。
RSpec が実行されるので、作成するテストコードは、通常の Ruby での開発と同じです。違うのは実行方法だけです。
- spec フォルダを作成して、その配下に *_spec.rb を作成(実行時に spec フォルダを探索します)
- *_spec.rb の記法も通常の RSpec(IDEやエディタの支援が効きます)
-
npm testまたはnpx rspec-wasmで実行(1コマンドで実行できます)
〇×ゲームを作ってテストしてみましょう
サンプルとして、ruby.wasm 向けの〇×ゲームをAIに作ってもらいました。
(英語で〇×ゲームのことを TicTacToe って言うんですね。)
1. プロジェクトの準備
適当なフォルダを作成し、rspec-wasm をインストールします。
mkdir tictactoe-wasm
cd tictactoe-wasm
npm init -y
npm install --save-dev rspec-wasm
2. ゲームロジックを書く (lib/tic_tac_toe.rb)
WebやDOMのことは一切考えない、純粋な Ruby のゲームクラスです。
class TicTacToe
attr_reader :board, :current_player, :winner
def initialize
@board = Array.new(9, nil)
@current_player = "X"
@winner = nil
end
def move(index)
return false if index < 0 || index > 8 || @board[index] || @winner
@board[index] = @current_player
if check_winner
@winner = @current_player
else
@current_player = (@current_player == "X" ? "O" : "X")
end
true
end
def draw?
@board.none?(&:nil?) && @winner.nil?
end
private
def check_winner
lines = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], # 行
[0, 3, 6], [1, 4, 7], [2, 5, 8], # 列
[0, 4, 8], [2, 4, 6] # 斜め
]
lines.any? { |a, b, c| @board[a] && @board[a] == @board[b] && @board[a] == @board[c] }
end
end
3. いつもの RSpec テストを書く (spec/tic_tac_toe_spec.rb)
require "tic_tac_toe" をして、RSpec スタイルでテストを書きましょう。
require "tic_tac_toe"
RSpec.describe TicTacToe do
subject(:game) { TicTacToe.new }
it "初期状態では空の盤面とプレイヤーXで始まること" do
expect(game.current_player).to eq("X")
expect(game.winner).to be_nil
end
it "交互に手を打てること" do
game.move(0) # X
expect(game.current_player).to eq("O")
game.move(1) # O
expect(game.current_player).to eq("X")
end
it "横一列が揃ったら勝利を判定できること" do
game.move(0) # X
game.move(3) # O
game.move(1) # X
game.move(4) # O
game.move(2) # X (勝利)
expect(game.winner).to eq("X")
end
it "埋まったマスには上書きできないこと" do
game.move(0)
expect(game.move(0)).to be false
end
end
4. テストを実行する
ターミナルで以下のコマンドを叩くだけです。
npx rspec-wasm
実行結果:
TicTacToe
初期状態では空の盤面とプレイヤーXで始まること
交互に手を打てること
横一列が揃ったら勝利を判定できること
埋まったマスには上書きできないこと
Finished in 0.03013 seconds (files took 0.93368 seconds to load)
4 examples, 0 failures
Ruby だけでテストまで完了です。(やはり、こうでなくては。)
おまけ (index.html)
ブラウザで表示するための HTML + ruby.wasm の実装です。
# HTTP経由でロジックをロード のところでゲームロジックを読み込んでいます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ruby.wasm 〇×ゲーム</title>
<script src="https://cdn.jsdelivr.net/npm/@ruby/4.0-wasm-wasi@2.10.1/dist/browser.script.iife.js"></script>
<style>
.board { display: grid; grid-template-columns: repeat(3, 80px); gap: 5px; margin-top: 10px; }
.cell { width: 80px; height: 80px; font-size: 32px; font-weight: bold; cursor: pointer; }
</style>
</head>
<body>
<h1>ruby.wasm Tic-Tac-Toe</h1>
<div id="status">読み込み中...</div>
<div class="board" id="board"></div>
<!-- data-eval="async" を追加 -->
<script type="text/ruby" data-eval="async">
require "js"
# HTTP 経由で lib/tic_tac_toe.rb を非同期取得して評価
response = JS.global.fetch("lib/tic_tac_toe.rb").await
code = response.text.await.to_s
eval(code)
$game = TicTacToe.new
def render
document = JS.global[:document]
status_el = document.getElementById("status")
board_el = document.getElementById("board")
if $game.winner
status_el[:innerText] = "勝者: #{$game.winner} 🎉"
elsif $game.draw?
status_el[:innerText] = "引き分け! 🤝"
else
status_el[:innerText] = "ターン: #{$game.current_player}"
end
board_el[:innerHTML] = ""
$game.board.each_with_index do |cell, idx|
btn = document.createElement("button")
btn[:className] = "cell"
btn[:innerText] = cell || ""
btn.addEventListener("click") do
if $game.move(idx)
render
end
end
board_el.appendChild(btn)
end
end
render
</script>
</body>
</html>
npx serve . などでローカルサーバーを立てて、そこにブラウザからアクセスすればちゃんと動きます。
おわりに
rspec-wasm は、実は自分が ruby.wasm で開発する中でほしいと思って作ったものです。
その結果、自分は気分よく開発が進められるようになりました。
AIにコードを書いてもらうことが多くなった昨今、アプリが正しいことをテストコードで行うことが増えるかもしれません。
そのとき、使い慣れたツールが使えたり、これまでのコードを流用できたりすると、大きな助けになると思います。
ruby.wasm でアプリを開発する際には、ぜひ rspec-wasm を使ってみてください。
- npm: rspec-wasm
- GitHub: rspec-wasm