【連載】Railsで目指せ、情熱エンジニア

第8回 実例で学ぶRailsアプリのテスト方法

井上真
New Bamboo

2011/12/22

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

インテグレーションテストのために「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'
    <internal:prelude>:10:in `synchronize'
    ./features/step_definitions/web_steps.rb:29:in `block (2 levels) in <top (required)>'
    ./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)

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

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

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

連載の前回へ
1/1

Index
実例で学ぶRailsアプリのテスト方法
  Page1
インテグレーションテストのために「Cucumber」を利用する
「グリーン、レッド、グリーン、リファクター」のリズムで

TechTargetジャパン

Coding Edge フォーラム 新着記事

@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

RSSフィード

キャリアアップ

@IT Sepcial

イベントカレンダー

PickUpイベント

- PR -
もっと見る

お勧め求人情報

Rails認定試験

ホワイトペーパーTechTargetジャパン

@IT Sepcial
ソリューションFLASH