Ruby on Rails チュートリアル 第3章 HTTPリクエスト テストの基本を解説

前回の続き

著者略歴

YUUKI

ポートフォリオサイト:Pooks

現在:RailsTutorial2周目


第3章 難易度 ★★ 3時間

挫折しないRailsチュートリアルの進め方を先にお読みください↓↓

Railsチュートリアルで挫折しない3つのポイント

この章ではRailsのテスト駆動開発を中心に解説する。


静的ページ

この章からトピックブランチで作業しろとのことなので、static-pagesブランチを切る。

$ cd ~/environment/sample_app

$ git checkout -b static-pages


静的なページの作成

rails generateでコントーラを生成する。

$ rails generate controller StaticPages home help

コントローラー名はStaticPages,homeとhelpアクションも作成する。

このようにコントローラ名をキャメルケースで渡すことにより、スネークケースでファイル名が生成される。

ここではstatic_pages_controller.rbという名前で生成されている。

Rubyでは、クラス名にキャメルケースが使われる慣習がある。


コマンドで削除する

コントローラを間違えて生成した場合、

rails destroy controller コントローラ名 アクション名

コマンドで元に戻すことができる。

モデルを削除する場合は

rails destroy model モデル名

マイグレーションの変更を元に戻す場合は

rails db:rollback

最初の状態に戻す場合は

rails db:migrate VERSION=0

マイグレーションにはそれぞれバージョン番号が付与される。

0は最初の状態であり、他の番号を付与されることで、各状態に戻せる。

ここで,ルーティングを確認してみる


routes.rb

Rails.application.routes.draw do

get 'static_pages/home'
get 'static_pages/help'
root 'application#hello'
end

getリクエストを受け取った時に対応するアクションが結びついている。

ここで、HTTPリクエスト(GET,POST,PATCH,DELETE)の違いを確認する。

GET
POST
PATCH
DELETE

ページを取得する
フォームから情報を送る
データを更新する
データを削除する

↑のget 'static_pages/home'の例だと、URLにアクセスした時に

「ブラウザがサーバーからページを取得する」

という命令を出している。

ここで、新しくFooというコントローラを生成し、その中にbarとbazアクションを追加してみる。

$ rails g controller Foo bar baz

Static_Pagesコントローラの時と同様、fooコントローラとそれに対応するビュー、アクションが追加されている。

先ほど紹介したコマンドでFooコントーラとその中のアクションも削除してみる。

$ rails destroy controller Foo bar baz

スッキリ♪


テスト

テストとは、ソースコードのバグを発見する為に、コードの検証を行う方法である。

テストを書けばバグを追うために余計な時間を使わずに済む為、開発者にとっては必須のスキル。

Tutorial師匠がまとめてくださったテストのガイドラインを載せておく。



  • アプリケーションのコードよりも明らかにテストコードの方が短くシンプルになる (=簡単に書ける) のであれば、「先に」書く

  • 動作の仕様がまだ固まりきっていない場合、アプリケーションのコードを先に書き、期待する動作を「後で」書く
    セキュリティが重要な課題またはセキュリティ周りのエラーが発生した場合、テストを「先に」書く

  • バグを見つけたら、そのバグを再現するテストを「先に」書き、回帰バグを防ぐ体制を整えてから修正に取りかかる
    すぐにまた変更しそうなコード (HTML構造の細部など) に対するテストは「後で」書く

  • リファクタリングするときは「先に」テストを書く。特に、エラーを起こしそうなコードや止まってしまいそうなコードを集中的にテストする


出典:コラム 3.3. 結局テストはいつ行えばよいのか


最初のテスト

rails generate controllerコマンドで、testファイルが生成されてるので確認する。

$ ls test/controllers/

static_pages_controller_test.rb


static_pages_controller_test.rb

require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest
test "should get home" do
get static_pages_home_url
assert_response :success
end

test "should get help" do
get static_pages_help_url
assert_response :success
end

end


assert_responseには「アサーション」と呼ばれる手法が含まれている。

コードの読み方としては

get〜 GETリクエストをhomeアクションに対して送信した→レスポンス成功

assert〜 コントローラからのHTTPレスポンスを検証(引数にsuccessでレスポンスが200(正常)であるか確認)

実際にテストを走らせて見る。

$ rails t

# Running:

..

Finished in 0.137601s, 14.5347 runs/s, 14.5347 assertions/s.

2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

テストスイートが成功したことが分かる。

(後々テストに成功すれば緑色、失敗すれば赤色)の色付けを行う。

なお、テスト実行に時間がかかる件については以下の要因がある。


  • Spring サーバーを起動してRails環境を事前読み込みするのに時間がかかる

  • Rubyそのものの起動に時間がかかる

Rubyの起動時間はGurardというテスト自動化ツールで改善できる(後々紹介)


RED・GREEN・REFACTOR

テスト駆動開発のサイクルは

1:RED(テストの失敗)

2:GREEN(テストの成功)

3:REFACTOR(リファクタリング)

まずはREDから行う。


RED

aboutページ用のテストを入力。


static_pages_controller_test.rb

 test "should get about" do

get static_pages_about_url
assert_response :success
end

testを走らせる。

$ rails t

1) Error:
StaticPagesControllerTest#test_should_get_about:
NameError: undefined local variable or method `static_pages_about_url' for #<StaticPagesControllerTest:0x0000000495d680>
test/controllers/static_pages_controller_test.rb:15:in `block in <class:StaticPagesControllerTest>'

3 runs, 2 assertions, 0 failures, 1 errors, 0 skips

RED。

エラー文を読むと、「Aboutページへのリンクが見つからない」と出ている。

ルーティングを追加してみる。


routes.rb

  get 'static_pages/about'


もう一度テスト

$ rails t

1) Error:
StaticPagesControllerTest#test_should_get_about:
AbstractController::ActionNotFound: The action 'about' could not be found for StaticPagesController
test/controllers/static_pages_controller_test.rb:15:in `block in <class:StaticPagesControllerTest>'

StaticPagesControllerにaboutアクションがない

というエラーが表示された。

という訳で、aboutアクションを追加。


static_pages_controller.rb

  def about

end


さらにテスト。

$ rails t

1) Error:
StaticPagesControllerTest#test_should_get_about:
ActionController::UnknownFormat: StaticPagesController#about is missing a template for this request format and variant.

フォーマットがないって怒られた。

フォーマット?ビューのことらしい。

static_pagesディレクトリ直下にaboutビューを追加してテスト。

$ touch app/views/static_pages/about.html.erb

$ rails t
Finished in 0.143344s, 20.9287 runs/s, 20.9287 assertions/s.

3 runs, 3 assertions, 0 failures, 0 errors, 0 skips

テスト通ったー!!

実際にaboutページを表示してみる。

OK.


ページにタイトルを付ける

titleタグでページにタイトルを付ける。

タイトルを付けることはSEO的にも重要。

書き方は簡単。各viewファイルに下記の記述をするだけ

<title>タイトル</title>

書いた後はブラウザのタブで確認する。

今回は各htmlファイルのタイトルをテストする。


static_pages_controller_test.rb

require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest
test "should get home" do
get static_pages_home_url
assert_response :success
assert_select "title","Home | Ruby on Rails Tutorial Sample App"
end

test "should get help" do
get static_pages_help_url
assert_response :success
assert_select "title","Help | Ruby on Rails Tutorial Sample App"
end

test "should get about" do
get static_pages_about_url
assert_response :success
assert_select "title","About | Ruby on Rails Tutorial Sample App"
end

end


テストを走らせてみる。

$ rails t

# 3 runs, 6 assertions, 3 failures, 0 errors, 0 skips

6つのアサーションのうち3に障害発生。

各ページのタイトルに

| Ruby on Rails Tutorial Sample Appを追加。

もう一度テストしてみる。

$ rails t

3 runs, 6 assertions, 0 failures, 0 errors, 0 skips

テストが通った。

ここで、各テストに同じタイトルを入れていることに気付く。

効率化を測る為に、setupという特別なメソッドを用いて、同じタイトルを変数に代入する。


static_pages_controller_test.rb

  def setup

@base_title = "Ruby on Rails Tutorial Sample App"
end

あとはassert_selectで文字列を式展開するだけ。


static_pages_controller_test.rb

assert_select "title","Home | #{@base_title}"



レイアウトと埋め込み

ここで、三つのタイトルで同じ文字が繰り返し使われていることや、HTML構造全体が各ページで重複している点が気になる。

これはRubyのDRY(Don't Repeat Yourself)という原則に反する為、繰り返しをやめDRYにする。

そのために、今回は「埋め込みRuby」というテクニックを使う。

まずはprovideメソッドとyieldメソッドでタイトル文字を埋め込み&呼び出してみる。


home.html.erb

<% provide(:title, "Home") %>

<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>

provideメソッドでtitleというラベルにHomeという文字列を関連付けている。

これをyieldメソッドでtitleラベルを呼び出している。

これで同じ結果が得られる。テストを走らせて確認。

$ rails t

3 runs, 6 assertions, 0 failures, 0 errors, 0 skips

これだとhead内は全部同じなので、もっとリファクタリングしてHTMLをDRYな構造にしてみる。

前の章で解説した共通部分は親のファイルにまとめるという理論を活用。

まずはlayout_fileにしたapplication.html.erbを元に戻す。

$ mv layout_file app/views/layouts/application.html.erb

次にheadタグ内に共通部分を追加。


application.html.erb

    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>


後はyield部分を各ページで書いていくだけ。


home.html.erb

<% provide(:title, "Home") %>

<h1>Sample App</h1>
<p>
This is the home page for the
<a href="https:/
/railstutorial.jp/">Ruby on Rails Tutorial</a>
sample application.
</p>


テストを走らせる。

$ rails t

3 runs, 6 assertions, 0 failures, 0 errors, 0 skips

リファクタリング完了!


ルーティングの設定

とりあえずルートをHomeページに設定。


routes.rb

  root 'static_pages#home'


これでroot_urlというRailsヘルパーが使えるようになった。

Homeページを確認。

ルートにもテストを設定してみる。


static_pages_controller_test.rb

  test "should get root" do

get root_url
assert_response :success
end

テストを走らせる。

$ rails t

5 runs, 5 assertions, 0 failures, 0 errors, 0 skips

OK。

一応、rootをコメントアウトしたらテスト失敗するか確認。


routes.rb

  #root 'static_pages#home'


$ rails t

1) Error:
StaticPagesControllerTest#test_should_get_root:
NameError: undefined local variable or method `root_url' for #<StaticPagesControllerTest:0x00000004ffbe60>
test/controllers/static_pages_controller_test.rb:6:in `block in <class:StaticPagesControllerTest>'

5 runs, 4 assertions, 0 failures, 1 errors, 0 skips


デプロイ

ここまでの作業をコミット、masterブランチにマージ、gitにpush

$ git commit -am "Finish static pages"

$ git checkout master
$ git merge static-pages
$ git push

テスト走らせてGREENならデプロイ

$ rails t

5 runs, 5 assertions, 0 failures, 0 errors, 0 skips
$ git push heroku


minitestとGuard

テスト結果のGREENとREDに色を付けるためにminitest reportersを導入。


test_helper.rb

require "minitest/reporters"

Minitest::Reporters.use!

テストを走らせてみて色が付いてればOK。

次に、Guardとかいうファイルシステムの変更を監視し、テストを自動的に実行してくれる便利なツールの設定をする。

Tutorial通りに進めてれば既にGemfileでインストールしてるようなので、あとは初期化するだけで動くらしい。

$ bundle exec guard init

08:50:02 - INFO - Writing new Guardfile to /home/ec2-user/environment/sample_app/Guardfile
08:50:02 - INFO - minitest guard added to Guardfile, feel free to edit it

Cloud9の場合はGuardの通知を有効にする為のtmuxをインストールする。

$ sudo yum install -y tmux

さっきの初期化でGuardfileが生成されたので編集。

以下のコードに注目


Guardfile

guard :minitest, spring: "bin/rails test", all_on_start: false do


GuardからSpringサーバーを使って読み込み時間を短縮し、

開始時にテストスイートをフルで実行しないようGuardに指示している。

Guard使用時のSpringとGitの競合を防ぐには、.gitignoreファイルにspring/ディレクトリを追加する。


.gitignore

# Ignore Spring files.

/spring/*.pid

実際にGuradを使ってみる。

新しいターミナルを開いて、以下のコマンドを入力。

$ bundle exec guard

[1] guard(main)>

この状態で、routs.rbのrootをコメントアウトしてみる


routes.rb

  #root 'static_pages#home'


保存すると

/usr/local/rvm/gems/ruby-2.4.1/gems/guard-2.13.0/lib/guard/jobs/pry_wrapper.rb:279: warning: method Pry#input_array is deprecated. Use Pry#input_ring instead

戻して再度保存。

/usr/local/rvm/gems/ruby-2.4.1/gems/guard-2.13.0/lib/guard/jobs/pry_wrapper.rb:279: warning: method Pry#input_array is deprecated. Use Pry#input_ring instead

あれ、同じやな・・・。

他のファイルで試してみたらできた。多分追加で設定しなきゃならないのだろう。今回はいいや。

ちなみに、returnを押せばフルで実行できる。

09:35:41 - INFO - Run all

09:35:41 - INFO - Running: all tests
Running via Spring preloader in process 14197
/home/ec2-user/environment/sample_app/db/schema.rb doesn't exist yet. Run `rails db:migrate` to create it, then try again. If you do not intend to use a database, you should instead alter /home/ec2-user/environment/sample_app/config/application.rb to limit the frameworks that will be loaded.
Started with run options --seed 63182

5/5: [==================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.14934s
5 tests, 5 assertions, 0 failures, 0 errors, 0 skips

/usr/local/rvm/gems/ruby-2.4.1/gems/guard-2.13.0/lib/guard/jobs/pry_wrapper.rb:279: warning: method Pry#input_array is deprecated. Use Pry#input_ring instead

にしても自動でtestしてくれるなんてこりゃ便利!

ここまでできたら、今回の作業までcommitしておこう。

$ git commit -am "Complete advanced setup"

[master 6d6d793c] Complete advanced setup
3 files changed, 12 insertions(+), 7 deletions(-)


pidの削除について

Unix系のシステムではユーザーやシステムが実行させている作業はprocessと呼ばれる一種のコンテナの内部で実行されている。システム上で動いている全てのプロセスを確認する場合は、

$ ps aux

んで、不要なプロセスを排除するには

$ kill -15 pidを指定

-15はCloud9のkillコードなので、他では違うかも。

開発中に動作がおかしくなったら、すぐにps auxで状態を確認し、必要に応じてkillする。


まとめ

この章ではtestが出てきてちょっと難易度上がったかもしれません。

とりあえず、テスト駆動開発ではRED・GREEN・REFACTORサイクルを繰り返すのが重要ということですね。

はい、皆さん声に出してー!「RED・GREEN・REFACTOR!!」

第4章へ


単語集


  • ブランチを切る

新しくブランチを生成すること。枝を切る、というようなイメージ。


  • キャメルケース

単語の頭文字を大文字でつなぎ合わせる命名規則のこと。

変数や関数の命名時に使われる。


  • スネークケース

単語間を_で繋げる命名規則のこと。


  • テスト駆動開発

TDDと略す。

最初にテストを書いて検証するテスト手法のことであり、「テストファースト」という考え方で浸透している。


  • 統合テスト

コントローラやモデルなどを統合させてうまく連携・動作しているかを確認するテスト手法のこと。


  • アサーション

テスト結果が期待値と同じであるのかの真偽判定を行い、そのテスト項目の成否をフレームワークに伝える。


  • ステータスコード

サーバーからブラウザに返してくるコードのことをステータスコードと言う。

正常処理は200。

よく見る404エラーは、(File)Not foundである(ファイルが見つからない)


  • リファクタリング

アプリケーションの外観の振る舞いを変えずに、内部のソースコードを簡略化すること。


  • コードの臭い

以下のようなイメージ


重複したコード

同一あるいは同様のコードが複数箇所に存在。

長すぎるメソッド

メソッド、関数、手続きが長くなりすぎている。

巨大なクラス

大きくなりすぎたクラス。神オブジェクト(英語版)を参照。

機能の横恋慕

他クラスのメソッドを過度に用いるクラス。

不適切な関係

他のクラスの実装の詳細に依存しているクラス。

相続拒否

基底クラスの規約が尊重されない形でのメソッドオーバーライド。リスコフの置換原則参照。

怠け者クラス

行うことが少なすぎるクラス。

重複メソッド

同一あるいは同様のメソッドが複数箇所に存在。

不自然な複雑さ

簡潔な設計で十分なところに、過剰に複雑なデザインパターンの使用を強制する。


出典:コードの臭い


  • SEO

検索エンジン最適化。Googleなどの検索エンジンで上位表示させる為にいろいろ仕掛けるアレ。


  • assert_select

現在表示しているページの一部をセレクタで特定して、確認条件が満たされているかを確認する。

assert_select("セレクタ",確認条件,"失敗した時のメッセージ")

様々な確認の仕方がある。

assert_select "title","Ruby" #タイトルタグの文字列が「Ruby」と一致すれば成功。

assert_select "form" #formタグが1以上存在していれば成功。
assert_select "form", false #formタグが存在しなければ成功。
assert_select "form input", 5 #formタグ直下のinputタグの数が5であれば成功。
assert_select "form input", 1..5 #formタグ直下のinputタグの数が1〜5であれば成功。


  • .gitignoreファイル

Gitの設定ファイルの1つ。ここで指定されたファイルはGitリポジトリに追加されなくなる。