Hatena::ブログ(Diary)

土屋つかさのテクノロジーは今か無しか

2017-02-16

rubyゲーム開発にユニットテスト/テスティングフレームワークを導入する(6)【CI/AppVeyor編】

 長々とやってきました「rubyゲーム開発にユニットテスト/テスティングフレームワークを導入する」シリーズもひとまず今回で一区切りになります(なぜならそろそろ作業を区切って同人誌の原稿執筆に入らないといけないからだ!)。今回はCIの導入です。

CIとはなにか

 集団で巨大なソフトウェアコーディングする際、個々のプログラマは日々ローカル環境でビルド&テストを行いますが、共有リポジトリ内の最新コードに対してビルド&テストを定期的に行い、コードの安定性を確認するのは人力で行うには高コストです。この作業自動化する試み、あるいはそのサービスのことををCI(continuous integration:継続的インテグレーション)と呼びます。

継続的インテグレーション
https://ja.wikipedia.org/wiki/%E7%B6%99%E7%B6%9A%E7%9A%84%E3%82%A4%E3%83%B3%E3%83%86%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3

 CI支援ソフトウェアの中で一番有名なのは恐らくJenkinsでしょう。Jenkinsはリポジトリからのコミット通知を受けて自身のサーバー上にコードをダウンロードビルド→テストなどの一連の作業を行います。これによって常に最新のコードに対してテストが適用され、またパッケージがアップされ、定期ビルドの手間が削減されるのです。

 Rubyでは広義のビルドタイミングがありませんが、実行時の構文エラーやユニットテストにおいてCIの支援を受けることができます。

AppVeyorについて

 今回はGitHub上の司エンジンのリポジトリに対して、AppVeyorでのCIが機能するようにします。AppVeyorはオープンソースプロジェクトに対して無償CIサービスを提供してくれる有難いサービスです。この手の無償サービスではTravisCIの方が有名なのですが、
今回は使えません。何故なら司エンジンのテストを行うにはWindows環境でなければならないからです。

 TravisCIはLinux環境でのCIが提供されるのですが、AppVeyorはなんとWindows環境でのCIが提供されます。土屋は実践してませんが、UIを伴ったリモートコントロールも可能なのだそうです(どうにもビルドがこける時とかに使う)。すごいなAppVeyor!

AppVeyorの環境設定

 APpVeyorでのアカウント作成からGitHubとの連携までの流れについてはこちらの記事が詳しいのでそちらをどうぞ。

How to use AppVeyor (AppVeyorの使い方)
http://qiita.com/takahashim/items/1851b6c3e05bb140bb09

司エンジンでの環境設定

 GitHubリポジトリのルートにappveyor.ymlファイルを配置します。YAML形式でAppVeyorで実行される各タスクシェルで実行される処理を記述していきます。

appveyor.yml

# appveyor.yml
install:
  - set PATH=C:\Ruby22\bin;%PATH%
  - set APPVEYOR_BUILD=true
  - gem install bundler
  - bundle install

build: off

before_test:
  - ruby -v
  - gem -v
  - bundle -v

test_script:
  - bundle exec rake spec

 "bundle install"によって、下記のGemFileが実行され、司エンジンの実行に必要なgemインストールされます。

GemFile

# frozen_string_literal: true
source "https://rubygems.org"

gem "rake"
gem "rspec"
gem "dxruby"
gem "parslet"

 "bundle exec rake spec"で実行されるspecタスクは、rakefileで以下のように記述されています。内容は前回と変わりませんが、タスクが宣言的に記述されるのがどうにも気持ちわるかったのでdo〜endで囲むようにしました。

rakefile

task :spec do

	require "rspec/core/rake_task"
	RSpec::Core::RakeTask.new("spec")

end

ビルド

 この状態でGitHubにコミットすると、自動的にAppVeyor上でテストが実行され、ログが保存されます。
f:id:t_tutiya:20170216231339p:image
f:id:t_tutiya:20170216231336p:image
f:id:t_tutiya:20170216231333p:image
 繰り返しになりますが、これの何が凄いのかというと、司エンジンのテストコードのほとんどはDXRuby/DirectXを経由してアプリウィンドウに描画を行っています。そのテストコードが通っているということは、AppVeyor上でゲーム画面が表示されているということです(確認はしていませんが、リモートコントロールで確認できる筈です)。
f:id:t_tutiya:20170216231332p:image
 AppVeyorはビルドの結果を示すバッジを提供しているので、これをGitHub側で設定することで、最新のコードのビルドが通っていることを示すことができます。これによって、コードの安定性を第三者に示すことができます。

終わりに

 昨年11月末に友達の超ウィザードエンジニアと飲んだ時に、「ゲームのコードでのユニットテストは現実的ではない」と言った時「違う。テストがしやすいようにコードを最適化するんだ」と返されて目からウロコが落ち、その方からアドバイスをもらいながら、ゲーム開発でのユニットテスト導入方法を模索してみました。

 CIまで実現出来たので、これでユニットテスト周りの技術を一通り触ったことになります。まさか2ヶ月半かかるとは思わなかったよ!w
 さきほども書いた通り、今回の内容は整理/加筆して同人誌の原稿とし、技術書典2で頒布予定なので、どうぞよろしくお願いします。
 みなさん、ゲーム開発でのCIベースによるテスト駆動開発は可能です!><

補足

 実はAppVeyor対応時にどうにも上手く行かなかったことが一個あります。司エンジンではBGM/SEの再生にAyame/rubyを利用しているのですが、このライブラリのrequireしている最中にAyame.dllの読みこみに失敗するのを解決する方法がわかりませんでした。

 ひとまず緊急回避として、appveyor.ymlのinstallタスク時に環境変数APPVEYOR_BUILDに値を設定し、Sound.rb内の該当箇所をスキップさせました。

Sound.rb

#TODO将来的にはプリミティブライブラリへの依存を解消したい
#TODO現状appveyor上でAyame.dllをロードする方法が不明なため緊急回避
unless ENV['APPVEYOR_BUILD'] == "true"
  require_relative './ayame.so'
end

 これでひとまずビルドは通るようになりました。

2017-02-12

無料2Dゲームフレームワーク「司エンジン」v2.2正式リリース!

 無料のゲームフレームフレーワーク「司エンジン」v2.2が正式リリースしました。以下からダウンロードできます。

・v2.2スターターキット
http://someiyoshino.main.jp/file/tsukasa/tsukasa_engine_starter_kit_v2_2.zip
 こちらには司エンジンのコード一式の他、実行可能ファイルとリファレンスマニュアルが同梱されています。直接サンプルコードを実行して動作を確認し、ゲーム開発を始められます。ダウンロードしたらmain_dev.exeを実行してみてください。

・司エンジンガイドブック第3版
http://someiyoshino.main.jp/file/tsukasa/tsukasa_engine_guide_book_for_v2_2.pdf
 ・PDF形式のガイドブックになります(B5/118ページ)
 ・サンプルコードの開発と最新のリファレンスマニュアルから構成されています。

GitHub
https://github.com/t-tutiya/tsukasa/releases/tag/v2.2

今後の予定


 現状でやりたいことはほぼやりきったと思っています。今後はこのエンジンを使って同人ゲームの開発を進めつつ、Unityへの移植と、DXRuby上での3D対応を模索したいと考えています。
 自作のゲームエンジンでゲームを作りたいと思い続けて何年もかかりましたが、ようやくスタート地点に立ちました(笑)。さあ、なんでも作るぞ!><

更新履歴


##v2.2(2017/2/14)

・ フォルダ構成
プラグインフォルダをネイティブファイルのフォルダとスクリプトファイルのフォルダに分離した

ビルド関連
・ Bundler環境を整備

ユニットテスト関連
RSpec環境を整備
・ テスト実行用のRakeFileを配置
・ テストコードの命名ルールを設定

・ 内部ロジック
・ カスタムコントロールのメソッド定義インターフェイスを変更
・ DXRubyオブジェクトをinitialize時に互換オブジェクトに差し替えられるようにした

・ サンプル
ブロック崩しゲームをサンプルに追加
・ カスタムシェーダーサンプルを更新

・ default_script.rb
・ 内容を整理
・ 追加
・ _GC_GARBAGE_COLLECT_
・ _GC_ENABLE_
・ _GC_DISABLE_
・ _GC_LATEST_GC_INFO_
・ _GC_STATUS_
・ 廃止
・ _PAD_ARROW_
・ _WINDOW_STATUS_
・ _SCREEN_MODES_
・ _FULL_SCREEN_
・ _MOUSE_WHEEL_POS_
・ _INPUT_MOUSE_

・ helper_script.rb(utility_script.rbから改名)
・ ファイル名を変更し、内容を整理。
・ _CHAR_IMAGE_の引数pathから_ARGUMENT_に変更
・ _TEXT_WINDOW_のフラグを管理する専用のDataコントロールを用意
・ _TO_IMAGE_で生成するImageコントロールのサイズ指定を必須とした

・ Controlコントロール
・ _END_FRAME_コマンドの名称を_HALT_に変更
・ _NEXT_/_BREAK_コマンドにブロックを付与できる仕様を廃止
・ _SEND_コマンドで設定したプロパティの値をブロック内で利用できるようにした
・ _SCOPE_コマンド廃止
・ _LOAD_NATIVE_コマンド廃止
・ _SERIALIZE_コマンドにcontrolオプションを追加
・ _ALIAS_のオプション名をnew/oldからnew_name/original_nameに変更(newが紛らわしいため)
・ command_listプロパティを廃止(ドキュメントには元々記載無し)
・ child_indexプロパティ廃止
・ (内部処理)exitプロパティを読み出し専用に変更(元々ドキュメントにはない)
・ (内部処理)Control#unshift/push_command_arrayメソッドを追加
・ (内部処理)Control#updateメソッドインターフェイスを変更

・ Windowコントロール
継承元をClickableLayoutに変更
・ Window.caption/bgcolor/icon_path/cursor_type/full_screen/screen_modes/mouse_wheel_pos/inactive_pause/mouse_offset_x/mouse_offset_yプロパティ追加

・ Drawableモジュール(旧Drawableコントロール)
モジュールに変更
・ shaderプロパティをDXRuby::Shaderを直接保持する形式に変更

・ Layoutableモジュール(旧Layoutableコントロール)
モジュールに変更
・ width/height/float_x/float_y/align_x/align_y/offset_x/offset_yプロパティ廃止
・ check_float/check_alignメソッド廃止

・ Clickableモジュール
・ 新規作成(旧ClickableLayoutコントロールから実装を移動)
・ collision_shapeプロパティをshapeに改名

・ ClickableLayoutコントロール
・ Clickableモジュールをインクルードする形式に変更

・ Imageコントロール
・ _PIXEL_コマンドのブロック引数名を_ARGUMENT_からcolorに変更
・ _DRAW_コマンドの第1引数をコントロールへの相対パスに変更

・ TileMapコントロール
・ _MAP_STATUS_コマンドのブロック引数名を_ARGUMENT_からstatusに変更

・ Inputコントロール
・ パッド番号をpad_codeプロパティで決定する形式にした
・ x/yプロパティ追加

・ RuleShaderコントロール
・ Shaderクラスを継承する形に変更し、RuleTransition.rbに改名

・ Shaderコントロール
・ 新規追加

・ TextPageコントロール
・ _CHAR_RENDERER_/_CHAR_WAIT_/_LINE_WAIT_コマンドブロックを初期値として設定できる仕様を廃止(ドキュメントには元々記載無し)
・ use_image_font/image_face初期化プロパティ廃止(ドキュメントには元々記載無し)
・ _INSERT_CHAR_IMAGE_コマンド追加
・ _CHAR_コマンドの名称を_INSERT_CHAR_に変更
・ _CHAR_COMMAND_の名称を_INSERT_COMMAND_に変更

・ pluginフォルダ
・ HorrorTextShader.rb追加

rubyゲーム開発にユニットテスト/テスティングフレームワークを導入する(5)【RSpec導入編】

 前回(http://d.hatena.ne.jp/t_tutiya/20170209/1486646849)まではユニットテストフレームワークとして、Rubyに標準添付されているMinitestを使用していました。Minitestは実装がコンパクトで、最小限の知識があればテストが実行出来て便利なのですが、モック/スタブの使い勝手が悪かったので、今回はRSpecというライブラリを使うことにします。RSpecRubyにおけるテストフレームワークデファクトスタンダードに位置付けられています。

準備1:インストール

 RSpecは標準添付されていないのでGemFileに記載してBundler経由でインストールします。gemコマンドで"gem install rspec"と直接実行しても良いのですが、GemFileに書いておけば、次に環境構築するのが楽になるので。

#GemFile

# frozen_string_literal: true
source "https://rubygems.org"

gem "rake"
gem "rspec"
gem "dxruby"
gem "parslet"

 bundleでinstallコマンドを実行すると、追記されたrspecインストールされます。

C:\data\tsukasa>bundle install
Fetching gem metadata from https://rubygems.org/..........
Fetching version metadata from https://rubygems.org/.
Resolving dependencies...
Installing diff-lcs 1.3
Installing rspec-support 3.5.0
Using bundler 1.14.3
Installing rspec-core 3.5.4
Installing rspec-expectations 3.5.0
Installing rspec-mocks 3.5.0
Installing rspec 3.5.0
Bundle complete! 1 Gemfile dependency, 7 gems now installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.

準備2:初期化処理

 次にrspec初期化処理を行います。"--init"オプションを付けてrspecを実行すると、カレントディレクトリ上にひな形のファイルやフォルダを自動生成してくれます。これらは自前で用意してもいいですが、楽です。

C:\data\tsukasa>bundle exec rspec --init
  create   .rspec
  create   spec/spec_helper.rb

 rspecの起動オプションを格納する.rspecファイルと、テストデータを配置するspecフォルダ、設定他が格納されたspec_helper.rbが生成されました。
 ちなみに、上記ではbundleコマンドを実行していますが、execはGemFileコンテキスト内でコマンド(ここでは"rspec --init")を実行するだけなので、現在の環境であれば直接rspecを実行しても変わらないかと思います(GemFile.lockが更新されるのかもしれないけど知らない)。

準備3:.rspecファイルの更新

 .rspecファイルはデフォルトでは空になっています。下記の3行を追加します。

--color
--format documentation
--require spec_helper

 "color"は出力結果に色を付けるオプション、"format documentation"は、出力結果を整形するオプションです。どちらも好みで使用すれば良いかと。そしてspec_helper.rbをrequireしておきます。

準備4:spec_helper.rbファイルの更新

 spec_heper.rbについては、こちらの記事を参照して内部の"=begin/=end"を削除しています。

RailsじゃないRspec3環境を構築する方法
http://qiita.com/yusabana/items/db44b81bdddf6ed0e9f5

 ちゃんと確認したわけではありませんが、RSpecの前バージョンとの互換性を取る場合にコメントアウトする要素が多くあるように見えるので、今から始める場合は外した方がいいかなと思います。例えばこれによって、describeは"RSpec."を先頭に設定しないと機能しません(たぶん)。

 上記以外にも、rspecを使うにあたり当該記事が非常に参考になりました。ありがとうございました。

準備5:RakeFileの更新

 大量に作成したテストファイルをrakeコマンド一発で実行できるようにするために、RakeFileSpec用のタスクを追加します。

task :spec

require "rspec/core/rake_task"
RSpec::Core::RakeTask.new("spec")

 これで準備が全て整いました。コマンドプロンプトから以下のrakeコマンドを実行します。

rake spec

 するとspecタスクが実行されて、RSpec::Core::RakeTaskがrequireされ、"./spec/**/*_spec.rb"に該当するファイル(つまり、specフォルダ配下にあるxxxx_spec.rbというファイル全て)を実行します。ファイルの末尾が"_spec.rb"でないと反応しないので注意です。

テストコード

 前回のスタブによってキー入力情報をエミュレートするテストコードをRSpec用に書き直すと以下のようになります。

#spec/test_input_base_spec.rb
require 'spec_helper'
require 'dxruby'
require './system/Tsukasa.rb'

RSpec.describe Tsukasa::Control do

  it '2017_02_08_1_キー入力確認' do
    #DXRuby::Input.key_down?(Tsukasa::K_Z)にスタブを設定
    allow(DXRuby::Input).to receive(:key_down?).with(Tsukasa::K_Z).and_return(true)

    #コントロールの生成
    control = Tsukasa::Control.new() do
      #動的プロパティの追加
      _DEFINE_PROPERTY_ test: nil
      #無限ループ
      _LOOP_ do
        #Inputオブジェクトをモックのクラスを指定して生成
        _CREATE_ :Input, id: :input
        #zキーが押された場合
        _CHECK_ [:_ROOT_, :input], key_down: Tsukasa::K_Z do
          #プロパティに値を設定
          _SET_ test: Tsukasa::K_Z
          #メインループを終了する
          _EXIT_
        end
        #1フレ送る
        _HALT_
      end
    end
    #メインループ
    DXRuby::Window.loop() do
      control.update(DXRuby::Input.mouse_x, DXRuby::Input.mouse_y) #処理
      control.render(0, 0, DXRuby::Window) #描画
      break if control.exit #メインループ終了判定
    end

    #テスト
    expect(control.test).to eq(Tsukasa::K_Z)
  end
end

 コードの内容はほとんど変わりませんが、Minitestではスタブの有効範囲をブロックで規定していたので、そのネストが無い分スッキリしているように見えます。スコープを厳密化しているMinitestの方がテストコードにふさわしいようにも思えるし、どっちがいいかはケースバイケースかな……。司エンジンについては、暫くはMinitestとRSpecを平行して使い、決定的な差がなければRSpecに一本化する予定でいます。
 また、MinitestではInput.key_down?(Tsukasa::K_Z)が返す値を1フレーム毎に設定できたのですが、同じ事をRSpecで実現できるのかはまだ確認できていません。

 ではテストを実行しましょう。コマンドプロンプトからrspecを実行すると検証が行えます。

C:\data\tsukasa>rspec spec/test_input_base_spec.rb

Randomized with seed 32851

Tsukasa::Control
  2017_02_08_1_キー入力確認

Top 1 slowest examples (0.37025 seconds, 95.4% of total time):
  Tsukasa::Control 2017_02_08_1_キー入力確認
    0.37025 seconds ./spec/test_input_base_spec.rb:7

Finished in 0.38827 seconds (files took 0.9234 seconds to load)
1 example, 0 failures

Randomized with seed 32851

 spec_helper.rb内で乱数のシードが自動生成されているようです。この辺も今後は制御が必要かもしれません。
 rakeを使えば、テストコードをまとめて実行できます。ファイル名を"〜_spec.rb"にするのを忘れないようにしましょう(土屋がそこでハマったので)。

C:\data\tsukasa>rspec spec

おわりに

 導入手順だけでいっぱいになってしまいRSpec記法説明ができませんでした。まあそもそも土屋自身がわかっていないので、今後書いて行きたいと思います。
 さて、なんとか一通りユニットテスト環境の構築ができましたので、技術書典2用の原稿を書き始める予定でいます(本当はCIまでやる予定だったが時間がなさげ)。お楽しみに〜。

余談

 RSpecには静的解析機能でもあるのかRspec実行時にRubyが"-w"オプション付きで起動しているようで、テストを実行すると「インデントが一致してない」だとか「"-"が符号なのか演算子なのか明示させた方が良い」みたいな警告が(英語)で表示されました。司エンジンの実装については対応したのですが、以下のparslet内部で発生している警告はどうすればいいんでしょうかw

C:/Ruby22/lib/ruby/gems/2.2.0/gems/parslet-1.7.1/lib/parslet/atoms/base.rb:86: warning: assigned but unused variable - value
C:/Ruby22/lib/ruby/gems/2.2.0/gems/parslet-1.7.1/lib/parslet/atoms/lookahead.rb:28: warning: assigned but unused variable - value
C:/Ruby22/lib/ruby/gems/2.2.0/gems/parslet-1.7.1/lib/parslet/error_reporter/deepest.rb:67: warning: assigned but unused variable - rank
C:/Ruby22/lib/ruby/gems/2.2.0/gems/parslet-1.7.1/lib/parslet/transform.rb:134: warning: instance variable @__transform_rules not initialized

2017-02-09

rubyゲーム開発にユニットテスト/テスティングフレームワークを導入する(4)【モックの自動化(テストダブル)編】

 前回(http://d.hatena.ne.jp/t_tutiya/20170205/1486285294)、キー入力を管理するオブジェクト(正確にはモジュール)をエミュレートするモックオブジェクトを作成し、それを差し替えることで、実際にキー入力を行わなくても、同等のテストが可能であることを確認しました。

 多少込み入ったユニットテストを行う場合、こういうモックオブジェクトの作成が頻繁に起こります。毎回フルスクラッチするのは面倒なので自動化を支援する機能を使うことにします。このような機能を総称してテストダブルと言います。

テストダブル(via wikipedia
https://ja.wikipedia.org/wiki/%E3%83%86%E3%82%B9%E3%83%88%E3%83%80%E3%83%96%E3%83%AB

 ちなみにこれはスタントダブル(役者そっくりの格好をさせて画面に映るスタントマンのこと)のもじりかと思います。

 最初はRubyの有名なテストダブルフレームワークである"rr(http://rr.github.io/rr/)"を使うつもりだったのですが、ソースコードを見る限りどうもMinitest内で使用するのは非推奨ぽかったので、今回はMinitest自身が提供しているライブラリMinitest::Mockを使うことにしました。

 前回のコードをMinitest::Mockに置き換えたのが以下になります。自前のモックオブジェクトがなくなり、Input#key_down?だけをスタブ化(特定のメソッドが呼びだされたときにその動きをフックすること)しています。また、1フレームごとに返す値を明示できるのはテストとして使いやすいかもしれません。

  #ゲーム側で判定タイミングのトリガーを用意するテスト
  def test_2017_02_08_1_キー入力確認_minitest_mock
    #Procのような物を生成する
    test_module_mock = MiniTest::Mock.new
    #Input.key_down?(Tsukasa::K_Z)が実行された時に返す真偽値を設定する
    #1フレーム目:false
    test_module_mock.expect :call, false, [Tsukasa::K_Z]
    #2フレーム目:false
    test_module_mock.expect :call, false, [Tsukasa::K_Z]
    #3フレーム目:true←ここで_CHECK_が成立する
    test_module_mock.expect :call, true, [Tsukasa::K_Z]

    #スタブを設定する
    DXRuby::Input.stub(:key_down?,  test_module_mock) do
    
      #このブロック内でInput.key_down?が実行されるとtest_module_mock#callが呼びだされる
    
      puts "zキーを押してください"
      #コントロールの生成
      control = Tsukasa::Control.new() do
        #動的プロパティの追加
        _DEFINE_PROPERTY_ test: nil
        #無限ループ
        _LOOP_ do
          #Inputオブジェクトをモックのクラスを指定して生成
          _CREATE_ :Input, id: :input
          #zキーが押された場合
          _CHECK_ [:_ROOT_, :input], key_down: Tsukasa::K_Z do
            #プロパティに値を設定
            _SET_ test: Tsukasa::K_Z
            #メインループを終了する
            _EXIT_
          end
          #1フレ送る
          _HALT_
        end
      end
      #メインループ
      DXRuby::Window.loop() do
        control.update(DXRuby::Input.mouse_x, DXRuby::Input.mouse_y) #処理
        control.render(0, 0, DXRuby::Window) #描画
        break if control.exit #メインループ終了判定
      end

      assert_equal(control.test, Tsukasa::K_Z)
    end

    #テスト
  end

 ただ、テストコードとして分かりやすいのか? というのは正直疑問です。Minitestのモックは実装がミニマムなため、凝ったことをするとコードが複雑になる傾向があるようです。また、stubのようなメソッドをを、MinitestがObjectを直接拡張して実現しているのも、ちょっとやりすぎではないかと感じました。

 次はRubyにおけるテストフレームワークデファクトスタンダードであるRSpecを使ってみる予定です。

補足:":call"とはなんぞや、という話(知りたい人向けのマニアックな話)

 今回やってみたモジュールメソッドにstubを設定する方法については、こちらの記事を参考にさせてもらいました。

minitestでモジュールメソッドにstubを使う
http://qiita.com/matsukaz/items/6616d6f18e98108e9207

 上のコードにおいて、スタブのブロック内でInput.key_down?が実行されると、スタブが機能してtest_module_mock#callが呼ばれ、それによってexpectした結果が返るのですが、そもそもこのcallってなんなのでしょうか?

 つまる所これはProc#callなのです。上記ではわざわざMiniTest::Mockを生成していますが、stubの引数に直接Procを設定しても同じように動作するわけです。むしろ、本来はProc#callが呼ばれるべき箇所を、こちらが無理矢理ハックしていると言った方が正確かもしれません。

 実際、Minitestの作者であるリャン・デイビスは、こういう時は直接Procを作ると土屋にメンションをくれました(フットワーク軽いなリャン!)

https://twitter.com/the_zenspider/status/829609382128209920

2017-02-06

メッセージ指向ゲーム開発言語「司エンジン」v2.2b11リリース

・Input#_INPUT_API_を追加
・Inputテストコードの追加
・Windowクラスの継承元をClickableLayoutからControlに変更し、Clickableをincludeする形式にした。
・ClickableでLayoutableをincludeするようにした(考慮漏れ)
リファクタリング
・ドキュメント整備

v2.2b11リリース

・v2.2バレンタインリリースに向けて最後の追い込みです。

Input#_INPUT_API_を追加

・DXRuby::Inputをモックオブジェクトに切り替えられる機構を追加しました。
・理想的にはrr(test duble)を使えるようにしたいんですが、モジュールのスタブを作る方法が上手く探せなかったので、rrの採用は2.3に送る予定です。

2017-02-05

rubyゲーム開発にユニットテスト/テスティングフレームワークを導入する(3)【I/Oモックの作成編】

 技術書典2に合わせて、以前書いたゲーム開発にユニットテストを導入する手法についてまとめた本を作るつもりでいます。追加として「I/Oモックの作成」「CIサービスへの登録」までできればいいなあと思っています(CIサービスまでは間に合わないかもしれない)。今回はI/Oモックについて。

I/Oモックとはなにか

 ゲームプログラムユニットテストを行う際には、ネックとなる部分が幾つか(あるいは大量に)ありまして、その中の一つに「ユーザーのキー入力に応じて行われる処理のテスト」があります。テストを実行した人が、実際に必要なキー入力を行い、その結果を確認すればいいのですが、これだと自動テストになりません。

 こういう時は、キー入力処理をラップし、実際のI/Oの代わりに必要な値を返すモックオブジェクトを用意します。司エンジンではキー入力にDXRuby::Inputを使っているので、これのラッパークラスを用意し、テスト時のみラッパークラスの方を使用する形にします。v2.2からこの機構が組み込まれます。

コードサンプル

 2.2で実際に動作するテストコードはこちらになります。

MiniTest.autorun

#テスト用DXRuby::Inputエミュレートクラス
class TestInput
  #DXRuby::Input.key_down?をフックする……※
  def self.key_down?(pad_code)
    #引数でK_Z(Zキー)が指定された場合はtrueを返す
    if pad_code == Tsukasa::K_Z
      return true
    else
      return false
    end
  end

  #フックされていないメソッドについてはDXRuby::Inputのメソッドをそのまま渡す
  def self.method_missing(command_name, options)
    return DXRuby::Input.send(command_name, options)
  end
end

class TestInputBase < Minitest::Test
  #ゲーム側で判定タイミングのトリガーを用意するテスト
  def test_2017_01_09_1_キー入力確認
    puts "zキーを押してください"
    #コントロールの生成
    control = Tsukasa::Control.new() do
      #動的プロパティの追加
      _DEFINE_PROPERTY_ test: nil
      #無限ループ
      _LOOP_ do
        #Inputオブジェクトをモックのクラスを指定して生成……※
        _CREATE_ :Input, id: :input, _INPUT_API_: TestInput
        #zキーが押された場合
        _CHECK_ [:_ROOT_, :input], key_down: Tsukasa::K_Z do
          #プロパティに値を設定
          _SET_ test: Tsukasa::K_Z
          #メインループを終了する
          _EXIT_
        end
        #1フレ送る
        _HALT_
      end
    end

    #メインループ
    DXRuby::Window.loop() do
      control.update(DXRuby::Input.mouse_x, DXRuby::Input.mouse_y) #処理
      control.render(0, 0, DXRuby::Window) #描画
      break if control.exit #メインループ終了判定
    end

    #テスト
    assert_equal(control.test, Tsukasa::K_Z)
  end
end

 2箇所だけ解説しておきます。

#Inputオブジェクトをモックのクラスを指定して生成……※
_CREATE_ :Input, id: :input, _INPUT_API_: TestInput

 Inputコントロールを生成する際、_INPUT_API_オプションにDXRuby::Input互換クラスを指定すると、キー入力判定時にそちらのクラスを実行します。省略時(つまり、通常時)はDXRuby::Inputが使用されます。

  #DXRuby::Input.key_down?をフックする……※
  def self.key_down?(pad_code)
    #引数でK_Z(Zキー)が指定された場合はtrueを返す
    if pad_code == Tsukasa::K_Z
      return true
    else
      return false
    end
  end

 モック側では想定される値を返します。このコードでは固定値になっていますが、Dataコントロールなどの値を見て、任意のタイミングで必要な値を返せます。現状ではまだちょっと使いにくいので、もっと簡単にキー入力をエミュレートする方法を考えようと思っています。
 DXRuby::Input以外にもモックを作れるようにするかは現在検討中です。DXRuby::Windowとかは作れるけど、あんまり意味ないかな……?

NEXT

 想像していたよりサクっと組めてしまって実は驚いています。あとはCIサービスで動けばもうなんでもできるな!(ホントかよ)