土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

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

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

準備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コマンド一発で実行できるようにするために、RakeFileにSpec用のタスクを追加します。

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