Help us understand the problem. What is going on with this article?

初学者が落ちがちなバグ穴と這い上がりのコツ(Rails編)

みなさん!今日もバグ穴を踏み抜いていますか!!
自分も日々、頭頂部から真っ逆さまに落ちては這い上がってを繰り返しています。
熟練者の方も初学者の方もぶつかるバグ穴の総数は実は同じで、熟練者になればなるほど、ぶつかる前に回避できたりするとか、要するにそういうことなんじゃないだろうか... :thought_balloon: と最近は考えています。
今回は初学者の方に向けて、いくつかありがちな落とし穴について共有しようと思い、並べてみました。

:warning: 基本的に参考URLとして挙げてあるリンク先のほうが詳しいです。概要説明からピンとくることがあればそちらをご参照ください。

TL;DR

エラー文をよく読もう!!!(身も蓋もない...)

想定読者

  • ざくっとRailsに触れたことがあり、さらに工夫したりし始めようかなと思っている方
  • 開発環境もできたしscaffoldのコマンドを叩いたことがある
  • RSpecをとりあえず使ってみたことがある
  • 「現場で使える Ruby on Rails 5速習実践ガイド」を教科書として読んだことがある
    • 今回の記事を考える土台として参考とさせていただきました!ありがとうございます :pray:

環境

$ 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 と表記することで、その箇所での変数の中身をチェックしたり、ステップ実行したりすることができます。

Gemfile
 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環境ですと、エラーの場合はデバッグ情報ページを出してくれます。
何が間違っているかは、ページ内の文章が教えてくれます。

スクリーンショット 2019-12-07 23.27.26.png

また、下部の黒い部分はconsoleとして使用でき、変数の内容などをチェックすることができます。便利ですね!

この画像では、 Idea となるべきモデル名を Idaa とtypoしています。
このとき、エラーは uninitialized constant (直訳で "初期化されていない定数" )ですが、Idaa なんて知らないぞ、ということですね。
他にもtypoでありがちなのは undefined local variable or method ~ ( "未定義のローカル変数またはメソッド" ) という表現ですが、これも字面通り存在しない変数やメソッドを呼び出した時のエラーです。~ の部分に存在しない変数orメソッドの表記が入るので、重点的に確認してください。
それ以外にもあらゆるエラーが存在しますが、まず何をおいても最初はエラー文をよく読んでください。

何をやればいいのかも書いてある

エラー文はとても親切なヤツなので、エラーと出会った人が次にどうすればいいのかを書いてあるケースもあります。

スクリーンショット 2019-12-08 0.03.23.png

こちらは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 書きましたか?

app/controllers/ideas_controller.rb
  # 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 を返してくれます。

app/models/idea.rb
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"]}

子/親のモデルで絞り込みってできるの?

もちろんできますよ :thumbsup:

突然ですがここにEmployeeモデルとDonutモデルがあります。
EmployeeたちがどんなDonutを食べているのか、を表現しているものとします。

app/models/donut.rb
class Donut < ApplicationRecord
  belongs_to :employee
end
app/models/employee.rb
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上で EmployeeDonut はそれぞれ別個のテーブルのため、明示的に参照させる必要があり、 joins() の指定が必要になります。

ActiveRecordに関しては他にも includes とかmerge とか scope とか...無限に考えることがあるのでぜひ上記の参考リンクをご覧ください。
SQLの話になってきますが、理解しておくと便利なキーワードとして他に INNER JOINLEFT OUTER JOIN などがあります。

もう、ほんとにこの辺は無限に勉強しなきゃいけなくて、参考もたくさん...。

テスト(RSpec)で特定のタイミングの時だけの挙動をテストしたい

日曜にお休みになるサービスについて、日曜日にアクセスすると「本日はお休みです。」とお知らせを自動で出したいとします。

app/views/welcome/index.html.erb
  <h1>welcome!</h1>

  <% if Time.zone.now.wday == 0 %>
    <p>本日はお休みです。</p>
  <% end %>

画面上での表示のことを確認したいので、念の為それをSystem Specでテストしておきましょう。
テストはいつどんな時に実行されても成功しなくてはいけません。
テストを書いたのがちょうど日曜日の場合、次のコードは成功するでしょう。
ですが、翌日の月曜日に実行してみると、失敗してしまいます。

spec/system/welcome_spec.rb
describe 'show notification', type: :system do
  it 'off on Sunday' do
    visit welcome_path
    expect(page).to have_content '本日はお休みです。'
  end
end

テストの成否が実行時期に左右されるのは良くないですね。
こういう時のために、テストでは「実行したのが日曜日だったとしたら」を指定することができます。
指定のためにはTimeHelpersを使う必要があります。

spec/rails_helper.rb
RSpec.configure do |config|
   # ...前略 ...
   config.include ActiveSupport::Testing::TimeHelpers
   # ...後略...
end
spec/system/welcome_spec.rb
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したタイミングでは時間操作をしておらず、実行したその日時での結果を検査してしまうためです。

spec/system/welcome_spec.rb
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

テスト(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 %>
config/locales/ja.yml
  ja:
    activerecord:
      models:
        idea: アイデア
      attributes:
        idea:
          name: 名前
          description: 説明
          picture: 画像

スクリーンショット 2019-12-08 15.20.20.png

ですが、これを延々と続けているとやがて ja.yml が肥大化していきます。
分割する方法として、config/locales/idea.yml とかに書いても読み込みしてくれますが、locales直下のみですとmodel以外のviewやmailerのymlも増えてきた時にカオスになることが予想されます。

そこで、config/application.rb にて、localesの下にフォルダ階層があっても読み込みしてくれるように設定します。

config/application.rb
  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原文を読みにいってみましょう。

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ライフをお過ごしください! :wave:

beta_chelsea
エセフルスタックエンジニャー
loupe
教員向けサービスSENSEI NOTE、SENSEI PORTALを運営する教育系スタートアップ
https://arrowsinc.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした