連載
» 2011年12月22日 16時04分 UPDATE

Railsで目指せ、情熱エンジニア(8):実例で学ぶRailsアプリのテスト方法

前回はRailsで使われるテストフレームワークをご紹介しました。今回は具体的なWebアプリを例に、簡単なテストを使ったリファクタリングについて解説します

[井上真,New Bamboo]

インテグレーションテストのために「Cucumber」を利用する

 前回は、Railsで使われるテストフレームワークをご紹介しました。今回から、いよいよ実際のテストを書きます。ただ書くだけでは物足りないので、前々回の連載で指摘したコードレビューの結果から、リファクタリングの候補をリストアップし、テストを書きながら1つ1つつぶしていきましょう。

  1. bitlyの設定はサーバの立ち上げ時にするべき
  2. 重複したコード
  3. 本来モデルにあるべきロジックがコントローラにある
  4. 不必要な構文「then」など

 まずは1の、「bitlyの設定」のロジックを変更したいと思います。この部分です。

class ItemsController < ApplicationController
  conf = APP_CONFIG["bitly"]
  @@bitly = Bitly.new(conf["username"], conf["apikey"])

 ただ、普通にリファクタリングによってコードを変更しても、ちゃんと機能するかどうかが不安なので、先にインテグレーションテストを設定しましょう。ここではRails開発者の間で人気の高いCucumber(キューカンバー)を利用します。

 Cucumberの設定はcucumber-railsを使うと簡単にできるようです。まずGemfileに以下を追加します。

group :test do
 gem "rspec-rails"
  gem 'cucumber-rails'
  gem 'capybara'
  gem 'database_cleaner'
end

 そして「bundle install」としてgemをインストールした後に、以下のようにrspecとcucumberをそれぞれインストールします。

rails g rspec:install
rails g cucumber:install --rspec --capybara

 その後にgeneratorを使うと、以下のような「feature」の雛形が作られます。

ruby script/rails generate cucumber:feature
Feature: Manage items
  In order to [goal]
  [stakeholder]
  wants [behaviour]
  Scenario: Register new item
  Given I am on the new item page
  And I press "Create"

 では、実際のfeatureを書いてみましょう。featureというのは機能のことで、どういう機能があって、誰が何ができるかを自然言語(ここでは英語ですが、日本語も使えます)で記述します。最初に、ログイン状態であればitemが追加登録できるというfeatureの記述例です。

Feature: Manage items
  In order to register new item
  As a user
  I want to add new item
  Scenario: Register new item
    Given I am logged in as "Bob"
    And I am on the root page
    And I follow "Users"
    And I follow "Bob"
    And I fill in "http://www.google.com" for "new_item"
    And I press "Add"
    Then I should see "Created an item"

 この状態でcucumberを走らせると、以下のステップを作る必要があると警告がでてきます。

[worklista (29df2bb...)]$ rake cucumber
1 scenario (1 undefined)
7 steps (5 skipped, 2 undefined)
0m0.026s
You can implement step definitions for undefined steps with these snippets:
Given /^I am logged in as "([^"]*)"$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end

 逆に言うとそれ以外のステップはすでに存在するということです。実は「rails g cucumber:install」としたときに、以下のファイルが作られています。

features/step_definitions/web_steps.rb
features/support/env.rb
features/support/paths.rb
lib/tasks/cucumber.rake
script/cucumber

 その中の「web_steps.rb」ファイルを覗いてみると以下のようなステップが作られています。

module WithinHelpers
  def with_scope(locator)
    locator ? within(locator) { yield } : yield
  end
end
World(WithinHelpers)
Given /^(?:|I )am on (.+)$/ do |page_name|
  visit path_to(page_name)
end
When /^(?:|I )go to (.+)$/ do |page_name|
  visit path_to(page_name)
end
When /^(?:|I )press "([^"]*)"(?: within "([^"]*)")?$/ do |button, selector|
  with_scope(selector) do
    click_button(button)
  end
end
When /^(?:|I )follow "([^"]*)"(?: within "([^"]*)")?$/ do |link, selector|
  with_scope(selector) do
    click_link(link)
  end
end

 では、未定義のステップをfeatures/step_definitions.rbに追加します。ユーザーがログインするフローを記述します。以下の通りです。

Given /^I am logged in as "([^"]*)"$/ do |arg1|
  Given %{I am on the home page}
  And   %{I follow "Sign up"}
  And   %{I fill in "bob@example.com" for "user_email"}
  And   %{I fill in "Bob" for "user_username"}
  And   %{I fill in "testforbob" for "user_password"}
  And   %{I fill in "testforbob" for "user_password_confirmation"}
  And   %{I fill in "dummy_code" for "user_invite_code"}
  And   %{I press "Sign up"}
  And   %{I follow "Login"}
  And   %{I fill in "bob@example.com" for "user_email"}
  And   %{I fill in "testforbob" for "user_password"}
  And   %{I press "Sign in"}
end

 ログインの詳細は今回のfeatureではあまり重要視していない部分なので、ここでは1つのステップにまとめてみました。

 さて、これで以下のように、ちゃんとfeatureがパスするはずです。

[worklista (1cc509a...)]$ rake cucumber
[2011-01-03 21:35:51] INFO  WEBrick 1.3.1
[2011-01-03 21:35:51] INFO  ruby 1.9.2 (2010-08-18) [x86_64-darwin10.4.0]
[2011-01-03 21:35:51] INFO  WEBrick::HTTPServer#start: pid=26542 port=9887
1 scenario (1 passed)
8 steps (8 passed)
0m15.348s

 実は今回のコミットではfeatures/support/env.rbに以下のような設定も付け加えておきました。

Capybara.javascript_driver = :selenium

 Cucumberは、通常はWebratというブラウザの動きをシミュレートするライブラリを使います。しかしながらシミュレータではJavaScriptの動作などをテストすることができません。そこでCapybaraというライブラリを使います。Capybaraを使うと、実際にブラウザを立ち上げて自動テストするためのさまざまなツールと簡単に統合することができます。ここではSeleniumというツールを使っています。こうすることで「@javascript」が付いたfeatureを、実際にブラウザ上でテストすることができるようになります。

 今回はブラウザとしてFirefoxを使っていますが、他のブラウザに切り替えることでクロスブラウザテストが可能です。実際のブラウザを使った自動テストはシミュレート版より遅いので、必要なテストにだけ使ったほうが良いですが、実際にコードを書かないステークホルダー(実際にシステムを使うエンドユーザーやプロジェクトに出資しているビジネスオーナーなど)や、プロダクトオーナーに自動化テストの威力をデモしたいときには、Sleniumのようにブラウザで自動で動くものを見せると結構インパクトがあるのでお勧めです。

「グリーン、レッド、グリーン、リファクター」のリズムで

 通常のテスト駆動開発では「レッド、グリーン、リファクター」のリズムで行うのが良いと言われます。レッドはテストケースが失敗していることを示し、グリーンはテストがパスしたことを示します。

 テスト駆動開発では、最初にテストを書き、このテストが失敗するのを確認します(レッド)。次に、このテストをパスさせるための最低限のコードを書きます(グリーン)。最後に、書かれたコードをリファクタリングすることで「機能追加」と「コードをきれいにする作業」を、順を追って行います。

 既存のコードに後からテストを追加する場合、この手法は一見使えないように思えますが、何とかするやり方はあります。先ほどのテストがグリーンなのを確認した後、リファクタリングする予定の箇所を一度削除してみましょう。

[worklista (8540c64...)]$ git co 8540c64f2a7ae4f0d4e7fdd3494bed7cce2ca4c2
-  conf = APP_CONFIG["bitly"]
-  @@bitly = Bitly.new(conf["username"], conf["apikey"])

 その状態でテストを走らせると、もちろんレッド(失敗)になりますよね。

[worklista (refactoring)]$ rake cucumber
(::) failed steps (::)
uninitialized class variable @@bitly in ItemsController (NameError)
./app/controllers/items_controller.rb:111:in `populate_retweet'
./app/controllers/items_controller.rb:89:in `populate'
./app/controllers/items_controller.rb:31:in `create'
:10:in `synchronize'
./features/step_definitions/web_steps.rb:29:in `block (2 levels) in '
./features/step_definitions/web_steps.rb:14:in `with_scope'
./features/step_definitions/web_steps.rb:28:in `/^(?:|I )press "([^"]*)"(?: within "([^"]*)")?$/'
features/manage_items.feature:12:in `And I press "Add"'
Failing Scenarios:
cucumber features/manage_items.feature:6 # Scenario: Register new item
1 scenario (1 failed)
8 steps (1 failed, 2 skipped, 5 passed)

 その状態で、今度はbitlyの設定をconfig.rbに移行します。

[worklista (2012b30...)]$ git show 2012b30d483d17cb89978c8f200895089445d180
commit 2012b30d483d17cb89978c8f200895089445d180
Author: Makoto Inoue <inouemak@googlemail.com>
Date:   Sun Dec 26 22:49:19 2010 +0000
    Moved Bitly initializer to config.rb
diff --git a/app/controllers/items_controller.rb b/app/controllers/items_controller.rb
index a002af4..3312722 100644
--- a/app/controllers/items_controller.rb
+++ b/app/controllers/items_controller.rb
@@ -5,7 +5,6 @@ require 'resolv-replace'
 class ItemsController < ApplicationController
   before_filter :authorise_as_owner
-
   def create
     @user = User.find(params[:user_id])
@@ -108,7 +107,7 @@ private
   end
   def populate_retweet(item)
-    url = @@bitly.shorten(item.url)
+    url = BITLY.shorten(item.url)
     item.bitly_url = url.short_url
     item.retweet   = url.global_clicks
   end
diff --git a/config/initializers/config.rb b/config/initializers/config.rb
new file mode 100644
index 0000000..d740917
--- /dev/null
+++ b/config/initializers/config.rb
@@ -0,0 +1,6 @@
+APP_CONFIG = YAML.load_file("#{Rails.root}/config/config.yml")[Rails.env]
+
+require 'bitly'
+Bitly.use_api_version_3
+conf = APP_CONFIG["bitly"]
+BITLY = Bitly.new(conf["username"], conf["apikey"])

 config.rbの中ではItemsControllerのクラス変数(@@bitly)は設定できないので、コンスタント(BITLY)に変えてみました。

 ではこの状態でもう一度テストを走らせてみましょう。

git co 2012b30d483d17cb89978c8f200895089445d180
[worklista (2012b30...)]$ rake cucumber
........
1 scenario (1 passed)
8 steps (8 passed)

 ちゃんとグリーンがでましたね。

 このように「グリーン、レッド、グリーン、リファクター」の手順を踏むことで、現在変更している箇所をカバーするテストがあるのをちゃんと確認しながらリファクターをすることができました。

 次回はコントローラのテストに移っていきます。

Copyright© 2017 ITmedia, Inc. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。