みなさん!今日もバグ穴を踏み抜いていますか!!
自分も日々、頭頂部から真っ逆さまに落ちては這い上がってを繰り返しています。
熟練者の方も初学者の方もぶつかるバグ穴の総数は実は同じで、熟練者になればなるほど、ぶつかる前に回避できたりするとか、要するにそういうことなんじゃないだろうか... と最近は考えています。
今回は初学者の方に向けて、いくつかありがちな落とし穴について共有しようと思い、並べてみました。
基本的に参考URLとして挙げてあるリンク先のほうが詳しいです。概要説明からピンとくることがあればそちらをご参照ください。
TL;DR
エラー文をよく読もう!!!(身も蓋もない...)
想定読者
- ざくっとRailsに触れたことがあり、さらに工夫したりし始めようかなと思っている方
- 開発環境もできたしscaffoldのコマンドを叩いたことがある
- RSpecをとりあえず使ってみたことがある
- 「現場で使える Ruby on Rails 5速習実践ガイド」を教科書として読んだことがある
- 今回の記事を考える土台として参考とさせていただきました!ありがとうございます
環境
$ ruby -v
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin18]
$ rails -v
Rails 6.0.1
$ rspec -v
RSpec 3.9
pry-byebugを入れよう
デバッグツールとして有名なgemに pry-byebug
があります。
ソースコード上の任意の箇所に binding.pry
と表記することで、その箇所での変数の中身をチェックしたり、ステップ実行したりすることができます。
group :development, :test do
- # Call 'byebug' anywhere in the code to stop execution and get a debugger console
- gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
+ gem 'pry-byebug'
end
Rails new 時点で byebug
が入っているのですが、ツールとして用途が重複するため、 byebug
は削除して pry-byebug
を追加しています。
シンタックスハイライトが効いてくれるのがありがたくて、自分は byebug
でなく pry-byebug
を使っています。
詳しい使い方はこちら!
参考: pry-byebug を使ってRailsアプリをデバックする方法 - Qiita
まずはエラー文を読もう
エラーが出て困った!というケースは多々あると思います。
そういう時は落ち着いて、エラーの文章をよく読みましょう。
エラーである、というのが分かったということは、画面もしくはサーバログに何かしら英字の出力があるはずです。
デバッグ情報ページ
RailsのDevelopment環境ですと、エラーの場合はデバッグ情報ページを出してくれます。
何が間違っているかは、ページ内の文章が教えてくれます。
また、下部の黒い部分はconsoleとして使用でき、変数の内容などをチェックすることができます。便利ですね!
この画像では、 Idea
となるべきモデル名を Idaa
とtypoしています。
このとき、エラーは uninitialized constant
(直訳で "初期化されていない定数" )ですが、Idaa
なんて知らないぞ、ということですね。
他にもtypoでありがちなのは undefined local variable or method ~
( "未定義のローカル変数またはメソッド" ) という表現ですが、これも字面通り存在しない変数やメソッドを呼び出した時のエラーです。~
の部分に存在しない変数orメソッドの表記が入るので、重点的に確認してください。
それ以外にもあらゆるエラーが存在しますが、まず何をおいても最初はエラー文をよく読んでください。
何をやればいいのかも書いてある
エラー文はとても親切なヤツなので、エラーと出会った人が次にどうすればいいのかを書いてあるケースもあります。
こちらはmigrationが終わってないときに出るエラーです。
文中にて指示されている通り、 rails db:migrate RAILS_ENV=development
を実行すると解決できます。
また、テストを実行するときにも、test環境のDBに対してmigrationを実行する必要があるのですが、その場合もちゃんと実行すべきコマンドの指示をしてくれます。
$ bin/rails test test/models/idea_test.rb
Running via Spring preloader in process 93481
Migrations are pending. To resolve this issue, run:
rails db:migrate RAILS_ENV=test
ちゃんと指示がtest環境について実行するコマンドになっていますね。
めちゃめちゃ優しい...すごい...。
とはいえ度し難いバグはたくさんある
エラーが出るものは文章をよく読み、従えば大抵は解決します。
開発にあたって深刻な問題となってくるのは、「エラーが何を言いたいのかは分かったが、結局お前は何を言っているんだ?」、とか、「エラーは出ずに正常に実行されるが、意図した動きじゃない!」 というアレです。
初学者の方であれば尚更、そういった問題につまづく頻度は多いのではないでしょうか。
初学者の方がRailsを触っているとき、つまづくかもしれないなと思ったエラーをさらにピックアップしていきます。
validation 書きましたか?
# POST /ideas
# POST /ideas.json
def create
@idea = Idea.new(idea_params)
respond_to do |format|
if @idea.save
redirect_to @idea, notice: 'Idea was successfully created.' }
else
render :new
end
end
end
上記はControllerの保存処理の部分です。
@idea.save
をしようとしたとき、次のエラーが出たとします。
ActiveRecord::NotNullViolation (SQLite3::ConstraintException: NOT NULL constraint failed: ideas.name)
エラーは @idea.name
に nil が入っていて、DB側でそれを許していない(NOT NULL制約がある)ことを示しています。
この場合、 @idea.save
は false となり、ユーザーにはnameを入力することを促すように表示されるのが理想です。
ですが、このとき @idea.save
は true となってしまい、実際にDBへの保存処理をしようとした結果、DB側に「NULLはダメです!」と拒否されている状態です。
このような時、 @idea.save
がfalseとなる原因として、Ideaモデルに適切なvalidationが設定されていないことが考えられます。
DBの制限を考慮したvalidationをモデルに設定してあげることで、意図通りに @idea.save
は false を返してくれます。
class Idea < ApplicationRecord
validates :name, presence: true
end
$ bin/rails console
>> idea = Idea.new(name: nil)
=> #<Idea id: nil, name: nil, description: nil, picture: nil, created_at: nil, updated_at: nil>
>> idea.save
=> false
>> idea.errors.messages
=> {:name=>["can't be blank"]}
子/親のモデルで絞り込みってできるの?
もちろんできますよ
突然ですがここにEmployeeモデルとDonutモデルがあります。
EmployeeたちがどんなDonutを食べているのか、を表現しているものとします。
class Donut < ApplicationRecord
belongs_to :employee
end
class Employee < ApplicationRecord
has_many :donuts
end
それぞれ次のように取得することができます。
# チョコレートドーナツを食べている社員たち
Employee.joins(:donuts).where(donuts: { name: 'chocolate' })
# 営業部の社員が食べたドーナツたち
Donut.joins(:employee).where(employees: { department: 'sale' }
ここで、 joins()
が無いと、ActiveRecord::StatementInvalid
の例外が出て、 no such column
とか言われてしまうと思います。
DB上で Employee
、Donut
はそれぞれ別個のテーブルのため、明示的に参照させる必要があり、 joins()
の指定が必要になります。
ActiveRecordに関しては他にも includes
とかmerge
とか scope
とか...無限に考えることがあるのでぜひ上記の参考リンクをご覧ください。
SQLの話になってきますが、理解しておくと便利なキーワードとして他に INNER JOIN
、LEFT OUTER JOIN
などがあります。
もう、ほんとにこの辺は無限に勉強しなきゃいけなくて、参考もたくさん...。
- ActiveRecord ~ 複数テーブルにまたがる検索(preload, eager_load, include, joins) - Qiita
- Rails における内部結合、外部結合まとめ - Qiita
- Active Record テーブルの結合や結合したテーブルを利用した検索や集計やソートについて - Qiita
- SQL素人でも分かるテーブル結合(inner joinとouter join) - Qiita
テスト(RSpec)で特定のタイミングの時だけの挙動をテストしたい
日曜にお休みになるサービスについて、日曜日にアクセスすると「本日はお休みです。」とお知らせを自動で出したいとします。
<h1>welcome!</h1>
<% if Time.zone.now.wday == 0 %>
<p>本日はお休みです。</p>
<% end %>
画面上での表示のことを確認したいので、念の為それをSystem Specでテストしておきましょう。
テストはいつどんな時に実行されても成功しなくてはいけません。
テストを書いたのがちょうど日曜日の場合、次のコードは成功するでしょう。
ですが、翌日の月曜日に実行してみると、失敗してしまいます。
describe 'show notification', type: :system do
it 'off on Sunday' do
visit welcome_path
expect(page).to have_content '本日はお休みです。'
end
end
テストの成否が実行時期に左右されるのは良くないですね。
こういう時のために、テストでは「実行したのが日曜日だったとしたら」を指定することができます。
指定のためにはTimeHelpersを使う必要があります。
RSpec.configure do |config|
# ...前略 ...
config.include ActiveSupport::Testing::TimeHelpers
# ...後略...
end
require 'rails_helper'
describe 'show a notification', type: :system do
it 'off on Sunday' do
travel_to('2019-12-08'.in_time_zone) do
visit welcome_path
expect(page).to have_content '本日はお休みです。'
end
end
it 'hide a notification on Monday' do
travel_to('2019-12-09'.in_time_zone) do
visit welcome_path
expect(page).not_to have_content '本日はお休みです。'
end
end
end
もっと厳密にやろうと思ったら7曜日分のテストがあった方が良いのかもしれませんが、元のerbの実装内容からみてこれくらいでまぁいいかな...と。(どこまでガッチリやりたいかはそれぞれの開発チームのさじ加減だと思います!)
他にも時間操作周りは色々できるので、必要に応じて調べてみてください。
あと、visitの行が重複しているので、ついbeforeに切り出してやりたくなることもあるのですが、この場合テストは失敗してしまいます。
何故なら、visitしたタイミングでは時間操作をしておらず、実行したその日時での結果を検査してしまうためです。
require 'rails_helper'
describe 'show a notification', type: :system do
before do
visit welcome_path
end
it 'off on Sunday' do
travel_to('2019-12-08'.in_time_zone) do
expect(page).to have_content '本日はお休みです。'
end
end
it 'hide a notification on Monday' do
travel_to('2019-12-09'.in_time_zone) do
expect(page).not_to have_content '本日はお休みです。'
end
end
end
- TimeHelpersについての参考
- [Rails: Timecopを使わなくても時間を止められた話 - TechRacho] (https://techracho.bpsinc.jp/penguin10/2018_12_25/67780)
- RailsでTimecopを使わず現在時間をずらすRSpecを書く - コード日進月歩
- System spec の導入、基本的な使い方の参考
- 「現場で使える Ruby on Rails 5速習実践ガイド」Chapter 5-5 System Specを書くための準備
- rspec-rails 3.7の新機能!System Specを使ってみた - Qiita
テスト(RSpec)が長くなってきて待つのが面倒くさい
豆知識ですが、次のようにするとその対象行のテストだけ走らせたりできます。
昔、これを知らなかったときに都度かなり待機しちゃってた時の自戒をこめて...。
# 32行目のテストのみ実行したい
$ bundle exec rspec spec/model/idea_spec.rb:32
日本語化したはいいものの無限にja.ymlが増える件
そういうことありませんか?(自分はありました)
分割して管理すると便利です。
config/locales/ja.yml
に日本語指定を書けば、form.label :name
などとした部分を日本語表記にしてくれます。
<%= form_with(model: idea, local: true) do |form| %>
<div class="field">
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<div class="field">
<%= form.label :description %>
<%= form.text_area :description %>
</div>
<div class="field">
<%= form.label :picture %>
<%= form.text_field :picture %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
ja:
activerecord:
models:
idea: アイデア
attributes:
idea:
name: 名前
description: 説明
picture: 画像
ですが、これを延々と続けているとやがて ja.yml
が肥大化していきます。
分割する方法として、config/locales/idea.yml
とかに書いても読み込みしてくれますが、locales直下のみですとmodel以外のviewやmailerのymlも増えてきた時にカオスになることが予想されます。
そこで、config/application.rb にて、localesの下にフォルダ階層があっても読み込みしてくれるように設定します。
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
config.i18n.default_locale = :ja
こうすることで、localeファイルを例えば次のようなツリー構造で持っておくことができるようになります。
config/locales
├── default.yml
├── models
│ ├── idea.yml
│ └── user.yml
└── views
├── idea.yml
└── user.yml
今、上記例はとりあえず日本語版だけでいいや...という感じで一覧性を重視したツリー構造ですが、enも持つなど多言語対応の場合はもっと工夫は必要かなと思います。en, jaでどこかでフォルダ分けるとか...もしくはidea.ja.ymlとかにするとか...。これもチーム、プロジェクトの方針によりますね。
自分のプロジェクトにベストなツリー構造を考え、管理してみましょう。
公式の解説、そしてmorizyunさんのブログがものすごく詳しいので是非読んでください!
サーバ再起動した?
上記のi18nの話題で、load_path の設定をした後に単純に http://localhost:3000 を再読み込みしただけでは日本語表記は反映されません。
設定がサーバ起動時に確認されているため、サーバ再起動が必要になります。
特にconfig周りなどで、「あれ、変えたはずなのに動かないな...」と感じる時は、一旦プロセスを停止して、改めて $ bin/rails server
を叩いてみましょう。
困ったときの念の為サーバ再起動。
公式ドキュメントを参照しよう
Rails、そしてRubyは多くの方が気になったこと、分からなかったことを積極的に公開してくれています。
検索すればweb記事という形でたくさんの知識を得ることができます。
一方、Railsもついにバージョン6を数え、全てのweb記事がそれに追従して更新されることはまず無いだろう、という事実があります。
その中で常に新しく確実な情報を得られるのはやはり公式ドキュメントです。
methodの使い方などに悩んだら、まず公式ドキュメントや対象gemのREADME原文を読みにいってみましょう。
- Railsガイド: https://railsguides.jp/
- Rubyドキュメント: https://www.ruby-lang.org/ja/documentation/
Google先生への尋ね方
...とは言え、公式のドキュメントのみで全てのトラブルをシューティングできるかというと、それもかなり難易度が高く感じます。
やはりエラー文を頼りに検索することも往々にしてあると思うのですが、そういう時は検索キーワードの中に、自分のプロジェクトのみで使用されている単語・モデル名などが入っていないかを注意するようにしています。
> Idea.foo
NoMethodError (undefined method `foo' for Idea (call 'Idea.connection' to establish a connection):Class)
例えば、上記のエラーですが、この文章を全部を一気に検索にかけても、思うような解決の糸口を得るのは難しいように思います。
なぜなら Idea
というモデルを取り扱っている開発者は、世界中であなただけだからです。
このときエラーが何者か知るために調べるべき単語は、 NoMethodError
とか、 undefined method
という部分かと思います。
(しかしGoogle先生はすごくて、そういう独自単語が混じっていても任意に解釈して適切な解をもらえることもしばしばありますが...)
大量のbacktraceが表示されるケースもあったりしますので、エラーの文面の中から、本当に着目すべき表現は何かを抽出し、検索してみましょう。
また、先述の通り、検索結果には古くてすでに現在のRailsでは適用できない解決方法なども発掘されることがあります。
発見した記事がいつ書かれたものなのか、RailsやRubyのバージョンがいくつのときの話なのか、ということは注意を払っておくと良いかなと思います。
経験あるのみ
色々書きましたが、最終的にはとりあえずたくさんエラーをにぶつかって、たくさん解決するのが一番かなと思います。
では、楽しいRailsライフをお過ごしください!