Help us understand the problem. What is going on with this article?

ゲームっぽい何かを作りながらRubyを勉強する その1 〜ディレクトリ構成とかtest-unitとかRakeとか〜

More than 3 years have passed since last update.

はじめに

以前Rubyに興味を持って文法などを調べたはいいものの、そのときはRubyで何かひとつのプログラムを書くまでには至らなかったので、気を取り直してEffective Rubyを片手に、ゲームっぽい何かのプログラムを試行錯誤しながら作ってみています。この記事では、その過程で得られた知見などを雑多にご紹介します。

勉強の目的が強いので、「最初はGemもRakeも何も使わず、ゼロからディレクトリ作ってファイル作って動かしてみる。必要性に応じてそれらを取り入れてみる」というスタンスでやってみています。それによって、RakeやGemが何のために存在していて、Rubyエンジニアはなぜそれらを使うのかを理解するのが狙いです。

この記事は、僕のように「とりあえずRubyはじめてみたけど、GemとかRakeとか併せて覚えることがいっぱいあって大変!」「プロジェクトのディレクトり構成とかどうするのが良いんだろう、いつまでも直下にソースファイル増殖させるわけにもいかないし。。。」といった悩みにとらわれて次の一歩がうまく踏み出せない方向けの記事です。もうバリバリRubyでお仕事されている方にはあまり役に立たないかと思いますので、ご了承ください。また、この記事ではRailsも一切登場しません。併せてご了承ください。

なお、この記事の成果(というほどでもありませんが、、、)は以下のGitHubリポジトリで公開しています。
https://github.com/chooyan-eng/mygame

(2016/11/14 修正)
公開時、ユニットテストの種類をMinitestと書いていましたが、完全に僕の勘違いで、正しくはtest-unitでした。@jnchitoさん、ご指摘&編集リクエストありがとうございます!勉強します。
(/修正)

シリーズ一覧

本文

とりあえずディレクトリ構成を作ってみる

ゼロから作る、というと、まず悩むのがディレクトリ構成です。以前Javaでcommanderを作った時は、Gradleでビルドするということを決めていたのでGradleの作法に従って作ればよかったのですが、今回は全くのゼロからのスタートです。

とりあえず、Rubyに限らずプログラムに必要なのはソースコードとテストコード、ということで、以下のようにsrctestのディレクトりを作るところから始めてみました。

$ mkdir src test 
$ tree
.
├── src
└── test

一般的なディレクトリ構成はまだよくわからないですが、とりあえずこれで進めて、いろいろ調べているうちに何か分かったら変えていく、という作戦で進めてみたいと思います。

とりあえずセーブ機能を作ってみる

ゲームと言ったらデータのセーブ、ということで、まずはファイルにデータを保存するプログラムを書いてみます。
とりあえず、ファイル名と保存対象の文字列を受け取って、その通りにファイルを保存するところまで。

src直下にsave_manager.rbを作ります。

save_manager.rb
module MyGame
  # module for managing game data
  module DataManager
    def self.save(file_name, data)
      File.open(file_name, 'w') do |f|
        f.puts(data)
      end
    end
  end
end

動作確認はまだしません。
main的なプログラムを書いてとりあえず動かしてみるのではなく、後述するtest-unitを使って、テストプログラムから動作を検証する、という方法で確認します。

テストコードをtest-unitで書いてみる

ソースを書いたら(もしくは書こうと思ったら)テストを書いて動作確認する、ということで、Ruby標準のtest-unitでとりあえずユニットテストを書いてみます。テストコードに慣れるためにも、極力main関数的なもので動作確認、というのは控えます。

ここでは以下の記事を参考にしています。
Ruby標準のテスティングフレームワークで手軽にテストコードを書く方法 - Qiita

save_manager_test.rb
require 'test/unit'
require '../src/save_manager.rb'

class TestSample < Test::Unit::TestCase
  def test_greeting
    MyGame::DataManager.save('sampledata.txt', 'hello sample data!')
  end
end
$ ruby save_manager_test.rb
Loaded suite save_manager_test
Started
.

Finished in 0.001633499 seconds.
-------------------------------------------------------------------
1 tests, 0 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
-------------------------------------------------------------------
612.18 tests/s, 0.00 assertions/s

うんうん、どうやら動いていそうです。テストコードを実行すると、セーブデータ(になる予定)のテキストファイルもできていました。1

$ ls -l
合計 8
-rw-rw-r--. 1 vagrant vagrant  19  9月 24 21:04 sampledata.txt
-rw-rw-r--. 1 vagrant vagrant 193  9月 24 21:01 save_manager_test.rb

Rakefileでテストを実行する

さて、ここで一つ疑問が出てきます。

とりあえずテストコードを1ファイル作りましたが、実装が進むにつれてこれが2ファイル、3ファイルと増えてきた場合、どのようにして一括で実行すれば良いのでしょうか。さすがに1ファイルずつ実行するのは面倒です。

と思いながらEffective Rubyを読んでいたら、ちょうどその答と言える内容が書いていました。

つまり、Rakeを使う、という方法です。

RakeはRuby製のビルド管理ツールだそうで、JavaのGradleやMavenなどに相当する仕組みのようです。ビルド管理ツールですので、test-unitの実行以外にもいろいろできるみたいですが、とりあえず今はテストの実行だけ、Rakeでできるようにしてみます。

Effective Ruby の項目36を見ると、Rakeの設定はRakefileというファイルに書いていくそうです。で、testディレクトリ以下にあるすべてのテストコードの実行は、以下のように記述します。2

Rakefile
require('rake/testtask')

Rake::TestTask.new do |t| 
  t.test_files = FileList['**/*_test.rb']
  t.warning = true
end

Rakefileを保存したら、rake testで実行します。
が、この時点ではまだテストは失敗します。テストコードであるsave_manager_test.rbrequireの書き方に問題があるのですが、この点については次の項目で説明します。

require で指定するパスを改善する

前述の通り、今の状態でrake testを実行すると、以下のようなエラーが出てテストコードが失敗してしまいます。

$ rake test
/home/vagrant/.rbenv/versions/2.3.1/lib/ruby/2.3.0/rubygems/core_ext/kernel_require.rb:55:in `require': cannot load such file -- ../src/save_manager.rb (LoadError)
    from /home/vagrant/.rbenv/versions/2.3.1/lib/ruby/2.3.0/rubygems/core_ext/kernel_require.rb:55:in `require'

...省略

rake aborted!
Command failed with status (1): [ruby -w -I"lib" -I"/home/vagrant/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rake-11.2.2/lib" "/home/vagrant/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rake-11.2.2/lib/rake/rake_test_loader.rb" "test/save_manager_test.rb" ]

Tasks: TOP => test
(See full trace by running task with --trace)

どうやら「../src/save_manager.rbなんてファイルないよ」、ということだそうです。でもさっきruby save_manager_test.rbで実行したときはそんなこと言われず、save_manager.rbもちゃんとrequireできていたのに、と思って試行錯誤していると、以下のことがわかりました。

  • testディレクトリ内でruby save_manager_test.rbと実行した場合は成功する
  • ホームディレクトリでruby test/save_manager_test.rbと実行すると、cannot load such file -- ../src/save_manager.rbと言われて失敗する(rake testの時と同じエラー)
    • というわけで、requireのパス指定は、実行時のカレントディレクトりからの相対パスになっているっぽい
  • 試しにrequire './src/save_manager.rb'のように書き換え、ホームディレクトリで実行すると正常にテストが成功する

まとめると、requireは、実行時のディレクトリからの相対パスでファイルを探しにいっているようです。
正確には、LOAD_PATHが通ったところからのパスで探すことと、普通にRubyプログラムを実行した場合はカレントディレクトリにLOAD_PATHが通る、ということなどが以下の記事にまとめられていました。

Rubyにおけるロードパス(require)のtips

さて、記事に書かれている通り、"./src"から指定するのもなんか間抜けなので、 -I オプションを使って require(save_manager.rb)と書けば良いように修正してみます。

まずはsave_manager_test.rbrequireを以下のように修正し、

save_manager_test.rb
require 'test/unit'
require 'save_manager.rb'

class TestSample < Test::Unit::TestCase

...以下略

実行は以下のようにします。

$ ruby -I ./src ./test/io/save_manager_test.rb

ただ、これだとまだRakeの実行時にパスが指定できていないので、rake testでも同様に実行できるよう、Rakefileにt.libs << './src'の行を追加します。

Rakefile
require('rake/testtask')

Rake::TestTask.new do |t|
  t.libs << './src'
  t.test_files = FileList['**/*_test.rb']

...以下略

これでrake testで実行したときもちゃんとsrc/save_manager.rbを見つけてくれるようになりました。

ついでに、requireする際に.rbの拡張子がつくのも若干格好悪いと思って調べたところ、requireでは拡張子は省略可能ということがわかりましたので、一緒に修正します。

結果、save_manager_test.rbrequire部分は以下のようになりました。

save_manager_test.rb
require 'test/unit'
require 'save_manager'

...以下略

ちなみに修正前は以下です。

save_manager_test.rb
require 'test/unit'
require '../src/save_manager.rb'

...以下略

全然違いますね。
この変更は見た目のすっきりさ以外にも大きな利点があります。

修正前のように「このファイルからの相対パス」で書いてしまうと、今後ディレクトリ構成が変わったりファイルを別のディレクトリに移動したりしたときにrequireの記述まで変えなくてはならない、という事態が発生します。

しかし、修正後のように「(設定で指定した)ソースディレクトリからの相対パス」としておくことで、テストファイルがどこにあっても変更なしで実行できるようになるのです。(ソースファイルが移動されたらさすがに要修正ですが。。。)

まとめ

ひとまずここまで、ソースコードとテストコードを書いて、テストを実行する、という土台が出来上がりました。まだまだゲームの完成には程遠いですが(というかどんなゲームにするのかも定かではないですが)、引き続き心の赴くままに作ってみて、分かったことがあればまとめていければと思います。

宣伝

記事中でもちょろっと出てきましたが、先日、同じような試みをJavaでもやりました。
こちらは"commander"という名前でとりあえず形になっていますので、Javaに興味のある方は見てみてください。

commander というコマンド実行履歴分析ツールを作りました



  1. この状態では保存場所がイケていないのと、テスト項目がこれで良いのか、という問題があります。これについては次回以降で修正予定です。 

  2. 実はこれだとtestディレクトリだけでなく、他のディレクトリに保存したxxx_test.rbも実行してしまします。この問題の解消方法も次回以降で検討します。 

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away