1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ruby.wasm で作ったアプリを RSpec でテストできるようにする rspec-wasm を作りました

1
Posted at

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 を使ってみてください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?