Xcode4でのユニットテストもGHUnitが良い

Xcode3まではGHUnitを利用していたが、Xcode4でユニットテストの実行が簡単になって、IDEでのサポートも増し、今回担当したプロジェクトでは新規でのXcode4での作業だったということもあり、下記の理由で標準のSenTestingKitを使ってみた。

  • プロジェクトを作成する時に自動的にユニットテストが組み込める
  • Command+Uですぐに実行できる
  • 実行結果がわかりやすくなった
  • 標準でデバッガを利用したデバッグが出来るようになった

使ってみたところ、IDEでのサポートが増したおかげで、ユニットテストが行いやすくなったのは実感できた。しかしいくつか不満がある。

  • 実行結果がオールグリーンの場合は何も表示されない
  • 非同期機能のテストが標準で行えない
  • アサーションの種類が少ない

といった点だ。どれもユニットテスにとって重要な点だ。

テストに失敗すると失敗個所が表示されるので良いのだが、すべて成功すると成功とは表示してくれない。いつ終了したのかも分かりにくい。ユニットテストっていうのはオールグリーンを確認することが重要だと気付いた。

今回のプロジェクトではNSURLConnectionを使用するような非同期機能は無かったので非同期機能のテストは行わなかったが、今後予定しているプロジェクトでは、SenTestingKitは標準では非同期テストが行えないので、GHUnitを利用して非同期機能のテストを行うことが必要となる。

アサーションの種類が多い方がテストを記述しやすく、分かりやすさも向上する。

結局のところ組み込みはプロジェクト作成時に一度行うだけで良いし、Command+Uでの実行もテストを行う時にターゲットを切り替えてテストを集中的に行えばいいので、GHUnitを使っていく方が良いという判断になった。

GHUnitで自動実行する方法

現在GHUnitとiUnitTestを試しています。iUnitTestを試したので次にGHUnitを試しています。

iUnitTestはそのままでテストが自動的に実行されますが、GHUnitは設定が必要でした。のでメモしておきます。

GHUnitでの自動実行の方法はXcodeで作成したGHUnitアプリケーションの実行可能ファイルに環境変数を付与します。

GHUnitアプリケーションを選択して「情報を見る」メニューで情報を表示します。引数タブを選択します。環境に設定される変数にGHUNIT_AUTORUNをYESで設定します。これで、アプリケーションを起動すればテストが自動実行されました。
GHUnit-UnitTestsInfo.png

Sinatraの使い方を調べつつアプリのテンプレートを作ってみた

Sinatraの使い方を学びつつ、ついでにKomodo IDEで使うSinatraアプリのテンプレートを作成してみます。

Sinatraアプリの原形とセッション&Flashメッセージの組み込み

とりあえずSinatraのインストールから。

$ sudo gem install sinatra
Successfully installed rack-1.0.1
Successfully installed sinatra-0.9.4


SinatraとRackがインストールされました。

アプリのオートリロードするためにShotgunをインストール。

$ sudo gem install shotgun

セッションはRackのセッションを利用し、nakajima/rack-flash @ GitHubでRails風のFlashメッセージを利用します。

$ sudo gem install rack-flash

Sinatraアプリ本体をapp.rbとして作成。
RackのセッションとFlashメッセージを利用できるように設定。
トップページでFlashメッセージをセットして、リンク先/helloでメッセージを表示します。

# -*- encoding: UTF-8 -*-
require 'sinatra'
require 'rack/flash'
 
configure do
  use Rack::Session::Cookie,
    #:key => 'rack.session',
    #:domain => 'example.com',
    #:path => '/',
    :expire_after => 3600,
    :secret => 'IgR~tKW4YxEvzSHqp^uArcmbFeJklf5dNVQDwCUhOosyiMZTGj'
  use Rack::Flash
end
 
get '/' do
  flash[:notice] = "Hello world!"
  "<a href='/hello'>Hello world!</a>"
end
 
get '/hello' do
  flash[:notice]
end

$ shotgun app.rb


でアプリを実行して、http://localhost:9393にアクセスして、動作を確認。

したのは良いんですけど、shotgunを終了したらゾンビになってしまいました。
のでSinatraでshotgunの代わりにRack::Reloaderを使う方法 – Hello, world! – s21gを参考に

configure :development do
  class Sinatra::Reloader < Rack::Reloader
    def safe_load(file, mtime, stderr = $stderr)
      ::Sinatra::Application.reset!
      use_in_file_templates! file
      stderr.puts "#{self.class}: reseting routes"
      super
    end
  end
  use Sinatra::Reloader
end


に変更して、

$ ruby app.rb

として、リロードされるようにしました。

HamlとSassを組み込む

HamlとSassをインストール。

$ sudo gem install haml

viewsディレクトリを作成します。

ページレイアウト用にviews/layout.hamlを作成します。
タイトルを表示してflashメッセージを表示するコード。

!!! XML
!!! Strict
 
%html
  %head
    %title Sinatra Template
    %link{:href => "/styles.css", :rel => "stylesheet", :type => "text/css", :media => "screen"}
  %body
    #branding
      %h1 Sinatra Template
    - if flash[:notice]
      %p.notice= flash['notice']
    - if flash[:error]
      %p.error_notice= flash['error']
 
    #content
      != yield

indexページ用にviews/index.hamlを作成します。

%form{:method => 'POST', :action => '/hello'}
  %input{:type => 'submit', :value => 'Say hello!'}

app.rbにこれらを表示するためのコードを追加&変更。

require 'haml'
get '/' do
  haml :index
end
 
post '/hello' do
  flash[:notice] = "Hello world!"
  redirect '/hello'
end
 
get '/hello' do
  flash[:notice]
end

indexページのSay hello!ボタンをクリックすると/helloにPOSTします。さらにそのPOSTでFlashメッセージをセットして、/helloへリダイレクト。/helloでFlashメッセージが表示されます、という内容。

次にSass。
views/styles.sassを作成。内容はあとでリセット入れたり自分用にカスタマイズするとして、今は動作検証のために簡単なコードに。

body
  :font
    :size 1.5em

app.rbにスタイルシートへアクセスするコードを追加。

get '/*.css' do |path|
  content_type 'text/css', :charset => 'utf-8'
  sass path.to_sym, :sass => {:load_paths => options.views}
end

で、動作確認。期待した通りページが表示されました。

ORM => DataMapperの組み込み

ORMはDataMapperを利用します。

$ sudo gem install datamapper
$ sudo gem install do_sqlite3

app.rbにDataMapperの設定を記述。
データベースにはSQLiteを使用。developmentではmemory、productionではファイルを使用します。

require 'dm-core'
require 'logger'
 
configure :development do
  # リロード設定が記述してあります。
  DataMapper.setup(:default, 'sqlite3::memory:')
  DataMapper::Logger.new(STDOUT, :debug)
end
 
configure :production do
  DataMapper.setup(:default, 'sqlite3:production.db')
end

ユーザ認証はどうしましょうか。maxjustus’s sinatra-authentication at master – GitHubとかsubbarao’s sinatra-openid at master – GitHubが便利そうで試してみたんですか、どちらもうまく動かず。DataMapperのプロパティのタイプにBCryptHashがあるので、それを利用して自作するのが簡単かな。とりあえず保留。

テスト環境の導入

テストにはShouldaとCucumber+Webratを利用しようとおもいます。

まずはShoulda。一緒にrack-testも。

$ sudo gem install shoulda
$ sudo gem install rack-test


testディレクトリを作成。

Testing
Sinatra tests can be written using any Rack-based testing library or framework. Rack::Test is recommended:

Sinatra: README

を参考にtest/app_test.rbを作成。

require File.join( File.dirname(__FILE__), '../app.rb')
require 'test/unit'
require 'shoulda'
require 'rack/test'
 
class AppTest < Test::Unit::TestCase
  include Rack::Test::Methods
 
  def app
    Sinatra::Application
  end
 
  context "Access pages" do
    should "show index" do
      get '/'
      assert_match 'Say hello!', last_response.body
    end
 
    should "show hello" do
      post '/hello'
      follow_redirect!
      assert_match 'Hello world!', last_response.body
    end
  end
end

次にCucumber。

$ sudo gem install cucumbe webrat


features, featuers/support, features/step_definitionsディレクトリを作成しました。

cucumber.ymlを「default: –language ja features」という内容で作成しました。

features/support/env.rbを作成しました。

# -*- encoding: UTF-8 -*-
app_file = File.join(File.dirname(__FILE__), '../../app.rb')
require app_file
Sinatra::Application.app_file = app_file
 
require 'test/unit/assertions'
require 'shoulda'
require 'rack/test'
require 'webrat'
 
Encoding.default_external = 'UTF-8'
 
Webrat.configure do |config|
  config.mode = :rack
  config.application_framework = :sinatra
  config.application_port = 4567
end
 
World(Test::Unit::Assertions) do
  def app
    Sinatra::Application
  end
 
  include Rack::Test::Methods
  include Webrat::Methods
  include Webrat::Matchers
end
 
Before do
  DataMapper.auto_migrate!
end

features/step_definitions/webrat_steps.rbは他のシステムで利用しているものをコピー。

問題はfeatures/step_definitions/result_steps.rb。これまではRSpecを利用していたのですが、今回はtest/unitを使うので、xpathの検証とかはどうするかなど。とりあえず動いたコードがこれ。

# -*- encoding: UTF-8 -*-
 
ならば /^"(.*)"と表示される$/ do |text|
  assert_match(/#{text}/m, @response.body)
end
 
ならば /^"(.*)"と表示されない$/ do |text|
  assert_no_match(/#{text}/m, @response.body)
end
 
ならば /^(\w+)メッセージが表示さる$/ do |message_type|
  assert_not_equal 0, Nokogiri::HTML.parse(@response.body).search("//*[@class='#{message_type}']").length
end
 
ならば /^(.*)リクエストが失敗する/ do |_|
  assert_not_equal 200, @response.status
end
 
ならば /ページ読み込みが成功する/ do
  assert_equal 200, @response.status
end


xpathの検査がかっこわるい…。良い方法があったらコメントください。

Rakefileは以下の内容にしました。

require 'cucumber/rake/task'
 
desc 'Run all tests'
task :test do
   require 'rake/testtask'
   Rake::TestTask.new do |t|
      t.test_files = FileList[File.join('test', '**', '*_test.rb')]
   end
end
 
desc "Run all features in features directory"
Cucumber::Rake::Task.new(:features)

あとは

  • Sassを完成させる
  • rack cacheの導入
  • モデル作成したときのためのFixtureの準備
  • configディレクトリを作成して設定ファイルをそこに置くか
  • publicディレクトリの作成
  • ほかいろいろ…

をやったりやらなかったり。

そしてKomodo IDEでプロジェクトテンプレートを作成。

Shouldaが良さそう

これまでRSpec使ってたけど、調べてみたらthoughtbot’s shoulda at master – GitHubが良さそうな気が。

Shoulda(もっと早く使っていれば)by 達人プログラマ。Rubyのテストには、RSpecもいいけど、Shouldaが良いよ。 « kentaroi’s weblog
RSpecよりShoulda、fixturesよりヘルパーとMocha – Unexplored Rails
Shoulda Roundup: Elegant, Maintainable Ruby Testing

を参考にして使ってみよう思う。

Cucumber+WebratでPHPアプリのテストをする

sudo gem install cucumber mechanize rspec webratPHP – cucumber – GitHub

を参考にCucumberでPHPアプリケーションの受け入れテストを作成・実行してみます。さらにこのチュートリアルでは日本語でテストを記述してみます。

Cucumberのインストール

Ruby 1.9.1をすでにインストールしてある環境です。まずはCucumber・Webrat・mechanize・rspecをインストールします。

$ sudo sudo gem install cucumber mechanize rspec webrat

ディレクトリの作成

テストするPHPアプリのトップディレクトリに以下のディレクトリを作成します。

features
features/support
features/step_definitions

環境設定ファイルの作成

features/support/env.rbを以下の内容で作成します。RSpecやWebratを読み込んで、各種設定を行っています。Seleniumを利用するといった場合はこのファイルを編集して設定を変更します。

# RSpec
require 'spec/expectations'
 
# Webrat
require 'webrat'
 
require 'test/unit/assertions'
World(Test::Unit::Assertions)
 
Webrat.configure do |config|
  config.mode = :mechanize
end
 
World do
  session = Webrat::Session.new
  session.extend(Webrat::Methods)
  session.extend(Webrat::Matchers)
  session
end

Cucumberのコマンドライン引数の設定

Cucumberのコマンドライン引数のデフォルトを設定するために以下の内容でルートディレクトリにcucumber.ymlを作成します。–language jaを指定することで日本語でテストを記述できるようになります。以下のdefaultではコマンドラインに結果が出力されますが、結果をHTMLで出力する設定を記述しておき、テスト実行時に適宜設定を指定することができます。

default: --language ja features

ステップの作成

features/step_definitionsディレクトリにwebrat_steps.rbとresult_steps.rbの2つのテストステップファイルを作成します。

features/step_definitions/webrat_steps.rb
Webratでページにアクセスしたりボタンをクリックしたりできるステップを作成します。

# -*- encoding: UTF-8 -*-
 
前提 /^(.*)ページを表示している$/ do |path|
  @response = visit "http://localhost/php_with_cucumber#{path}"
end
 
もし /^(.*)ページを表示する$/ do |path|
  @response = visit "http://localhost/php_with_cucumber#{path}"
end
 
もし /^"(.*)"ボタンをクリックする$/ do |button|
  @response = click_button(button)
end
 
もし /^"(.*)"リンクをクリックする$/ do |link|
  @response = click_link(link)
end
 
もし /^"(.*)""(.*)"と入力する$/ do |field, value|
  @response = fill_in(field, :with => value) 
end
 
もし /^"(.*)"から"(.*)"を選択する$/ do |field, value|
  @response = select(value, :from => field) 
end
 
もし /^"(.*)"をチェックする$/ do |field|
  @response = check(field) 
end
 
もし /^"(.*)"のチェックを外す$/ do |field|
  @response = uncheck(field) 
end
 
もし /^"(.*)"を選択する$/ do |field|
  @response = choose(field)
end
 
もし /^パスが"(.*)"のファイルを"(.*)"に添付する $/ do |path, field|
  @response = attach_file(field, path)
end

ページの表示ではhttp://localhost/php_with_cucumberを直に書き込んで、テストを記述するfeatureファイルではアドレスを省略できるようにしています。

features/step_definitions/result_steps.rb
結果の検査を行うステップを作成します。

# -*- encoding: UTF-8 -*-
 
ならば /^"(.*)"と表示される$/ do |text|
  response_body.to_s.force_encoding("UTF-8").should =~ /#{text}/m
end
 
ならば /^"(.*)"と表示されない$/ do |text|
  response_body.to_s.force_encoding("UTF-8").should_not =~ /#{text}/m
end
 
ならば /^(\w+)メッセージが表示さる$/ do |message_type|
  @response.should have_xpath("//*[@class='#{message_type}']")
end
 
ならば /^(.*)リクエストが失敗する/ do |_|
  @response.should_not be_successful
end
 
ならば /ページ読み込みが成功する/ do
  @response.code.should == "200"
end

今回のテストでは使用しないステップも含まれています。実際に動作するか検証してませんが、こんな感じで作成するということで参考にしてください。

テスト対象のコード

実際にはテストを記述してコードを作成していくかもしれませんが、今回はすでに作成した以下のPHPコードをindex.phpとして保存してテストしてみます。機能はテキストエリアにテキストを入力してSubmitをクリックすると、入力したテキストが表示されるというものです。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">
 
<head>
    <title>PHP with Cucumber</title>
</head>
<body>
    <h1>PHP with Cucumber</h1>
 
    <?php if(!isset($_GET['text']) or empty($_GET['text'])) print '<p>テキストを入力してください。</p>'; ?>
 
    <form action="/php_with_cucumber/index.php" method="get">
        <p><textarea name="text" rows="10" cols="100"></textarea></p>
        <p><input type="submit" value="Submit" /></p>
    </form>
 
    <p class="printed_text"><?php if(isset($_GET['text'])) print $_GET['text']; ?></p>
 
    <p><a href="./">リセット</a></p>
</body>
</html>

テストの作成

テストを作成します。featuresディレクトリにshow.featureファイルを作成します。ページの表示・結果の表示・表示のリセットをテストします。

機能: トップページの表示とサブミット
 
  シナリオ: トップページの表示
    もし /ページを表示する
    ならば ""と表示される
    かつ "テキストを入力してください。"と表示される
 
  シナリオ: テキストの入力と結果の表示してリセットする
    前提 /ページを表示している
    もし "text"に"テスト用のテキストです。"と入力する
    かつ "submit"ボタンをクリックする
    ならば "テスト用のテキストです。"と表示される
    かつ "テキストを入力してください。"と表示されない
    もし "リセット"リンクをクリックする
    ならば "テキストを入力してください。"と表示される

もっと機能があれば機能ごとにファイルを分割します。またこの例では、テスト内容は不十分ですので、ステップやシナリオを追加して試してみてください。

テストの実行

ではテストを実行してみます。コマンドラインでルートディレクトリに移動してcucumberを実行します。

$ cucumber
Using the default profile...
機能: トップページの表示とサブミット
 
  シナリオ: トップページの表示                                # features/show.feature:3
    もし /ページを表示する                                 # features/step_definitions/webrat_steps.rb:7
    ならば ""と表示される # features/step_definitions/result_steps.rb:3
    かつ "テキストを入力してください。"と表示される                    # features/step_definitions/result_steps.rb:3
 
  シナリオ: テキストの入力と結果の表示してリセットする     # features/show.feature:8
    前提 /ページを表示している                # features/step_definitions/webrat_steps.rb:3
    もし "text"に"テスト用のテキストです。"と入力する # features/step_definitions/webrat_steps.rb:19
    かつ "submit"ボタンをクリックする         # features/step_definitions/webrat_steps.rb:11
    ならば "テスト用のテキストです。"と表示される      # features/step_definitions/result_steps.rb:3
    かつ "テキストを入力してください。"と表示されない    # features/step_definitions/result_steps.rb:7
    もし "リセット"リンクをクリックする           # features/step_definitions/webrat_steps.rb:15
    ならば ページ読み込みが成功する              # features/step_definitions/result_steps.rb:19
    かつ "テキストを入力してください。"と表示される     # features/step_definitions/result_steps.rb:3
 
2 scenarios (2 passed)
11 steps (11 passed)
0m0.029s

すべてのシナリオがパスしました。ここで、たとえば「ならば “テスト用のテキストです。”と表示される」を「ならば “テストのテキストです。”と表示される」に変更してCucumberを実行してみると、11 steps (1 failed, 4 skipped, 6 passed)となります。コマンドラインではカラーで表示されるので、真っ赤な画面だったらfailedで、緑だったらpassedです。

おまけ

作成したファイルの書庫: php_with_cucumber.zip

最終的なディレクトリ・ファイル構成

.
|-- cucumber.yml
|-- features
|   |-- show.feature
|   |-- step_definitions
|   |   |-- result_steps.rb
|   |   `-- webrat_steps.rb
|   `-- support
|       `-- env.rb
`-- index.php