レイアウトのリンク概論
Railsにおいても、リンクを直接記述することは可能です。
<a href="/static_pages/about">About</a>
しかし、リンクを直接記述するというのは、いかにも力押しで、Railsらしいエレガントなコードではないですね。変数・定数の定義以外では、できる限りリテラルの埋め込みは避けたいものです。
さらに、以下のような事情もあります。
- aboutページのURLは、
/static_pages/about
より/about
のほうが好ましい - Railsでは、aboutページのようなページへのリンクには、名前付きルートを使うのが慣例である
となると、Aboutページへのリンクは以下のように書くのがRails的です。
<%= link_to "About", about_path %>
前述「Railsらしい記法」には、以下のような利点があります。
- コードの意味がわかりやすくなる
-
about_path
の定義を書き換えることで、about_path
が使われているすべてのURLを一度に変更することができるようになる
Railsチュートリアルでは、今後使う予定のURLとルーティング(route)とのマッピングを、以下のように定義しています。
ページ名 | URL | 名前付きルート |
---|---|---|
Home | / | root_path |
About | /about | about_path |
Help | /help | help_path |
Contact | /contact | contact_path |
Sign Up | /signup | signup_path |
Log In | /login | login_path |
Contactページ
演習 - サンプルアプリケーションにContact (問い合わせ先) ページを作成してみるで既に作成していました。当該項目をご参照ください。
RailsのルートURL
名前付きルートをサンプルアプリケーションの静的ページで使うために、ルーティング用のファイル(config/routes.rb
)を編集していきます。
現時点で、既にHomeページのルーティングは設定されています。root
ルーティングが存在しないと、Railsアプリケーションそのものが動作しないからです。
root 'static_pages#home'
名前付きルートを含むルートURLを設定することには、以下のような利点があります。
- ブラウザからアクセスしやすくなる
- 名前付きルートを使ってURLを参照できるようになる
-
root
であれば、root_path
やroot_url
といったメソッドを使えるようになる
-
rails console
からapp.root_path
やapp.root_url
メソッドを使うこともできます。現時点におけるapp.root_path
およびapp.root_url
の結果は以下の通りでした。
>> app.root_path
=> "/"
>> app.root_url
=> "http://www.example.com/"
ルーティング用のファイルのget
ルールは、第1引数にURL、第2引数にオプションハッシュを取ります。オプションハッシュの:to
キーに値を与えることによって、名前付きルートを定義することができます。
早速config/routes.rb
を編集し、名前付きルートを定義しましょう。例えば、Helpページに対する記述は、以下のように変換します。
- get 'static_pages/help'
+ get '/help', to: 'static_pages#help'
Helpページに対して名前付きルートを定義したところで、rails console
から、help_path
メソッドやhelp_url
メソッドを使ってみましょう。
>> app.help_path
=> "/help"
>> app.help_url
=> "http://www.example.com/help"
help_path
メソッドやhelp_url
メソッドが使えるようになっていますね。
逆に、名前付きルートを定義していないページに対して、_path
や_url
メソッドを呼び出すことはできません。
>> app.about_path
...略
NoMethodError (undefined method `about_path' ...略)
現時点でAboutページに対しては名前付きルートを定義していないので、このようなエラーになるのですね。
最終的に、config/routes.rb
は以下のように書き換えます。
Rails.application.routes.draw do
root 'static_pages#home'
- get 'static_pages/home'
- get 'static_pages/help'
- get 'static_pages/about'
- get 'static_pages/contact'
+ get '/help', to: 'static_pages#help'
+ get '/about', to: 'static_pages#about'
+ get '/contact', to: 'static_pages#contact'
end
rails console
から、再びabout_path
メソッドを呼び出してみましょう。
>> app.about_path
=> "/about"
今度は無事呼び出すことができました。
テストの修正
現状でrails test
を実施すると、テストは失敗します。
# rails test
Running via Spring preloader in process 974
...略
ERROR["test_should_get_help", StaticPagesControllerTest, 0.26505280000856146]
test_should_get_help#StaticPagesControllerTest (0.27s)
NameError: NameError: undefined local variable or method `static_pages_help_url' ...略
ERROR["test_should_get_home", StaticPagesControllerTest, 0.33508309998433106]
test_should_get_home#StaticPagesControllerTest (0.34s)
NameError: NameError: undefined local variable or method `static_pages_home_url' ...略
ERROR["test_should_get_about", StaticPagesControllerTest, 0.39137299999129027]
test_should_get_about#StaticPagesControllerTest (0.39s)
NameError: NameError: undefined local variable or method `static_pages_about_url' ...略
ERROR["test_should_get_contact", StaticPagesControllerTest, 0.4491715999902226]
test_should_get_contact#StaticPagesControllerTest (0.45s)
NameError: NameError: undefined local variable or method `static_pages_contact_url' ...略
4/4: [=============================================================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.50959s
4 tests, 0 assertions, 0 failures, 4 errors, 0 skips
「static_pages_about_url
等の名前が参照できない」というエラーですね。これらの名前は、例えば「about_path
」等に置き換えたので、テストも相応に書き直す必要があります。
require 'test_helper'
class StaticPagesControllerTest < ActionDispatch::IntegrationTest
def setup
@base_title = "Ruby on Rails Tutorial Sample App"
end
test "should get root" do
get root_path
assert_response :success
end
test "should get home" do
- get static_pages_home_url
+ get root_path
assert_response :success
assert_select "title", "Home | #{@base_title}"
end
test "should get help" do
- get static_pages_help_url
+ get help_path
assert_response :success
assert_select "title", "Help | #{@base_title}"
end
test "should get about" do
- get static_pages_about_url
+ get about_path
assert_response :success
assert_select "title", "About | #{@base_title}"
end
test "should get contact" do
- get static_pages_contact_url
+ get contact_path
assert_response :success
assert_select "title", "Contact | #{@base_title}"
end
end
以上の修正が完了すれば、テストは再び通ります。
# rails test
...略
5/5: [=============================================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.31410s
5 tests, 9 assertions, 0 failures, 0 errors, 0 skips
演習 - 名前付きルートを変更してみる
1. 実は名前付きルートは、as:オプションを使って変更することができます。有名なFar Sideの漫画に倣って、Helpページの名前付きルートをhelf
に変更してみてください (リスト 5.29)。
Rails.application.routes.draw do
root 'static_pages#home'
- get '/help', to: 'static_pages#help'
+ get '/help', to: 'static_pages#help', as: 'helf'
get '/about', to: 'static_pages#about'
get '/contact', to: 'static_pages#contact'
end
2. 先ほどの変更により、テストがred
になっていることを確認してください。リスト 5.28を参考にルーティングを更新して、テストをgreen
にして見てください。
1.が終了した時点で、一旦テストを実行してみます。
# rails test
...略
ERROR["test_should_get_help", StaticPagesControllerTest, 1.884794099983992]
test_should_get_help#StaticPagesControllerTest (1.89s)
NameError: NameError: undefined local variable or method `help_path' for ...略
5/5: [=============================================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.07678s
5 tests, 7 assertions, 0 failures, 1 errors, 0 skips
確かにtest_should_get_help
のところでテストが失敗していますね。
rails console
で、app.help_path
メソッドとapp.helf_path
メソッドの存在を確かめてみましょう。
>> app.help_path
NoMethodError (undefined method `help_path' for #<ActionDispatch::Integration::Session:0x000055f201d2df40>)\
>> app.helf_path
=> "/help"
app.help_path
メソッドが存在しない一方、app.helf_path
メソッドが存在します。
この場合、test_should_get_help
のテストを通すためには、test/controllers/static_pages_controller_test.rb
に以下の変更を行う必要があります。
test "should get help" do
- get help_path
+ get helf_path
assert_response :success
assert_select "title", "Help | #{@base_title}"
end
以上の変更を保存した上で改めてrails test
を実行します。
# rails test
...略
5/5: [=============================================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.15824s
5 tests, 9 assertions, 0 failures, 0 errors, 0 skips
今度はテストが通りました。
3. エディタのUndo機能を使って、今回の演習で行った変更を元に戻して見てください。
全体としては、以下2つの内容の変更となります。
Rails.application.routes.draw do
root 'static_pages#home'
- get '/help', to: 'static_pages#help', as: 'helf'
+ get '/help', to: 'static_pages#help'
get '/about', to: 'static_pages#about'
get '/contact', to: 'static_pages#contact'
end
test "should get help" do
- get helf_path
+ get help_path
assert_response :success
assert_select "title", "Help | #{@base_title}"
end
以上の変更を保存した上で改めてrails test
を実行します。
# rails test
...略
5/5: [=============================================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.86960s
5 tests, 9 assertions, 0 failures, 0 errors, 0 skips
無事テストが通りました。これで心置きなく次の学習にかかれます。
余談
get '/help', to: 'static_pages#help', as: 'helf'
以上のように書くべきところを、誤って以下のように書いてしまいました。
get '/help', to: 'static_pages#help', as: helf
これでどうなったかといいますと…
# rails test
/var/www/sample_app/config/routes.rb:3:in `block in <top (required)>': undefined local variable or method `helf' for #<ActionDispatch::Routing::Mapper:0x000055bb8e302880> (NameError)
...略
# rails console
/var/www/sample_app/config/routes.rb:3:in `block in <top (required)>': undefined local variable or method `helf' for #<ActionDispatch::Routing::Mapper:0x0000563606493238> (NameError)
...略
rails test
もrails console
もできなくなってしまいました。「未定義の変数名を使ってしまった」というエラーですね。
名前付きルート
名前付きルートを定義したことにより、レイアウト中で名前付きルートを使えるようになりました。具体的には、「link_to
メソッドの第2引数に名前付きルートを使えるようになった」というのが大きな変化です。早速headerパーシャルとfooterパーシャルにあるlink_to
メソッドの記述を変更していきましょう。
なお、headerパーシャルでは、Web共通の慣習に従って、ロゴにもHomeページへのリンクを追加しています。
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
- <%= link_to "sample app", '#', id: "logo" %>
+ <%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav navbar-nav navbar-right">
- <li><%= link_to "Home", '#' %></li>
- <li><%= link_to "Help", '#' %></li>
+ <li><%= link_to "Home", root_path %></li>
+ <li><%= link_to "Help", help_path %></li>
<li><%= link_to "Log in", '#' %></li>
</ul>
</nav>
</div>
</header>
<footer class="footer">
<small>
The <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
by <a href="http://www.michaelhartl.com/">Michael Hartl</a>
</small>
<nav>
<ul>
- <li><%= link_to "About", '#' %></li>
- <li><%= link_to "Contact", '#' %></li>
+ <li><%= link_to "About", about_path %></li>
+ <li><%= link_to "Contact", contact_path %></li>
<li><a href="http://news.railstutorial.org/">News</a></li>
</ul>
</nav>
</footer>
例えば、/about
ではAboutページが表示されます。
このときのHTTPリクエストとそれに対するrails server
の反応は以下のとおりです。
Started GET "/about" for 172.17.0.1 at 2019-09-26 11:37:03 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by StaticPagesController#about as HTML
Rendering static_pages/about.html.erb within layouts/application
Rendered static_pages/about.html.erb within layouts/application (0.8ms)
Rendered layouts/_rails_default.erb (140.5ms)
Rendered layouts/_shim.html.erb (0.3ms)
Rendered layouts/_header.html.erb (0.7ms)
Rendered layouts/_footer.html.erb (0.5ms)
Completed 200 OK in 349ms (Views: 307.2ms)
演習 - 名前付きルート
1. リスト 5.29のようにhelf
ルーティングを作成し、レイアウトのリンクを更新してみてください。
Rails.application.routes.draw do
root 'static_pages#home'
- get '/help', to: 'static_pages#help'
+ get '/help', to: 'static_pages#help', as: 'helf'
get '/about', to: 'static_pages#about'
get '/contact', to: 'static_pages#contact'
end
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav navbar-nav navbar-right">
<li><%= link_to "Home", root_path %></li>
- <li><%= link_to "Help", help_path %></li>
+ <li><%= link_to "Help", helf_path %></li>
<li><%= link_to "Log in", '#' %></li>
</ul>
</nav>
</div>
</header>
この変更を加えた後、rails console
におけるapp.help_path
メソッドとapp.helf_path
メソッドの存在確認の結果は以下の通りになります。
>> app.help_path
NoMethodError (undefined method `help_path' for #<ActionDispatch::Integration::Session:0x0000561721e38dc8>)
>> app.helf_path
=> "/help"
一方、rails server
の結果は以下の通りになります。
Started GET "/help" for 172.17.0.1 at 2019-09-26 11:44:11 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by StaticPagesController#help as HTML
Rendering static_pages/help.html.erb within layouts/application
Rendered static_pages/help.html.erb within layouts/application (0.5ms)
Rendered layouts/_rails_default.erb (194.1ms)
Rendered layouts/_shim.html.erb (0.4ms)
Rendered layouts/_header.html.erb (0.6ms)
Rendered layouts/_footer.html.erb (1.0ms)
Completed 200 OK in 376ms (Views: 342.8ms)
名前付きルートの名前を変えても、エンドポイントが/help
のまま変わっていないことは重要です。config/routes.rb
において、get
ルールの第1引数を変えなければ、エンドポイントは変わらないのです。
2. 前回の演習と同様に、エディタのUndo機能を使ってこの演習で行った変更を元に戻してみてください。
Rails.application.routes.draw do
root 'static_pages#home'
- get '/help', to: 'static_pages#help', as: 'helf'
+ get '/help', to: 'static_pages#help'
get '/about', to: 'static_pages#about'
get '/contact', to: 'static_pages#contact'
end
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav navbar-nav navbar-right">
<li><%= link_to "Home", root_path %></li>
- <li><%= link_to "Help", helf_path %></li>
+ <li><%= link_to "Help", help_path %></li>
<li><%= link_to "Log in", '#' %></li>
</ul>
</nav>
</div>
</header>
リンクのテスト
初めての結合テストです。ここでは、「アプリケーションのHTML構造を調べて、レイアウトの各リンクが正しく動くかどうかをチェックする」というテストを書く方法を学習していきます。
まず、site_layout
というテストのテンプレートを作成します。
# rails generate integration_test site_layout
Running via Spring preloader in process 1434
invoke test_unit
create test/integration/site_layout_test.rb
Railsが渡されたファイル名の末尾に_test
という文字列を追加しているのがポイントなのだそうです。
リンクのテストの全体像
今回のテストの全体像は以下のとおりです。
- ルートURL(Homeページ)にGETリクエストを送る。
- 正しいページテンプレートが描画されているか確かめる。
- Home、Help、About、Contactの各ページへのリンクが正しく動作するか確かめる。
test/integration/site_layout_test.rb
の全体像は以下のとおりです。
require 'test_helper'
class SiteLayoutTest < ActionDispatch::IntegrationTest
test "layout links" do
get root_path
assert_template 'static_pages/home'
assert_select "a[href=?]", root_path, count: 2
assert_select "a[href=?]", help_path
assert_select "a[href=?]", about_path
assert_select "a[href=?]", contact_path
end
end
リンクのテストは何を確認するか
上記のコードでは、以下の確認が行われます。
-
root_path
へのGETリクエストの送出 -
root_path
へGETリクエストを送出して返ってきたリソースに対する以下の確認-
static_pages/home
というテンプレートが描画されているか -
root_path
が示すリソースをリンク先とするa
要素が2つ存在するか -
help_path
が示すリソースをリンク先とするa
要素が1つ存在するか -
about_path
が示すリソースをリンク先とするa
要素が1つ存在するか -
contact_path
が示すリソースをリンク先とするa
要素が1つ存在するか
-
実際のコードの解説
例えば、以下のコードを取り上げてみます。
assert_select "a[href=?]", about_path
当該テストコードでは、Railsは?
を自動的にabout_path
に置換します1。現時点で、about_path
に対応するエンドポイントは/about
なので、これにより、以下のようなHTMLコードがあるかをチェックすることになります。
<a href="/about">...</a>
現時点で、static_pages/home
テンプレートが描画されたときのルートURLへのリンクは2つ存在します2。ルートURLが2つあるかどうかをチェックする場合は、以下のテストコードを用います。オプションハッシュで:count
キーに対する値が2
と定義されているのがポイントです。
assert_select "a[href=?]", root_path, count: 2
この他にも、assert_select
には様々な機能があります。興味がありましたら、Railsチュートリアル 表5.2を参照してみてください。
テストを実行してみる
統合テストのみを実施する場合は、Railsの開発環境で以下のコマンドを実行します。
# rails test :integration
rails test
の部分はrails t
でもOKですね。
# rails t :integration
...略
6/6: [=============================================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.20712s
6 tests, 14 assertions, 0 failures, 0 errors, 0 skips
無事にテストが通りました。全体のテストはどうでしょうか。
# rails test
...略
6/6: [=============================================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.81602s
6 tests, 14 assertions, 0 failures, 0 errors, 0 skips
全体のテストも無事通りました。
「リンクに間違った変更が加えられたら、テストですぐに気づくことができる」という仕組みが無事整備されましたね。
演習 - リンクのテスト
1. footerパーシャルのabout_path
をcontact_path
に変更してみて、テストが正しくエラーを捕まえてくれるかどうか確認してみてください。
<footer class="footer">
<small>
The <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
by <a href="http://www.michaelhartl.com/">Michael Hartl</a>
</small>
<nav>
<ul>
- <li><%= link_to "About", about_path %></li>
+ <li><%= link_to "About", contact_path %></li>
<li><%= link_to "Contact", contact_path %></li>
<li><a href="http://news.railstutorial.org/">News</a></li>
</ul>
</nav>
</footer>
上記変更をした場合、テストの結果はどうなるでしょうか…という話ですね。テストは通らないはずです。
# rails t :integration
...略
FAIL["test_layout_links", SiteLayoutTest, 1.887058500025887]
test_layout_links#SiteLayoutTest (1.89s)
Expected at least 1 element matching "a[href="/about"]", found 0..
Expected 0 to be >= 1.
test/integration/site_layout_test.rb:10:in `block in <class:SiteLayoutTest>'
6/6: [=============================================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.89008s
6 tests, 13 assertions, 1 failures, 0 errors, 0 skips
めでたく(?)テストは通りませんでした。「a[href="/about"]
にマッチする要素が(HTMLコード中に)見つからない」という不適合を報告してきています。
…この状態で先には進めないので、app/views/layouts/_footer.html.erb
に対する先ほどの変更はもとに戻しておきましょう。
2.1. リスト 5.35で示すように、Applicationヘルパーで使っているfull_title
ヘルパーを、test環境でも使えるようにすると便利です。こうしておくと、リスト 5.36のようなコードを使って、正しいタイトルをテストすることができます。
ENV['RAILS_ENV'] ||= 'test'
...略
class ActiveSupport::TestCase
# ...略
fixtures :all
+ include ApplicationHelper
# ...略
end
require 'test_helper'
class SiteLayoutTest < ActionDispatch::IntegrationTest
test "layout links" do
get root_path
assert_template 'static_pages/home'
assert_select "a[href=?]", root_path, count: 2
assert_select "a[href=?]", help_path
assert_select "a[href=?]", about_path
assert_select "a[href=?]", contact_path
+ get contact_path
+ assert_select "title", full_title("Contact")
end
end
現時点で、テストは問題なく通ります。
# rails test
...略
6/6: [=============================================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.68342s
6 tests, 15 assertions, 0 failures, 0 errors, 0 skips
2.2. ただし、これは完璧なテストではありません。例えばベースタイトルに「Ruby on Rails Tutoial」といった誤字があったとしても、このテストでは発見することができないでしょう。
module ApplicationHelper
# ページごとの完全なタイトルを返します。
def full_title(page_title = '')
- base_title = "Ruby on Rails Tutorial Sample App"
+ base_title = "Ruby on Rails Tutoial Sample App"
if page_title.empty?
base_title
else
page_title + " | " + base_title
end
end
end
以下のように、site_layout_test
は通ってしまいます。
# rails t test/integration/site_layout_test.rb
...略
1/1: [=============================================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.88365s
1 tests, 6 assertions, 0 failures, 0 errors, 0 skips
2.3. この問題を解決するためには、full_title
ヘルパーに対するテストを書く必要があります。そこで、Applicationヘルパーをテストするファイルを作成し、リスト 5.37のFILL_IN
の部分を適切なコードに置き換えてみてください。
require 'test_helper'
class ApplicationHelperTest < ActionView::TestCase
test "full title helper" do
assert_equal full_title, "Ruby on Rails Tutorial Sample App"
assert_equal full_title("Help"), "Help | Ruby on Rails Tutorial Sample App"
end
end
早速application_helper_test.rb
に対してrails test
を実行してみます。
# rails test test/helpers/application_helper_test.rb
...略
FAIL["test_full_title_helper", ApplicationHelperTest, 0.04009939997922629]
test_full_title_helper#ApplicationHelperTest (0.04s)
--- expected
+++ actual
@@ -1 +1 @@
-"Ruby on Rails Tutoial Sample App"
+"Ruby on Rails Tutorial Sample App"
test/helpers/application_helper_test.rb:5:in `block in <class:ApplicationHelperTest>'
1/1: [=============================================================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.04308s
1 tests, 1 assertions, 1 failures, 0 errors, 0 skips
見事(?)テストで不適合が出ました。
2.余談. base_title
を正しく直したときに、テストが通るかを確認してみます。
base_title
の中身を"Ruby on Rails Tutorial Sample App"に直してみます。
module ApplicationHelper
# ページごとの完全なタイトルを返します。
def full_title(page_title = '')
- base_title = "Ruby on Rails Tutoial Sample App"
+ base_title = "Ruby on Rails Tutorial Sample App"
if page_title.empty?
base_title
else
page_title + " | " + base_title
end
end
end
再びapplication_helper_test.rb
に対してrails test
を実行してみます。
# rails test test/helpers/application_helper_test.rb
...略
1/1: [=============================================================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.02249s
1 tests, 2 assertions, 0 failures, 0 errors, 0 skips
今度こそテストが通りました。