実例で学ぶRailsアプリのテスト方法:Railsで目指せ、情熱エンジニア(8)
前回はRailsで使われるテストフレームワークをご紹介しました。今回は具体的なWebアプリを例に、簡単なテストを使ったリファクタリングについて解説します
インテグレーションテストのために「Cucumber」を利用する
前回は、Railsで使われるテストフレームワークをご紹介しました。今回から、いよいよ実際のテストを書きます。ただ書くだけでは物足りないので、前々回の連載で指摘したコードレビューの結果から、リファクタリングの候補をリストアップし、テストを書きながら1つ1つつぶしていきましょう。
- bitlyの設定はサーバの立ち上げ時にするべき
- 重複したコード
- 本来モデルにあるべきロジックがコントローラにある
- 不必要な構文「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 © ITmedia, Inc. All Rights Reserved.