fv17の日記

Webエンジニアの備忘用ブログです。主にWeb界隈の技術に関して書いています。

RailsにBootstrap3およびBootstrap4を導入する方法

gemを入れることですぐにできる。Bootstrap3か4を使うかで利用するgemが異なる。

Bootstrap3:bootstrap-sass gem
Bootstrap4:bootstrap gem

Bootstrap3を導入する

Gemfile

gem 'bootstrap-sass', '~> 3.3.7'
gem 'sass-rails', '>= 3.2'

app/assets/stylesheets/application.scss

application.scss に、あるいは独自のscssファイルを作成する場合は app/assets/stylesheets/custom.scss などとファイル作成し、先頭に次の2行を追加する。

@import "bootstrap-sprockets";
@import "bootstrap";

JavaScriptを必要とする機能を有効化する

app/assets/javascripts/application.js

//= require rails-ujs
//= require jquery  # 追加
//= require bootstrap  # 追加
//= require turbolinks
//= require_tree .

参照

github.com

Bootstrap4を導入する

公式doc

github.com

手順詳細の解説動画

medium.com

Qiita

qiita.com

RSpecを学ぶ時、書く時に参考になる記事一覧

随時更新中。

「everyday Rails RSpecによるRailsテスト入門」を読了後に読むモノ。
初めてRSpecを学ぶ場合は、何も考えずに書籍の購入をやりこむのがオススメ。

使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」

qiita.com

アンチパターン】Arrange、Act、Assert(AAA)を意識できていないRSpecのコード例とその対処法

qiita.com

Arrange、Act、Assert(AAA)を意識したRSpecの書き方を学べる。

RSpecが複雑になってきた場合に、下記を示してくれる。

  • どこに何を書けば良いか
  • どうすれば可読性が上がるか

個人的に、記事中で紹介されている「テストコードを書き始める前に大枠だけ固め」る技は、テストに限らず通常の実装時にも、「とりあえずロジックだけ日本語で書き出す」みたいに応用できるのでオススメ。AAAはJavaの書籍「実践JUnit」とかでも出てくるので、懐かしい。

Railscasts - How I Test

railscasts.com

Better Specs

画面右上で日本語選択できます。
www.betterspecs.org

【初心者向け】レビュワーをイライラさせるRSpec集と解決方法

tech.medpeer.co.jp

可読性、保守性の高いRSpecについて、メドピアのエンジニアの方がポイントを書いてくださっている。

特に、「5. テスト対象が同じ、複数のテストケースで、subjectが使われていない」の内容は、subjectが効果を発揮するのはどういう場面かが、具体例をもとに示されており分かりやすい。

簡易的なサンプルコードではsubjectの有用性がイマイチ理解できなかったが、記事内で挙げられている具体例ではsubjectでまとめることにより、同一の内容をテストしているということが明確になり分かりやすい。

長いメソッド名、メソッドチェーン連発、引数が微妙に違うとか、一目見た時に同じ処理だと思ってたけどよくよく見てみると違うじゃん!という保守性の悪いコードを何回か見たことがあるので、subjectを使うことにより同一処理が明確になるのは嬉しいし、何より文字数減ってスッキリしている。

なお5.以外の内容を読む前から当たり前と感じない場合は、リーダブルコードや「Arrange、Act、Assert(AAA)を意識できていないRSpecのコード例とその対処法」を読むのがオススメかもです。

RSpec - コントローラスペックでは何をテストすべきか

前提

Rails5からは request spec で記述することが推奨され、公式から controller spec を書くことは非推奨とされている。

テストすべき内容

  • Webリクエストが成功したか
  • 正しいページにリダイレクトされたか
  • ユーザー認証が成功したか
  • レスポンスのテンプレートに正しいオブジェクトが保存されたか
  • ビューに表示されたメッセージは適切か

出典:「Rails テスティングガイド」における「7 コントローラの機能テスト」。

具体的なテストコード

下記、および書籍 Everyday Rails の第5章 コントローラスペックを参照。
RailsのController Spec | 酒と涙とRubyとRailsと

コントローラスペックからリクエストスペックへの移行方法

@t2kojima さん*1がqiitaの記事でまとめてくださっている。

Rails5でコントローラのテストをController specからRequest specに移行する - Qiita

【パーシャル】インスタンス変数の直接参照ではなく、localsで値を渡す

なぜlocalsで渡す必要があるのか

インスタンス変数を直接使うと、Viewと特定のControllerとの依存が強まり、別の箇所で再利用しにくくなるため。

悪い例

Controller (良い例と同じ)

class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
  end
end

View

# herder内で直接@userを参照
<%= render 'header' %>

良い例

View

# herder内では変数userを参照
<%= render 'header', user: @user %>

上記により、将来的に下記のような再利用ができる

<%= render 'header', user: @owner %>

参照

Rails Best Practice

上記と似たようなコードが記載
Rails Best Practices - Replace instance variable with local variable

Qiita - Rails Best Practices の警告をちゃんと考える

Rails Best Practiceを日本語で解説
Rails Best Practices の警告をちゃんと考える - Qiita

Railsガイド レイアウトとレンダリング 3.4.4 ローカル変数を渡す

localsの使い方を解説
レイアウトとレンダリング | Rails ガイド

JavaScriptのファイルを読み込む位置はどこにすべきか?

回答

bodyタグの終端、すなわち </body> の直前。

理由

  • 表示速度を速くするため

javascriptファイルを読み込んでいる間は、HTMLファイルを読み込まない。そのため、ページのレンダリングを行わせてからjavascriptファイルを読み込むことで表示速度を早くする。その結果、SEO対策になるなどのメリットもある。

  • 可読性を上げるため

CSSファイルをheadタグに、JSファイルをbodyタグの終端にまとめることで可読性を上げる。

参考

teratail.com

【各章まとめ】Everyday Rails - RSpecによるRailsテスト入門 - 第8章 スペックをDRYに保つ

復習用まとめ

本章で学ぶこと

  • ワークフローをサポートモジュールに切り出す
  • テスト内でインスタンス変数を再利用するかわりにletを使う
  • shared_contextに共通のセットアップを移動する
  • カスタムマッチャの作成
  • テストを集約して、複数のスペックを一つにする

サポートモジュールに、ログインのフローを切り出す

複数のスペックでの何回も書かれている処理を共通処理として切り出す。例えば、ログイン処理など。ポイントは、メソッド名称を見ただけで何をしているのか一目でわかるように切り出すこと。

切り出し方

spec/supportディレクトリ以下に、login_support.rbを作成

module LoginSupport
  def sign_in_as(user)
    visit root_path
    click_link "Sign in"
    fill_in "Email", with: user.email
    fill_in "Password", with: user.password
    click_button "Log in"
  end
end

RSpec.configure do|config|
  config.include LoginSupport
end

呼び出し方

上記のようにmoduleに、「config.include XxxxxSupport」と記述するか、呼び出すスペック内にてinclude

RSpec.feature "Projects" ,type::feature do
  include LoginSupport  #  ←使うスペックでincludeする

  scenario "user creates a new project" do
    # ...
  end
end

Device gemを使っている場合のログインフローの切り出し方

特に上記のように画面操作をシュミレートしなくても、フィーチャーテストにおいてDeviceのヘルパーをつかうことで、ログイン状態にすることができる。

まずは、rails_helper.rbに次の一行を追加

RSpec.configure do |config|
  # ...
  config.include Devise::Test::IntegrationHelpers, type: :feature
end
RSpec.feature "Projects" ,type::feature do 
  scenario "user creates a new project" do
    user = FactoryBot.create(:user)
    sign_in user  # セッションを作成する
    visit root_path  # セッション作成後に遷移させることを忘れずに
    # ...
  end
end

letで遅延読み込み

なぜletを使うか?

beforeブロックでテストデータを用意する場合、下記のデメリットがある。

  • テスト実行のたびに毎回実行され、不要データが作成されテスト速度が遅くなる可能性がある。
  • 要件が増えるにつれ可読性が悪くなる。

letの使い方

RSpec.describe Note, type: :model do
  let(:user) { FactoryBot.create(:user) }
  let(:project) { FactoryBot.create(:project, owner: user) }

  # このテストではletが呼び出される
  it "is valid with a user, project, and message" do
    note = Note.new(
      message: "This is a sample note.",
      user: user,
      project: project,
    )
    expect(note).to be_valid
  end

  # このテストではletが呼び出されず、無駄なデータが作成されない
  it "is invalid without a message" do
    note = Note.new(message: nil)
    note.valid?
    expect(note.errors[:message]).to include("can't be blank")
  end
end

let!を使う必要がある場合

テスト内で明示的にletの値が呼び出されない場合、例えば検索のテスト等でテスト実行前にデータを用意しておく必要がある場合など、let!を使うことでテスト前にlet内部のコードが呼び出される仕組みとなっている。

  # これより上のコードは略

  describe "search message for a term" do
    let!(:note1) do  FactoryBot.create(
      :note,
      project: project,
      user: user,
      message: "This is the first note")
    end

    let!(:note2) do  FactoryBot.create(
        :note,
        project: project,
        user: user,
        message: "This is the second note")
    end

    let!(:note3) do  FactoryBot.create(
        :note,
        project: project,
        user: user,
        message: "First, preheat the oven.")
    end

    context "when a match is found" do
      it "returns notes that match the search term" do
        expect(Note.search("first")).to include(note1, note3)
      end
    end

    # letとした場合、このテストではデータが全く作成されなくなってしまう
    # let!にすることで、テスト実行前にデータが作成された状態になる 
    context "when no match is found" do
      it "returns an empty collection" do
        expect(Note.search("message")).to be_empty
        expect(Note.count).to eq 3
      end
    end
  end

shared_context

テストにおける条件(=context)、セットアップを共有する。毎回同じセットアップコードを書いているならば、shard_contextで共通化できないかを考えてみる。

通化の方法

spec/support/contexts ディレクトリ以下に、project_setup.rb などと切り出す

RSpec.shared_context "project setup" do
  let(:user) { FactoryBot.create(:user) }
  let(:project) { FactoryBot.create(:project, owner: user) }
  let(:task) { project.tasks.create!(name: "Test task") }
end

使う側の実装

RSpec.describe TasksController, type: :controller do
  include_context "project setup"  # ←この一行を加えるだけでセットアップ完了。

  describe "#show" do
    it "responds with JSON formatted output" do
      sign_in user
      get :show, format: :json,
        params: { project_id: project.id, id: task.id }
      expect(response.content_type).to eq "application/json"
    end
  end 
  .
  .
end

カスタムマッチャ

読みにくいエクスペクテーションを分かりやすいカスタムマッチャとして切り出す。処理をメソッドでまとめ、保守性の高い名前を付けることとやってることは同じ。

RSpec の重要な信条のひとつは、人間にとっての読みやすさです。

spec/support/matchers以下にファイルを作成

spec/support/matchers/content_type.rb

RSpec::Matchers.define :have_content_type do |expected|
  match do |actual|
    content_types = {
      html: "text/html",
      json: "application/json",
    }
    actual.content_type == content_types[expected.to_sym]
  end
end

...一旦ここまで。