読者です 読者をやめる 読者になる 読者になる

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

RailsでCucumberとRSpecを使ってテストを行う例

Rails gem テスト

RailsでCucumberとRSpecを使ったテストの流れを実例を通して説明します。

CucumberとRSpecのテスト環境の構築方法については、RailsでCucumberとRSpecのテスト環境を構築する方法を参照してください。

目次

1. CucumberとRSpecの特徴と違い
2. テスト方針
3. テストの流れ
4. ユーザーストーリーの作成
5. Cucumberでシナリオを作成しテスト失敗
6. RSpecで単体テストを実装し、それを通るコードを実装
7. Cucumberでシナリオのテスト成功
8. まとめ

1. CucumberとRSpecの特徴と違い

<Cucumberの特徴>
・テストを自然文で記載可能(Featureファイル)ので、読んでいて分かりやすい
・Stepファイルでその英文と実際のコードをマッチングさせるため、テスト対象の言語を問わない
これにより、Featureファイルを読みやすく、ソース実装に依存しない形にできる
=> 自然文を活かして「総合テスト/結合テスト」をCucumberで実施。

<RSpecの特徴>
・英語の文のように書ける
・モックが強力&簡単
・ファイル変更時に自動実行(AutoTest)やテストカバー率(RCov)が図れる
=> モック機能を活かして、View, Controller, Modelの「単体テスト」で使用

2. テスト方針

CucumberとRSpecは日々進化しており、結合テストはどちらでも実行可能です。
それぞれの違いを対比させると、
・Cucumberは読みやすいが、書くのは面倒
・RSpec(結合テスト)は読みやすくないが、書くのは楽
という印象です。

RSpecだけでもガリガリかけますが、
システムがなんのためにあるんだっけというものを意識しながら設計をしたいため、
・単体テスト(View,Controller,Modelの各々単体をテストする) => RSpec
・結合テスト(View,Controller,Modelを結合させて、画面と画面の遷移をテスト) => Cucumber
・総合テスト(画面遷移をまとめた何か目的があるシナリオをテスト) => Cucumber
と今回はしました。今現在もソースは変わっているので、これが正解というの人それぞれ違うと思います。

3. テストの流れ

A. ユーザーストーリーを作成する。
B. Cucumberで1つのシナリオに焦点を合わせてシナリオを作成し、テストに失敗することを確認する。
C. RSpecで単体テストを記載し、そのテストコードに通るソースコードを実装する。
 その後、できればリファクタリングもする。
D. Cucumberのシナリオに成功することを確認する。できれば、その後にリファクタリングする。
E. 次のシナリオについて B. から繰り返す。


4. ユーザーストーリーの作成
今回は一般的なユーザ登録システムを作成します。テスト方法の説明のため、途中までしかやりませんが。<画面遷移図>
ユーザ一覧画面 <=> ユーザ登録・編集画面
        <=> ユーザ詳細画面

といった画面があり、
・管理者が全てのユーザ情報を確認する
・管理者がユーザ情報を登録/編集/削除する
・管理者がユーザ情報の詳細を確認する

といったものが思いつきます。
これの一番目を実装していきます。

5. Cucumberでシナリオを作成しテスト失敗

では、「管理者が全てのユーザ情報を確認する」というシナリオを記載します。
Cucumberでは、
・シナリオを自然文(英語や日本語など)で書いたフィーチャファイル(.feature)を "features配下" に作成
・その後、自然文と実際のコードのマッチングを でステップ定義ファイル( ".rb")を "step_definitions配下" に作成
するという流れです。

フィーチャファイルの構文

まずは、フィーチャファイルを作成する前に構文を理解しましょう。
フィーチャファイルは英語が基本ですが、日本語でも記載可能です。日本語の構文を記載しています。
「フィーチャ」が1つあり、その下に「シナリオ」が複数記載でき、「シナリオ」には「前提/もし/ならば」が必須です。
他には、「かつ/しかし」を記載することで、その文字列の上部の「前提/もし/ならば」を複数記載できます。
さらに、「シナリオアウトライン」では、「例」で表形式にデータを入れると、マッチングさせて複数のデータパターンでシナリオを実行することができます。
日本語で記載するには、最初に"# language: ja"が必要です。

# language: ja
フィーチャ: [フィーチャを記載]
 [フィーチャの説明文を自由に記載可能(省略可能)]

  # シャープ(#)はコメントとして記載可能
  シナリオ: [シナリオ1の概要を記載]
    前提 [前提を記載]
    もし [アクションを記載]
    ならば [結果を記載]

  シナリオ: [シナリオ2の概要を記載]
    前提 [前提を記載]
    かつ [もう1つの前提を記載]
    もし [アクションを記載]
    かつ [もう1つのアクションを記載]
    ならば [結果を記載]

  シナリオアウトライン: [シナリオアウトラインを記載]
    前提 [前提を記載]
    もし <input>を入力する
    ならば <result>が表示される
    例:
      | input | result |
      |       0 |       0  |
      |       1 |       1  |
      |       2 |      10 |
      |       3 |    100 |
フィーチャファイルの作成

では、実際のユーザーストーリーを記載します。

$ vim features/user_index.feature

# language: ja
  フィーチャ: 管理者が全てのユーザ情報を確認する
    管理者として、
    登録している全てのユーザを見たい。
    なぜなら、ユーザを管理したいから。


  シナリオアウトライン: ユーザ一覧画面を表示
    前提  ユーザ数が&lt;ユーザ数&gt;件登録されている
    もし ユーザ一覧画面を表示する
    ならば &lt;ユーザ数&gt;件のユーザ情報が表示されること
    例:
      | ユーザ数  |
      |     0   |
      |     1   |   
||< 

*** Cucumberを実行する
>|ruby|
$ cucumber features/user_index.feature 
Using the default profile...
# language: ja
フィーチャ: ユーザ一覧画面
  管理者として、
  登録している全てのユーザを見たい。
  なぜなら、ユーザを管理したいから。

  シナリオアウトライン: ユーザ一覧画面を表示     # features/user_index.feature:8
    前提ユーザ数が&lt;ユーザ数&gt;件登録されている    # features/user_index.feature:9
    もしユーザ一覧画面を表示する           # features/user_index.feature:10
    ならば<ユーザ数>件のユーザ情報が表示されること # features/user_index.feature:11

    例: 
      | ユーザ数 |
      | 0    |
      Undefined step: "ユーザ数が0件登録されている" (Cucumber::Undefined)
      features/user_index.feature:9:in `前提ユーザ数が<ユーザ数>件登録されている'
      | 1    |
      Undefined step: "ユーザ数が1件登録されている" (Cucumber::Undefined)
      features/user_index.feature:9:in `前提ユーザ数が<ユーザ数>件登録されている'

2 scenarios (2 undefined)
6 steps (6 undefined)
0m0.074s

You can implement step definitions for undefined steps with these snippets:

前提(/^ユーザ数が(\d+)件登録されている$/) do |arg1|
  pending # express the regexp above with the code you wish you had
end

もし(/^ユーザ一覧画面を表示する$/) do
  pending # express the regexp above with the code you wish you had
end

ならば(/^(\d+)件のユーザ情報が表示されること$/) do |arg1|
  pending # express the regexp above with the code you wish you had
end
ステップ定義の実装

フィーチャファイルは実装できましたので、
次はCucumberのステップ定義を実装します。
上記のテスト結果の "前提(/^..." などをコピーして "pending ..." と記載されている箇所を実装していきます。
自然文とソースコードをマッチングさせるときに、「あったらいいな」という考えてソースコードを記載すると、
利用側を強く意識するのでメソッド設計がより分かりやすいものになります。

$ users_steps.rb
前提(/^ユーザ数が(\d+)件登録されている$/) do |num|
  # num分だけユーザデータを作成する
  num.to_i.times do |i|
    User.create(name: "User#{i}", email: "user#{i}@example.com")
  end
end

もし(/^ユーザ一覧画面を表示する$/) do
  visit users_path # ユーザ一覧画面に遷移する。Capybaraのvisitメソッド
end

ならば(/^(\d+)件のユーザ情報が表示されること$/) do |num|
  # ページにユーザ情報(email)が存在するか確認する
  num.to_i.times do |i|
    expect(page).to have_content("user#{i}@example.com")
  end
end
再度Cucumberの実行
$ cucumber features/user_index.feature 
Using the default profile...
# language: ja
フィーチャ: ユーザ一覧画面
  管理者として、
  登録している全てのユーザを見たい。
  なぜなら、ユーザを管理したいから。

  シナリオアウトライン: ユーザ一覧画面を表示     # features/user_index.feature:8
    前提ユーザ数が<ユーザ数>件登録されている    # features/step_definitions/users_steps.rb:1
    もしユーザ一覧画面を表示する           # features/step_definitions/users_steps.rb:7
    ならば<ユーザ数>件のユーザ情報が表示されること # features/step_definitions/users_steps.rb:11

    例: 
      | ユーザ数 |
      | 0    |
      undefined local variable or method `users_path' for #<Cucumber::Rails::World:0x007f9d35aea540> (NameError)
      ./features/step_definitions/users_steps.rb:8:in `/^ユーザ一覧画面を表示する$/'
      features/user_index.feature:10:in `もしユーザ一覧画面を表示する'
      | 1    |
      uninitialized constant User (NameError)
      ./features/step_definitions/users_steps.rb:3:in `block (2 levels) in <top (required)>'
      ./features/step_definitions/users_steps.rb:2:in `times'
      ./features/step_definitions/users_steps.rb:2:in `/^ユーザ数が(\d+)件登録されている$/'
      features/user_index.feature:9:in `前提ユーザ数が<ユーザ数>件登録されている'

Failing Scenarios:
cucumber features/user_index.feature:8 # Scenario: ユーザ一覧画面を表示
cucumber features/user_index.feature:8 # Scenario: ユーザ一覧画面を表示

2 scenarios (2 failed)
6 steps (2 failed, 3 skipped, 1 passed)
0m0.065s

users_pathが宣言されていないと出ています。
では、これは実装をしないといけないので、
今度はRSpecで単体テストを実施していき、そのテストが通るように実装していきましょう。

6. RSpecで単体テストを実装し、それを通るコードを実装

RSpecの基礎構文

はじめに、RSpecの簡単なルールを説明します。

・ファイル名 *_spec.rb ... それを認識してRSpecが走る。
・describe -- ○○について
・context -- ○○の場合
・subject -- itのオブジェクトを設定
・it / specify -- ○○となること
・expect -- itブロック内で使う
・should have / include / be_xxx
・例外 raise_error(RuntimeError, 'msg ...')

基本例
describe "Array" do
  before { @arr = Array.new }

  it "should be empty" do
    expect(:arr).to be_empty
  end

end
||< 

*** Controllerとその関連ファイルを作成します
>|ruby~
$ rails g controller Users --helper false
....
UsersController用のRSpecファイルを作成します。詳細はコメントを見て下さい。
$ vim spec/controllers/users_controller_spec.rb
require 'rails_helper'

# UsersControllerについてのテスト
RSpec.describe UsersController, :type => :controller do

  # GET #indexメソッドのテスト
  describe "GET #index" do

    # HTTP 200を返すことをテスト
    it "responds successfully with an HTTP 200 status code" do
      get :index
      expect(response).to be_success
      expect(response).to have_http_status(200)
    end

    # index テンプレートをレンダリングすることをテスト
    it "renders the index template" do
      get :index
      expect(response).to render_template("index")
    end

    # @usersに全てのuserを設定していることテスト
    it "loads all of the users into @users" do
      user1 = User.create!(name: "test user1", email: "user1@example.com")
      user2 = User.create!(name: "test user2", email: "user2@example.com")
      get :index

      expect(assigns(:users)).to match_array([user1, user2])
    end
  end
end

では、RSpecを実行します。
以下のようなエラーが出ます。以降は文が長くなってしまうので、ピックアップするエラー分のみを表示します。

$ rspec --color --format doc spec/

UsersController
  GET #index
    responds successfully with an HTTP 200 status code (FAILED - 1)
    renders the index template (FAILED - 2)
    loads all of the users into @users (FAILED - 3)

Failures:

  1) UsersController GET #index responds successfully with an HTTP 200 status code
     Failure/Error: get :index
     ActionController::UrlGenerationError:
       No route matches {:action=>"index", :controller=>"users"}
     # ./spec/controllers/users_controller_spec.rb:11:in `block (3 levels) in <top (required)>'

  2) UsersController GET #index renders the index template
     Failure/Error: get :index
     ActionController::UrlGenerationError:
       No route matches {:action=>"index", :controller=>"users"}
     # ./spec/controllers/users_controller_spec.rb:18:in `block (3 levels) in <top (required)>'

  3) UsersController GET #index loads all of the users into @users
     Failure/Error: user1 = User.create!(name: "test user1", email: "user1@example.com")
     NameError:
       uninitialized constant User
     # ./spec/controllers/users_controller_spec.rb:24:in `block (3 levels) in <top (required)>'

Finished in 0.00799 seconds (files took 1.93 seconds to load)
3 examples, 3 failures

Failed examples:

rspec ./spec/controllers/users_controller_spec.rb:10 # UsersController GET #index responds successfully with an HTTP 200 status code
rspec ./spec/controllers/users_controller_spec.rb:17 # UsersController GET #index renders the index template
rspec ./spec/controllers/users_controller_spec.rb:23 # UsersController GET #index loads all of the users into @users
RSpecオプションの設定

ところで、毎回 "--color" や "--format doc" をオプションとしてつけるのが面倒くさい場合、
".rspec" ファイルにオプションを記載することが可能です。

$ vim .rspec
--color
--format doc
Routingの追加

rspecコマンドの結果として"No route matches {:action=>"index", :controller=>"users"}"と出ていたので、
route.rbにルートを追加します。

$ vim config/route.rb

Rails.application.routes.draw do
  # get /users をアクセスすると、UsersControllerのindexアクションが呼ばれる
  get 'users' => 'users#index'
end
end
UsersControllerの実装

rspecコマンドを実行します。
今度は、"The action 'index' could not be found for UsersController"が出るので、
UsersControllerクラスにindexメソッドを実装します。
index.html.erbはUserの一覧を表示するので、DBからUserを取得し、それをindex.html.erbに渡します。

class UsersController < ApplicationController
  def index
    @users = User.all
    # 暗黙的にapp/views/<controller名>/<メソッド名>.html.erbを呼ぶ
    # 今回の場合は、app/views/index.html.erbを呼ぶ
  end
end
UserモデルのRSpec作成

では、"$ rspec spec/"を実行してみます。

今度は、" uninitialized constant UsersController::User"と出るので、
Userモデルを作成しましょう。と思いきや、UserのRSpecを先に作成しましょう。

まずは、マイグレーションファイルやらモデルファイルを作成します。

  $ rails g model User name:string email:string
    invoke  active_record
    create    db/migrate/20140718160926_create_users.rb
    create    app/models/user.rb
    invoke    rspec
    create      spec/models/user_spec.rb
||< 

次に、Userモデル用のRSpecを記載します。
>|ruby|
require 'rails_helper'

RSpec.describe User, :type => :model do

  # before()はテスト実行前に実行するソースコードを記載
  before { @user = User.new(name: "test user", email: "user@example.com") }

  # it を @user にする
  subject { @user }

  # respond_toはメソッドを呼び出せるか調べる
  # つまり、今回の場合は、it(@user)に対して、
  # @user.nameと@user.emailが呼び出せるか調べてる
  it { should respond_to :name }
  it { should respond_to :email }

  # be_validはデータのValidationが正しいか調べる
  it { should be_valid }

end
||< 

マイグレーションをします。
>|ruby|
$ rake db:migrate
== 20140718160926 CreateUsers: migrating ======================================
-- create_table(:users)
     -> 0.0017s
== 20140718160926 CreateUsers: migrated (0.0019s) =============================

そして、Userモデルのテストを実行します。ActiveRecordパワーにより3件成功です。

  $ rspec spec/models/user_spec.rb
  User
    should respond to #name
    should respond to #email
    should be valid

  Finished in 0.00976 seconds (files took 1.97 seconds to load)
  3 examples, 0 failures
index.html.erbの実装

Userモデルの準備ができたので、UsersControllerのRSpecを実行します。

$ rspec spec/controllers/users_controller_spec.rb
....

Failures:

  1) UsersController GET #index responds successfully with an HTTP 200 status code
     Failure/Error: get :index
     ActionView::MissingTemplate:
       Missing template users/index, application/index with {:locale=>[:en], :formats=>[:html], :variants=>[], :handlers=>[:erb, :builder, :raw, :ruby, :jbuilder, :coffee]}. Searched in:
         * "#<RSpec::Rails::ViewRendering::EmptyTemplatePathSetDecorator:0x007fd2df4226b0>"

....

上記のように、"Missing template users/index ..."と出ているので、"index.html.erb"を実装します。
ちなみに、Viewについては、要件がかわりやすく、また、Cucumberのテストと競合するので今回はRSpecは作成しません。

  $ vim app/views/users/index.html.erb

  <h1>ユーザ一覧</h1>

  <ul>
    <% @users.each do |user| %>
    <li>
      <%= user.name %> |
      <%= user.email %>
    </li>
    <% end %>
  </uL>
画面の確認

では、全体のRSpec($ rspec spec/)を実行します。すると、All greenになってます。

UsersController
  GET #index
    responds successfully with an HTTP 200 status code
    renders the index template
    loads all of the users into @users

Finished in 0.04051 seconds (files took 1.94 seconds to load)
3 examples, 0 failures
n-mac:test_rspec_cucmeber yanagisawashojiro$ rspec spec

UsersController
  GET #index
    responds successfully with an HTTP 200 status code
    renders the index template
    loads all of the users into @users

User
  should respond to #name
  should respond to #email
  should be valid

Finished in 0.04497 seconds (files took 1.95 seconds to load)
6 examples, 0 failures


ユーザ一覧画面ができたので、画面を見ましょう。
その前に、データが一つもないのでコンソールから追加しておきます。

$ rails console

001:0> User.create(name: "test user", email: "user@example.com")
002:0> User.all.count #=> 1
003:0> exit

サーバを起動($ rails server)し、画面は次の通りです。
f:id:nipe880324:20140719031445p:plain:w480

7. Cucumberでシナリオのテスト成功

Cucumberのテスト

Cucumberのテストを実行すると、成功しています。
これで、ユーザ一覧画面の基本的な機能を実装できることができました。

$ cucumber features/user_index.feature 

Using the default profile...
# language: ja
フィーチャ: ユーザ一覧画面
  管理者として、
  登録している全てのユーザを見たい。
  なぜなら、ユーザを管理したいから。

  シナリオアウトライン: ユーザ一覧画面を表示     # features/user_index.feature:8
    前提ユーザ数が<ユーザ数>件登録されている    # features/step_definitions/users_steps.rb:1
    もしユーザ一覧画面を表示する           # features/step_definitions/users_steps.rb:8
    ならば<ユーザ数>件のユーザ情報が表示されること # features/step_definitions/users_steps.rb:12

    例: 
      | ユーザ数 |
      | 0    |
      | 1    |

2 scenarios (2 passed)
6 steps (6 passed)
0m0.969s
リファクタリング

では、テストを通ったので、リファクタリングをしていきます。
Viewのリファクタリングです。
View内にあるループや共通部分は、別ファイルに記載し、renderで呼び出すことで可読性や再利用性を上げることができます。
ループ構造になっているので、ループ箇所を削除して、"render"メソッドに変更します。

$ vim app/views/users/index.html.erb

<h1>ユーザ一覧</h1>

<ul>
  <!-- Railsは_user.html.erbを探して、
      @usersの数だけ_user.html.erbをレンダリングする -->
  <%= render @users %>
</uL>

削除した箇所をuser.html.erbを作成します。
二つを合わせれば、元のソースコードと比べると、ループがなくなったのがわかるでしょうか。
これで可読性が上がりました。

$ vim app/views/users/_user.html.erb

<li>
  <%= user.name %> | 
  <%= user.email %>
</li> 

ソースコードをリファクタリングしたので、
CucumberとRSpecを実行し、テストに成功することを確認して下さい。

8. まとめ

他の機能や、モデルの入力チェックなど作り込みはあまいですが、
長くなってしまったのでここら辺でまとめます。

このように、
 1. Cucumberでシナリオを作成しテスト失敗する(featureファイルとstepファイルを記載)
 2. RSpecでテストを記載し、そのテストが通るように実装する、できるようならリファクタリングする
 3. Cucumberのシナリオが通ることを確認する、できるようならリファクタリングする
といった流れで少しずつ機能を作っていきます。

CapybaraやRSpecのメソッドなどより詳細については次の公式ドキュメントを読めば分かると思います。