System Specによる開発

## はじめに

こちらは社内技術勉強会用の資料として作成したものです。
Ruby on Railsにおいて、RSpecの機能である、System Specを使用して開発をしてみます。

サンプルプログラムを使用して、小さな開発を行いつつ、その動作確認をRSpecで行います。

## サンプルプログラムについて

### 概要

サンプルプログラムUV Eatsは、Webサイト上で料理のメニューを注文すると配達してもらえる、というサービスであるとします。以下のような動作をするものとします。

* あらかじめ登録されているユーザでログインして利用します。
* メニュー一覧からひとつのメニューを選択して注文します。
* 注文時は配達先住所、支払方法、クレジットカード番号を入力します。

今回使用する画面は以下の4つです。

* ログイン画面 (Sessionsコントローラ)
* メニュー画面 (Menusコントローラ)
* 注文画面 (Ordersコントローラ)
* 注文完了画面 (Ordersコントローラ)

サンプルプログラムのソースコードは[こちら](https://github.com/ktakase00/uveats_demo)を参照してください。

### 画面イメージ

![uveats_pages.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27959/f9f34e6e-d43d-6d60-1aa9-530a2e60ead7.png)

### DB

DBは以下の3つのテーブルで構成されています。

* ユーザ(users)
* メニュー(menus)
* 注文(orders)

![uveats tables.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27959/86301935-60fc-7933-95d3-0c6807cf709c.png)


## RSpecの使い方について

### その1: テストコードの作成

テストコードは、specディレクトリ内に、specファイルを作成して記述します。ログイン処理のテストであれば、ログイン処理を実装しているSessionsコントローラに関連するものとして、`spec/system/sessions_spec.rb` に記述します。

ログイン画面を表示してみるコードは、以下のような内容になります。

```ruby:spec/system/sessions_spec.rb
require 'rails_helper'

RSpec.describe "Sessions", type: :system, js: true do
  it 'ログイン画面が表示されること' do
    visit '/sessions/new'
  end
end
```

System Specは、`describe` のオプション `type` に、`:system` を指定することで記述します。

> `js`というオプションが指定されていますが、これはこのサンプルプログラムの都合によるものです。付加しておいてください。

テストの内容は、`it` のブロック内に記述していきます。

### その2: 初期データの登録

例えば、サービスにログインできるようにするためには、あらかじめログイン可能なユーザが登録されている必要があります。あらかじめデータを登録しておく方法として、[FactoryBot](https://github.com/thoughtbot/factory_bot) を使用します。

FactoryBotの使い方は、[GETTING_STARTED](https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md) や [こちらの記事](https://qiita.com/kagesumi3m/items/10244978273ffffa9b92) を参照してください。

### その3: RSpecの実行

RSpecを実行するには、以下のようにコマンドを実行します。`$WEB_ROOT` は、Railsプロジェクトのルートディレクトリであるとします。

```sh
cd $WEB_ROOT
bundle exec rspec -fd spec/system/sessions_spec.rb
```

以下のように表示されます。これは、1件のテストを実行し、0件が失敗した、ということを表しています。

```
1 example, 0 failures
```

### その4: ページ内の文字列を検出

ログイン画面には、「メールアドレス」というラベルが表示されるとします。

ログイン画面が正しく表示されたかどうかを判定するには、URLにアクセスした後、このラベル文字列が含まれるページが表示されたかどうかを調べることでわかります。RSpecの構文で、以下のように記述します。

```ruby
expect(page).to have_content('メールアドレス')
```

specファイル全体では以下のようになります。

```ruby:spec/system/sessions_spec.rb
require 'rails_helper'

RSpec.describe "Sessions", type: :system, js: true do
  it 'ログイン画面が表示されること' do
    visit '/sessions/new'
    expect(page).to have_content('メールアドレス')
  end
end
```

## CapybaraによるWebブラウザの操作方法

### その1: Webページへのアクセス

System Specは、自動でWebブラウザを操作してWebアプリケーションにアクセスし、動作の確認を行う仕組みです。Webブラウザの操作には、[Capybara](https://github.com/teamcapybara/capybara) を使用します。

Capybaraの構文を使用して、Webページにアクセスするには `visit` メソッドを使用します。

例えば、ログインページのパスが `/sessions/new` であったとすると、以下のように記述することで、ログインページにアクセスすることができます。

```ruby
visit '/sessions/new'
```

### その2: テキストフィールドへの入力

たとえばメニューを注文する画面において、配達先住所の入力欄が以下のように定義されていたとします。

```html
<label for="order_delivery_address">配達先住所</label>
<input type="text" id="order_delivery_address" name="order[delivery_address]" value="">
```

この要素に対して「東京都新宿区内藤町11番地」と入力したい場合は、Capybaraの `fill_in` メソッドを使用し、以下のいずれかのように記述します。

```ruby
# 入力対象をラベルで指定する場合
fill_in '配達先住所', with: '東京都新宿区内藤町11番地'
# 入力対象をid属性で指定する場合
fill_in 'order_delivery_address', with: '東京都新宿区内藤町11番地'
# 入力対象をname属性で指定する場合
fill_in 'order[delivery_address]', with: '東京都新宿区内藤町11番地'
```

### その3: ドロップダウンの項目の選択

メニューを注文する画面において、支払方法の洗濯欄が以下のように定義されていたとします。

```html
<select name="order[payment_method]">
  <option value="01">現金</option>
  <option value="02">クレジットカード</option>
</select>
```

このドロップダウンで「クレジットカード」を選択したい場合は、Capybaraの `select` メソッドを使用して、以下のように記述します。

```ruby
select 'クレジットカード', from: 'order[payment_method]'
```

選択したい項目はvalueではなく、テキストで指定していることに注意してください。

### その4: ボタンのクリック

メニューを注文する画面において、入力フォームの内容を送信するボタンが以下のように定義されているとします。

```html
<input type="submit" value="注文する">
```

このボタンをクリックするには、`click_button` メソッドで、以下のように記述します。

```ruby
click_button '注文する'
```

リンクをクリックしたい場合や、ボタンとリンクを区別したくない場合などについては、[Clicking links and buttons](https://github.com/teamcapybara/capybara#clicking-links-and-buttons) を参照してください。

## 課題の実施にあたって

RSpecとCapybaraの簡単な使い方を確認したところで、課題を始めましょう。ここで、みなさんに無理難題を申し付けます。

課題にあたっては、ローカルPCで普段使用しているWebブラウザを使用しないでください。使用してもよいWebブラウザは、dockerコンテナ内にインストールされているheadless chromeのみとします。

ページがどのように表示されているのか分からなくて困る？まぁ、とりあえずはじめましょう。

## 課題1

ログイン画面が表示されることを確認してください。

**説明**

ログイン画面は、サンプルプログラムではSessionsコントローラ、newアクションに実装されています。Webブラウザでこのアクションに向かってアクセスし、ログイン画面が表示されることを確認してください。

**Hint 1**

ログイン画面を表示するためのテストコードは、前述「RSpecの使い方について」の「その4: ページ内の文字列を検出」に記載されています。実際にテストコードをspecファイルに記述して実行してみてください。

**Hint 2**

パスを固定文字列で直接指定してもかまいませんが、Sessionsコントローラ、newアクションにアクセスするためのURL(またはパス)は、URLヘルパーメソッド `new_session_url` や `new_session_path` で取得することができます。これらのヘルパーメソッドでURL部分を置き換えてみましょう。

**Hint 3**

headless chromeの画面を直接見ることはできませんが、必要なタイミングでスクリーンショットを作成することができます。スクリーンショットを作成するには以下のように記述します。

```ruby
take_screenshot
```

ログイン画面にアクセスした後に、スクリーンショットを作成してみましょう。テストコード内、`visit` メソッド呼び出しの次の行に追加して、実行してみてください。

スクリーンショットは `tmp/screenshots/` というディレクトリに、png形式のファイルで作成されます。フォルダウィンドウでこのフォルダを開いておき、画像がプレビュー表示される状態にしておくとよいでしょう。

<img width="300" alt="uveats_login.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27959/03e7a265-b032-3282-6374-74edfc49b81b.png">

また、スクリーンショットではなく、生成されたHTMLを確認したい場合は、`visit` メソッド呼び出しの後に以下のように記述しておくと、RSpecを実行しているターミナルの画面に出力されます。

```ruby
puts page.body
```

## 課題2

ログイン画面にメールアドレスとパスワードを入力し、ログインを成功させてください。

**説明**

ログイン画面にはメールアドレス、パスワードの2つの入力欄と、ログインボタンがあります。DBに存在するユーザのメールアドレスとパスワードを入力し、ログインボタンをクリックしてください。

**Hint 1**

テスト用のデータベースには、最初はレコードが1件も登録されていない状態です。ログインするためには、あらかじめユーザを1件登録しておく必要があります。

ユーザのレコードは、FactoryBotを使用すると、以下のようなコードで登録することができます。

```ruby
user = create(:user)
```

`user`変数にUserモデルのインスタンスが代入されます。このインスタンスにはFactoryBotによって自動的に生成されたメールアドレスとパスワードがセットされています。それぞれ、user.mail、user.password とすると値を参照することができます。これらの値を、入力欄に与えてみましょう。

**Hint 2**

入力フォームのへの入力とボタンのクリックは、前述のように、`fill_in`、`click_button` で行うことができます。

**Hint 3**

ログインに成功すると、メニュー一覧画面が表示されます。ログインに成功したかどうかは、スクリーンショットを作成する、または、ページの内容に`Menus`という文字が含まれているかどうかを判定する、などの方法により確認することができます。

## 課題3

注文画面を完成させてください。

**説明**

ユーザがWebサイトにログインし、メニュー画面でいずれかのメニューを選択したとします。すると、注文画面が表示されます。

<img width="700" alt="uveats_order.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27959/8a3bc349-a504-8bcd-4c51-51ea64cfe5e2.png">

この注文画面のビューとコントローラの開発を行ってください。

注文画面には以下の入力項目があるとします。これらの項目がある入力フォームをビュー(`app/views/orders/_form.html.erb`)に追加してください。

|入力項目|物理名|入力方法|選択肢|
|---|---|---|---|
|配達先住所|delivery_address|テキスト|-|
|支払方法|payment_method|ドロップダウン|`01`:現金、`02`:クレジットカード|
|カード番号|card_number|テキスト|-|

Ordersコントローラ(`app/controllers/orders_controller.rb`)では、`create`アクションで注文画面から送信された内容を受け取り、Orderモデルで、DBのordersテーブルにレコードを登録します。

Orderモデルのインスタンスを生成するには、以下の属性値を指定します。

|属性名|物理名|型|値の取り出し元|
|---|---|---|---|
|メニューID|menu_id|数値|`params[:menu_id]`|
|ユーザID|user_id|数値|`@me.id`|
|配達先住所|delivery_address|テキスト|`params`の`order`内、`delivery_address`|
|支払方法|payment_method|ドロップダウン|`params`の`order`内、`payment_method`|
|カード番号|card_number|テキスト|`params`の`order`内、`card_number`|

Ordersコントローラの`order_params`メソッドに、上記の属性値を取り出すコードを記述してください。

テストコードは、`spec/system/orders_spec.rb`に記述してください。

**Hint 1**

ビューファイル `_form.html.erb` 内での、テキスト入力欄、送信ボタンなどの記述方法は、`menus`や`users`などのビューファイルを参考にしてください。

ドロップダウンの記述方法は、Railsガイドの [SelectタグとOptionタグ](https://railsguides.jp/form_helpers.html#select%E3%82%BF%E3%82%B0%E3%81%A8option%E3%82%BF%E3%82%B0) を参考にしてください。以下のようなコードになります。

```erb
<%= form.select :payment_method, options_for_select([['現金', '01'], ['クレジットカード', '02']]) %>
```

**Hint 2**

どのようなHTTPリクエストが送信されているかを確認したくなった場合は、Railsのログを見ることで確認することができます。ログは `log/test.log` に出力されます。たとえば、注文画面での入力内容は以下のように送信されていることがわかります。

```
Started POST "/orders?menu_id=11" for 127.0.0.1 at 2020-02-18 23:38:23 +0900
Processing by OrdersController#create as HTML
  Parameters: {"order"=>{"delivery_address"=>"MyText1", "payment_method"=>"01", "card_number"=>"MyText1"}, "commit"=>"登録する", "menu_id"=>"11"}
```

新しいターミナルを開き、以下のコマンドを実行して、常にログを確認できる状態にしておくとよいでしょう。

```sh
tail -f log/test.log
```

**Hint 3**

注文を行うためには、あらかじめDBに以下のデータが必要です。

* ログインする(=注文する)ユーザ
* 注文するメニュー

このうち、ログインするユーザは、`spec/system/orders_spec.rb`内の以下の行により、自動的に登録されるようになっています。

```ruby
  include_context :system_shared_context
```

メニューは、FactorbyBotを使用して、以下のように記述すると登録することができます。

```ruby
create(:menu)
```

データを登録したら、Capybaraを使用して入力欄を埋め、「登録する」ボタンをクリックしてください。

## 課題4 おまけ

サンプルプログラムでは、注文画面において、支払方法に「クレジットカード」、カード番号に「0000」を入力すると、決済に失敗したとして例外が発生するようになっています。

例外を検知し、例外のメッセージと共に注文画面を再表示するようにプログラムを修正してください。

## 課題1の解答例

```ruby:spec/system/sessions_spec.rb
require 'rails_helper'

RSpec.describe "Sessions", type: :system, js: true do
  it 'ログイン画面が表示されること' do
    # ここにテストコードを書く
    visit new_session_path
    take_screenshot
  end
end
```

## 課題2の解答例

```ruby:spec/system/sessions_spec.rb
require 'rails_helper'

RSpec.describe "Sessions", type: :system, js: true do
  it 'ログインに成功すること' do
    # ここにテストコードを書く
    login_user = create(:user)
    visit new_session_path
    fill_in 'user[mail]', with: login_user.mail
    fill_in 'user[password]', with: login_user.password
    click_button 'ログイン'
    expect(page).to have_content('Menus')
  end
end
```

`let`を使って書く場合は以下のようになります。

```ruby:spec/system/sessions_spec.rb
require 'rails_helper'

RSpec.describe "Sessions", type: :system, js: true do
  describe 'ログインに' do
    let (:login_user) { create(:user) }

    it '成功すること' do
      visit new_session_path
      fill_in 'user[mail]', with: login_user.mail
      fill_in 'user[password]', with: login_user.password
      click_button 'ログイン'
      expect(page).to have_content('Menus')
    end
  end
end
```

## 課題3の解答例

ビュー

```erb:app/views/orders/_form.html.erb
  <div class="field">
    <%= form.label :delivery_address %>
    <%= form.text_field :delivery_address %>
  </div>

  <div class="field">
    <%= form.label :payment_method %>
    <%= form.select :payment_method, options_for_select([['現金', '01'], ['クレジットカード', '02']]) %>
  </div>

  <div class="field">
    <%= form.label :card_number %>
    <%= form.text_field :card_number %>
  </div>

  <div class="actions">
    <%= form.submit nil, { class: 'button' } %>
  </div>
```

コントローラ

```ruby:app/controllers/orders_controller.rb
    def order_params
      params.require(:order).permit(
        :delivery_address,
        :payment_method,
        :card_number,
      ).merge({
        menu_id: params[:menu_id]
      }).merge({
        user_id: @me.id
      })
    end
```

テストコード

```ruby:spec/system/orders_spec.rb
require 'rails_helper'

RSpec.describe "Orders", type: :system, js: true do
  include_context :system_shared_context

  # ここにテストコードを書く
  describe '新規登録に' do
    let (:menu) { create(:menu) }
    let (:order) { build(:order) }
    let (:payment_trans) { { '01' => '現金', '02' => 'クレジットカード' } }

    it '成功すること' do
      visit new_order_path({ menu_id: menu.id })
      fill_in 'order[delivery_address]', with: order.delivery_address
      select payment_trans[order.payment_method], from: 'order[payment_method]'
      fill_in 'order[card_number]', with: order.card_number
      click_button '登録する'
      expect(page).to have_content('Order was successfully created.')
    end
  end
end
```

## おわりに

開発において、DBの値のリセットや入力フォームへの値の入力などの作業をテストコードに任せる、ということを体験していただくことができましたでしょうか？

今回はRuby on RailsとSystem Specでしたが、他の開発言語、フレームワークでも同様なことができるものもあるでしょう。

テストコードはテストを行う手段というだけでなく、開発時にも使ってみていただけるとよいと思います。