RSpec作成の流れ
RSpecの作り方の流れをまとめておきたい。
※ 過去のメモと重複箇所多
テストコードはプロダクトコードに比べると絶対的な正解というのがあまりなく、色々な書き方が存在するので、解答例と寸分狂わずみたいなことを目指さなくて良い。
RSpec
スペックファイル作成
bundle exec rails g rspec:system AdminArticlePreviews
ドライバ設定, supportファイルの読み込み設定
① スペックファイルごとにドライバの初期設定が記載されているが、共通項目として別のsupportディレクトリ配下にドライバの設定ファイルを作成する
# spec/support/capybara.rb RSpec.configure do |config| config.before(:each, type: :system) do driven_by :selnium, using: :headless_chrome, screen_size: [1920, 1080] end end
- ドライバとはテストにおける動作システム。テストの実行環境のこと。デフォルトで設定されているRack::Testは高速だが、JSをテストすることができないので、JSが使えるseleniumドライバを設定している
- 互換性の問題でヘッドレスブラウザ(テストで使うGUIを持たないブラウザ)にはChromeを採用した方が良いらしい。Chromeを使うためにはwebdriversジェムを追加する。そうすると、依存関係にあるselenium-webdriverも一緒にインストールしてくれるので明示的に書かなくていいとのこと。(詳しくはよくわからない)
② 各スペックがsupport配下のファイル設定を読み込むよう、rails_helper.rbに記載
# 元々コメントアウトになっているものを外せばOK Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
FactoryBotでテストデータを作成
① FactoryBotのファイルを作成
$ bundle exec rails g factory_bot:model user
② テストデータを作成する
FactoryBot.define do factory :user do sequence(:name, "general_1" } password { "password" } password_confirmation { "password" } role { :general } trait :admin do sequence(:name, "admin_1") role { :admin } end end end
- sequenceで連番データの作成ができる
- ユニークなテストデータを作りたい時に使う
- sequenceはブロックを渡さずに第二引数を渡すとRubyの.nextメソッドが呼び出され、上記のような書き方でも連番が作れる。
sequence(:email) { |n| "test#{n}@example.com" }
のように連続させたい数字が真ん中に挟まっている場合、.next
が上手く反映されないので大人しくブロックを渡す - 管理者権限はtraitを用いて付与
③ FactoryBotの記載省略を設定ファイルに追加
通常、FactoryBot.create(:user)のような記載でダミーデータを作成するが、設定ファイルに下記の記載をすることで先頭のFactoryBotを省略できる
# spec/rails_helper.rb confin.include FactoryBot::Syntax::Methods
user = create(:user)ないし、let(:user) { create(:user) }みたいな形で作成できる。
ログイン処理を共通項目として切り分ける
ログイン後のテストにおいて、一々ログイン処理をテストに書き込むのはDRYさに欠ける。このため、ログイン処理をモジュールに切り分ける。
① support配下にログイン処理を作成
# spec/support/login_macros.rb module LoginMacros def login_as(user) visit admin_login_identifier_path click_link 'Login' fill_in 'Email', with: user.email fill_in 'Password', with: 'password' click_button 'Login' end end
ちなみにmacrosとは
パソコンで、複雑な操作の手順をあらかじめ登録しておき、必要なときに簡単に実行させる機能。マクロ機能。 macro(マクロ)の意味 - goo国語辞書
② 共通項目なので、こちらもrails_helper.rbに読み込みを記載
RSpec.configure do |config| config.include FactoryBot::Syntax::Methods # 明示的にLoginMacrosの読み込みを記載 config.include LoginMacros end
実際にテストを作成
前述した通り、テストコードに絶対的な正解はないんだそう。ただ現状、自分の乏しい経験値では的確なコードを書くのはかなり難しい。たくさん書いて慣れていくしかないと思う。
require 'rails_helper' RSpec.describe "AdminArticlesPreviews", type: :system do let(:user) { create(:user, :admin)} describe 'ログイン後' do describe '画像のコンテンツブロック挿入後' do context '画像のファイルをアップロードしていない場合' do it '記事のプレビューページを閲覧できる' do login_as(user) click_link '記事' click_link '新規作成' fill_in 'タイトル', with: 'test' click_button '登録する' click_link 'ブロックを追加する' click_link '画像' click_link 'プレビュー' switch_to_window(windows.second) expect(page).to have_content('test') end end end end end
こんな感じで書いた。 テストを実行すると、
エラー発生
結論、テスト環境にもシードデータを追加することで解決。。
bundle exec rails db:seed_fu RAILS_ENV=test
seed-fuは、既に存在しているが変更したいレコードだけ更新したり、ファイル単位で実行できたり、簡単に書けるようなシンタックスシュガーがあったりど便利です。 railsで初期データを入れる(seed-fuの使い方) - Qiita
rails db:seedを何も考えずに使うと実行する度に同じデータが登録されてしまう。
もしくは、spec/spec_helper.rbに下記を記載
RSpec.configure do |config| config.before :suite do SeedFu.seed end end
テスト実行の度にシードデータを入れている。
着眼点として
- current_siteがnilである
- そもそもSiteのデータ登録が必要?
という思考には至らなかった。もし思い至ってもシードデータをテスト環境に追加しようとは思えなかった。
click_buttonとclick_linkについて
RSpec Capybara href の無い a タグにハマる - かもメモ
自分は検証で調べてbtnならclick_button、見た目がボタンではなくただのリンクならclick_linkみたいな使い方をして来たけれど、aタグはリンクでないとマッチしないらしい。
見た目がボタンならボタンというわけではないらしい。
参考にしたサイト
【Rails】はじめてのSystemSpec(RSpec) - Qiita
RSpecメモ(3)
letを使う
let(:project) { create(:project) } let!(:task) { create(:task, project_id: project.id) }
FactoryBotのファイルにprojectとのアソシエーションを記載すれば、テストデータ作成の際にproject_idを記載しなくても良いものと思っていたけれど、上手く行かなかったのでproject_idも記載
※ 遅延するletとitの前に作成してくれるlet!の使い所に注意
別ダブを開く時のテスト
projectの詳細画面に遷移し、task一覧へのリンクをクリックすると別タブが開かれる仕様。別タブを開く記載switch_to_window(window.second)
がないためテストが失敗していた。
RSpecのテストで別タブをテストする方法 - study-outputの日記
visit project_path(project) click_link 'View Todos' switch_to_window(window.second) # 追加 expect(page).to have_content task.title (省略)
click_linkやclick_buttonの後に記載、最後に開いたタブに移動する場合はwindow.last
、次に開いたタブに移動する場合はwindow.second
を記述する。
日付表示
expect(find('.task_list')).to have_content(Time.current.strftime('%Y-%m-%d')) # 削除 expect(find('.task_list')).to have_content(Time.current.strftime('%-m/%d %-H:%M')) #編集
時刻をformat文字列に従って文字列に変換した結果を返します。 strftimeメソッド
実際のビュー画面とdeadlineの表示がことなっていたので変更
しかしこの書き方だと、Viewファイルの仕様が変わった時にこのままのテストで対応できなくなってしまう模様。
RSpecの日付表示を、viewで使っているメソッドに合わせて修正 - Kuni-Blog、こちらのサイトを同じ状態だった。
viewファイルで使っているヘルパーメソッドを用いてテストも書き換えるのが望ましい。
<!-- app/views/tasks/index.html.erb --> <td><%= short_time(task.deadline) if task.deadline? %></td>
expect(find('.task_list')).to have_content(short_time(Time.current))
↑のような形で記述する
さらにこのままだとshort_timeを使ったことでNo methodエラーが出てしまうので、rails_helper.rbにApplicationHelper
の読み込みを記載する。
# spec/rails_helper.rb RSpec.configure do |config| config.include ApplicationHelper end
この記載により、RSpecでもヘルパーを用いることができる。
RSpecメモ(2) Capybara
Capybara
- Webアプリ用のE2E(end to end…始めから終わりまでテストすること)テストフレームワーク
- 複数あるドライバ(動作システム)の中から使いたいものを選択できる
→Capybaraのテスト実行環境(Headlessブラウザ)を選択
※ ヘッドレスブラウザとはGUIを持たないブラウザ。人が操作する目的のものではない。
GUI…マウスや指などで操作できる画面を持たないブラウザのこと。テストの実行環境。
https://wa3.i-3-i.info/word1371.html
Capybaraを使うことで、Webアプリのブラウザでの自動実行、JavaScriptの動作確認、Headlessブラウザ(GUIのないブラウザ)を操作できる
RSpecファイル作成、使用するドライバ設定
❶ テストファイル作成
bundle exec rails g rspec:system User, UserSessions, Task
❷ 作成したスペックファイルにテストを記載
require 'rails_helper' RSpec.describe "Users", type: :system do # デフォルトではRack::Testというドライバが設定されている before do driven_by(:rack_test) end pending "add some scenarios (or delete) #{__FILE__}"
Rack::Test…デフォルトのドライバ。高速だがJSを使えない。 Rackとは
Seleniumドライバ
JavaScriptもサポートしているseleniumドライバを使う場合、デフォルトでFirefoxを使ってテストする様に設定されているが、互換性の問題でChromeを使うように設定しておいた方がいいとのこと。Chromeを使うためにはwebdriversジェム
を追加する。webdriversはライブラリの依存関係上、selenium-webdriver
も一緒にインストールしてくれるので、明示的に書かなくて済むそうな。
他にも、Webkitドライバ(Seleniumより高速)、Poltergeistなどがある
ドライバの設定
通常テストで複数スペックを作成するので、各ファイルに自動的に記載されているドライバの設定は削除し、自作したsupportディレクトリにまとめておく
# spec/support/capybara.rb supportディレクトリを自分で作成 RSpec.configure do |config| config.before(:each, type: :system) do driven_by :selenium, using: :headless_chrome end end
こっちの書き方でもOKだった
RSpec.configure do |config| config.before(:each, type: :system) do driven_by(:selenium_chrome_headless) end end
末尾の_headless
を外すことで実際のブラウザでの動作が見られる
rails_helper.rbなどにsupport配下のファイルを読み込む設定を記載
これにより上で作成したsupport配下の設定ファイルが読み込まれる
# spec/rails_helper.rbの下記のコメントアウトを外す Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
capybaraの構文
describeやit等の起点はRSpecのもの、vistやfill_in '', with: ''
というのは、Capybara独自の起点
.rspec
下記のようにルートディレクトリに.rspecファイルを作成し、下記の様に記述することでテスト結果を見やすくすることができる。
--format documentation --color
RSpecで特定のテストを実行する方法
# ファイル名と行数指定してテストが実行できる $ rspec spec/models/user_spec.rb:12
もしくは
spec_helper.rbに以下の記述を追加し、focus: trueを使える様にする
RSpec.configure do |config| config.filter_run_when_matching :focus end
テストファイルのdescribe, context, itの内、実行したいテストブロックの先頭にf
をつけてfdescribe
, fcontext
, fit
のようにすることで特定のテストのみ実行ができる。
fの消し忘れ対策として
Rubocopでclass: RuboCop::Cop::RSpec::Focusを設定しておくと消し忘れた場合に検知できる。 pre-commitやCIを使って自動でRubocopを実行する様にしておくと便利とのこと。
参考にしたサイト
【Rspec】Capybaraについて | プログラミングマガジン
【Rspec】Capybaraの構文まとめ | プログラミングマガジン
RSpecメモ(1)
RSpecを書く際のポイントメモ
■ エラーチェック
task = user.tasks.new(title: “”, content: “aaa”, status: :todo) expect(task.valid?).to be(false) expect(task.errors).not_to be_empty expect(task.errors[:status]).to eq [“can’t be blank”]
- errorが空出ないことの確認だけではなく、エラー文に想定する文言が含まれているかもテストする
■ タイトルの重複チェック
it ‘is invalid with a duplicate title’ do user = User.create(email: “user@example.com”, password: “password”) task1 = user.tasks.create(title: “aaa”, content: “aaa”, status: :todo) task2→task_with_duplicated_title = user.tasks.build(title: “aaa”, content: “aaa”, status: :todo) expect(task2.valid?).to be(false) expect(task2.errors[:title]).to eq [“has already been taken”] end
- 一つ目のタスクを適当に作成し、二つ目を一つ目と同じtask.titleにすることでtitleの重複状態を作る
- 一つ目をcreateで作成し、二つ目をbuildで未saveにしているのは二つ目がtitleの重複によってsaveできないことを検証するテストだから
- 基準(?)となる一つ目のtaskオブジェクトは他で使わないので変数に入れる必要はない
- タスクの変数をtaskやtask1と書くだけだとイメージしづらいので、task_without_titleやtask_with_duplicated_titleの様に具体的な名前にすると良い
■ be_validやbe_invaidマッチャを使う
expect(task.valid?).to be(false) # 書き換え expect(task).to be_valid
expect(task.invalid?).to be(true) expect(task).to be_invalid # 良いか悪いかはさておき(分からない)、こっちでも通った expect(task).not_to be_valid
FactoryBotについて
# spec/factories/users.rb FactoryBot.define do factory :user do sequence(:email) { |n| "user_#{n}@example.com" } password { "password" } password_confirmation { "password" } end end
# spec/factories/tasks.rb FactoryBot.define do factory :task do sequence(:title, "title_1") content { "content" } status { :todo } deadline { 1.week.from_now } association :user end end
- contentとstatusは一意である必要がないので、そのまま打ち込む
associationでuserとの関連を記載することで、データ作成時にuserを記載する必要がなくなる(後述)。
上記のsequenceの書き方について
連番データを作成することができる。ユニークな値を作りたい場合に用いる。
sequence(:title) { |n| "title_#{n}" }
の書き方の方がどういう動きか見えやすい気がするが…
sequence(:title, "title_1")
は何が起きているのか分からない。
二つ目の書き方では何が起こっているのか。
こちらのサイトによると、「ブロックを渡さずに第二引数を渡すと、.next(Rubyメソッド)
が呼ばれるようになっている」ので
このように異なる連番のtitleを作り出せる(ちなみにテストをカタカナで書くと上手くいかない)
emailのsequenceは変えたい番号が文字列の途中に挟まっているので、nextメソッドで上手く変更を入れることができない。ブロックを使う。
FactoryBotに記載したデータを使う
user = User.create(email: "test@example.com", password: "password") task = user.tasks.build(title: "test1", content: "test1") expect(task).to be_valid
みたいな形でexample(テストの1単位)を作ってきたが、FactoryBotを追加することで、記載が楽になる。
↓
確かにごちゃごちゃしていない
user = FactoryBot.create(:user) task = FactoryBot.build(:task, user: user)
↓
さらに、先ほどspec/factories/tasks.rbにassociation :user
というuserとの関連を記載したが、アレによってuserの記載が省略できる
task = FactoryBot.build(:task)
一層シンプルに!
さらに…
↓
rails_helper.rb内のconfigブロック内に以下を記載
# spec/rails_helper.rb config.include FactoryBot::Syntax::Methods
これにより、データ作成の先頭にあったFactoryBotをも省略できる
task = build(:task)
もはや原型がない。DRYすぎて返って分かりづらい気もするけど、テスト数が増えてくればこの簡素な感じが役に立つのかなと思う。や
所感
こうやって順を追って、「こういう理由だからこの記載を省略できる」と分かりやすいのかもしれない。最初から洗練された簡素なコードを見てしまうと、情報量の少なさにどうしたら良いのか分からなくなった。
書き方に全く馴染みがなかったのもあるけれど、いきなりFactoryBotでテスト用のダミーデータ作成、RSpecのテスト構文を色々試してみるだと混乱するだけなので、まずはFactoryBotを使わず、慣れないマッチャを使わず、RSpecのDRY回避をせず、少しずつ試してみれば良かったなと思います。
よく分からないままlet(:user) { emai: "", password: "password"}みたいなことをしていたので、与えられた条件の中で上手く工夫することが苦手だなぁと・・・。 あとは本来アプリ上の不具合を予防するためにテストを書くはずなのに、そのテスト自体でエラーが出ちゃう状態に苦笑いでした笑
参考にしたサイト
RSpec
使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita
使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 - Qiita
後々参考にしたいサイト
使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」 - Qiita
FactoryBot
FactoryBot(FactoryGirl)チートシート - Qiita
Factorybotを使ったテストデータの作成方法 - Qiita
Sequence
JSとDOM
DOM(Document Object Model)
ドキュメントを物として扱うモデル。 JSからHTMLにアクセスする仕組みのこと。この仕組みによって文書構造、スタイル、内容を変更することができる。DOMを操作して画面をちょこちょこ変えるのがJSの役割(ただし、ひと昔前の内容らしい)
DOMのツリー構造 JavaScript HTML DOM
DOMは、文書をノードとオブジェクトで表現する。ノードとは、上記画像における一つ一つの要素のことをそう呼ぶ。HTMLにおけるエレメントやタグのこと。
ウェブページは文書です。この文書はブラウザーのウィンドウに表示されるかHTMLソースとして表示することが可能です。しかし両方の場合においてもそれは同じ文書です。ドキュメントオブジェクトモデル(DOM)は、その同じ文書を表現、保存する方法です。DOMはウェブページの完全なオブジェクト指向の表現で、JavaScriptのようなスクリプト言語から変更できます。
ID名からノードを取得し、操作する
<button id="button-delete">削除ボタン</button> <button id="button-update">更新ボタン</button>
コンソールに以下を打ち込む
document.getElementById("id名")
ID名から要素を取得するメソッド
button-deleteというIDが振られた要素が取得されている。
単なる文字列ではなくこれがDOMであり、オブジェクト。
削除ボタンだけ赤になった。
上記のような方法で取得したオブジェクトを介してメソッドを実行することができる。
document.getElementById('ID名')で取得した要素を定数に代入し、定義した定数に対してinnerTextメソッドを用いるとタグに囲まれたテキストが表示された。
上記のように新たに文字列を代入するとボタンの文字が変更された。
このようにコンソールにJSを打ち込むことでDOM(ドキュメントオブジェクトモデル)の取得ができる。実際はイベントドリブンと呼ばれる「何かをしたら何かが起きる」という処理を追加することで動かす。
- あるボタンをクリックしたらポップアップを出す
- マウスポインターがある文字の上に乗ったらその文字を色付ける
- あるテキストエリアの内容が変更されたらそれらの文字をプレビュー表示する
イベントは下記のように設定する
DOMに対してaddEventListenerを設定することでイベントを仕掛けられる
// 一番上の行は「ブラウザがDOMの解析をし終わったら〜」というような一種のイベント設定処理 // この記述がないと、DOMの解析がし終わる前に、削除ボタンのDOMを取得しようとするなど上手く動かないことがある document.addEventListener('DOMContentLoaded', () => { console.log("DOMContentLoaded") // 削除ボタンのDOMを取得 const buttonDelete = document.getElementById("button-delete") // 更新ボタンのDOMを取得 const buttonUpdate = document.getElementById("button-update") // 削除ボタンにクリックイベントを仕掛ける buttonDelete.addEventListener('click', () => { // クリックされた時に動く処理 alert("削除!") }) // 更新ボタンにクリックイベントを仕掛ける buttonUpdate.addEventListener('click’, () => { // クリックされた時に動く処理 }) });
↑クリックイベントを仕掛けた後、実際にボタンをクリックすると、こんな感じで画面上部にポップアップが出現する。
演習の考え方まとめ
(なぜか画像が陰った・・・)
↑アラートボタンをクリックするとテキストエリア内に記載した文字がポップアップとして出現する
やってみたこと(失敗)
<p>テキストエリアの内容をアラートで出す</p> <textarea id="textarea"></textarea> <button id="button">アラート</button>
document.addEventListener('DOMContentLoaded', ()=>{ console.log("DOMContentLoaded") const buttonAlert = document.getElementById('button') const textarea = document.getElementById('textarea').innerText buttonAlert.addEventListener('click', ()=>{ alert(textarea) }) });
ボタンが反応しなかった・・・
原因はよく分からないけど、パソコン再起動で解決した
検証でコードを確認した
innerTextメソッドを使ってしまったので、タグに囲まれた内容を表示しようとしているのが間違いだったのかも?
検証の内容を参考に下記のように書き換える(成功)
参考というか検証のカンニングです。
<textarea id="textarea"></textarea> <button id="button">アラート</button>
document.addEventListener('DOMContentLoaded', () => { console.log("DOMContentLoaded") const button = document.getElementById("button") button.addEventListener('click', () => { const text = document.getElementById('textarea') alert(text.value) }) });
できた!
考え方
- テキストエリア内の内容を特定したいのでidを指定
ボタンも特定したいのでidを指定
アラートを押した時に何かしたい
まずアラートのDOMを取ってくる
const buttonAlert = document.getElementById('button')
↑これに対してイベントを設定したい
const text = document.getElementById('textarea'); buttonAlert.addEventListener('click', ()=>{ const value = text.value // 一々定数に入れなくてもOK alert(value) });
最初からalert(text.value)
でいい。
演習2
全く分からなかったので、検証で調べた。
<label for="amount">数値</label> <input type="number" name="amount" id="js-amount"> <button id="js-add-button">増やす</button> <button id="js-minus-button">減らす</button> <p>結果</p> <p id="js-result">0</p>
まず、何はともあれDOMを取得しておく
const jsAddButton = document.getElementById('js-add-button') const jsMinusButton = document.getElementById('js-minus-button') const jsAmount = document.getElementById('js-amount') const jsResult = document.getElementById('js-result') jsAddButton.addEventListener('click', ()=>{ // 試しにここでalert('jsAmount.value')など入れてみてちゃんと動くか、またどう表示されるのか確認する // 少しずつ少しずつ実装しよう◎ // jsResultのinnerTextに対してjsAmountの値を入れている jsResult.innerText = jsAmount.value })
この時点で、inputに入力した内容を結果に表示させることには成功
https://i.gyazo.com/a769ee62b6a2b218a10c2c77e1061572.mp4
const jsAddButton = document.getElementById('js-add-button') const jsMinusButton = document.getElementById('js-minus-button') const jsAmount = document.getElementById('js-amount') const jsResult = document.getElementById('js-result') // 現在値を取得。最初は0で設定する var currentValue = 0 jsAddButton.addEventListener('click', ()=>{ jsResult.innerText = currentValue + jsAmount.value })
↑のようにしてみると・・・
https://i.gyazo.com/ceb18d08737aeb5c198d7a1523aee632.mp4
上手くいかない。
jsAmount.valueはあくまでも文字列として取得した値なので、文字列のまま足されてしまい、思う結果にならない。このため、文字列を数値に変換する作業が必要。
parseIntを使う
currentValue = currentValue + parseInt(jsAmount.value) jsResult.innerText = currentValue
https://i.gyazo.com/cf6fd88f196ab22f0d32b17735e48600.mp4
できた!感動😳
減らすボタンは増やすボタンをコピペして、AddをMinusに、+をーに変更すればOK
https://i.gyazo.com/4460af2e2b31fe9aad50b447207e5b0d.mp4
演習3
何も分からない笑
<input type="text" placeholder="GitHubアカウント名" id="github-name"> <button onclick="fetchRepositories()">リポジトリ取得</button> <ul id="repository-lists"> <!-- 例 --> <!-- <li><a href="url************">repo-name</a></li> --> </ul>
const fetchRepositories = async () => { const githubName = document.querySelector("#github-name").value try}
感想
一気にやろうとすると死ぬので、まず一つ決めてどう記載すれば実現できるのか、細かく分けて考える。分からないことだらけなので、タスクを細分化するのがまず難しいです。慣れるしかなさそう。とはいえ、仕組みは解説動画を見て腑に落ちました👏
Procとは
Procとは?
- Procとはブロックを持ち運びに便利なオブジェクトにしたものです。
- ProcはクラスなのでProc.newでオブジェクトを作る事が出来ます。
- Proc.newによって作成されたProcオブジェクトはcallで呼び出すことが出来ます。 https://qiita.com/k-penguin-sato/items/7f98335ef631ea5ce7ad
ブロックはdo ~ end
や{...}
の塊のこと。これ自体はオブジェクトではないらしい。オブジェクト指向のRubyにおいてオブジェクト以外のものは存在できないらしい。
(正直ここには未だにピンときていない)
調べれば調べるほどよく理解できずに迷宮に迷い込んでいたけれど、ブロックは確かに持ち運びづらそう。ブロックをprocでオブジェクト化して持ち運びやすくしよう!みたいな説明が一番分かりやすかった。
Procの使用例
# ブロックをProcオブジェクト化して変数に代入している sample_proc_1 = proc { |n| n * n } sample_proc_2 = Proc.new { |n| n ** 3 } # Procオブジェクトはcallメソッドで呼び出せる sample_proc_1.call(2) #=> 4 sample_proc_2.call(2) #=> 8
持ち運びしやすい!!!
メソッドから呼び出すとは?🙄
def sample_proc(&my_proc) # (&引数名)のようにメソッドの引数を宣言する # ② 呼び出し元で指定されたブロックがProcオブジェクトに変換されてる puts my_proc.call(2) # ③ 渡されたブロック引数をcallメソッドで呼び出すことができる end sample_proc { |n| n * 2 } # ① sample_procメソッドを呼び出し時にブロック{ |n| n * 2 }を引数として渡している
なるほどeachメソッドやmapメソッドもこんなふうに定義されているのかなと思った。
def each(&引数名) # 何らかの処理 end 配列.each { ブロック }
実際の内部構造を見てみたかったけれど、調べても出てこなかったので合っているのかは分かりません。
感想
ransackのカスタム述語に出てきたprocが何か分からずに調べましたが、かなり難しく感じました。Rubyのオブジェクトについて勉強する時、実際の使用例が見えづらいのが理解に苦しむ要因だなと思います。
yieldやIambdaや何やは一気に考えるとどんどん迷い込みそうだったので省いて考えました。proc
やProc.new
以外にもProcオブジェクトを作る方法はあるとだけ覚えておきます。。
あと今はぼんやりと理解できたようなできていないような、数日後にはまた訳分からなくなってそうな程度の理解度です。難しい🥲
参考にしたサイト
RubyのProcとは?ブロック・Proc・lamdaの違いをマスター | ポテパンスタイル
権限のプルダウン(メモ書き)
権限のプルダウンを作るために調べたことまとめ
ransack軽く復習
ransackは検索機能を実装できるgem
コントローラ
# ビューファイルから送られてくるパラメータqをもとにテーブルからデータを検索する # whereメソッドのransack版なイメージ @q = User.ransack(params[:q]) # ransackメソッドで取得したデータをresultメソッドを用いてActiveRecord_Relationのオブジェクトに変換する @results = @q.result
ビュー
<!-- ransackで定義されているsearch_form_forメソッドを用いる --> <%= search_form_for @q, url: ○○_path do |f| %> <%= f.label :name_cont, 'ユーザー名'%> <%= f.search_field :name_cont %> <!-- 文字入力であればtext_field、数値入力であればnumber_fieldでも可 --> <%= f.submit '検索'%> <% end %>
_contメソッド
ransackで用意されているメソッド
検索したワードが含まれているレコードを取得するためのメソッド。
name_contと、_の前に指定したカラムに対してあいまい検索(検索したワードが含まれたデータが取得)される
_eq
完全に一致する検索結果を表示したい場合に用いる。
name_eqのように記述
詳細に検索をかける
<%= search_form_for @q, url: ○○_path do |f| %> <%= f.label :name_cont, 'ユーザー名'%> <%= f.search_field :name_cont %> <!-- 年齢を検索するフォームも追加 --> <%= f.label :age_eq, '年齢' %> <%= f.number_field :age_eq %> <%= f.submit '検索'%> <% end %>
セレクトボックスを作成する
f.seletcでセレクトボックスを作ることができる
<%= form_with model: @user, url: ○○_path, do |f| %> <%= f.select :role_eq, User.roles_i18n.invert.map{|key, value| [key, User.roles[value]]}, { include_blank: t('defaults.unspecified') }, { class: 'form-control mr-1' } %>
どう使うのか、
見慣れないものがたくさんついている。。。
まず、_18n
って何?
【Rails】enumをI18n対応させるenum_helpが便利すぎた - ひよっこエンジニアの雑多な日記
enumをI18n対応させるために用いるenum_helpジェムをインストールすることで使える
enum
class User < ActiveRecord::Base enum role: { general: 0, admin: 1 } end
# roleカラムに入っている数値によって値が定数名が表示される # roleが0の場合 user.role => "general"
この文字列を日本語で表示させたい時に用いるのがコレ!
gem 'enum_help'
翻訳ファイルに追加
ja: enum: user: role: general: '一般' admin: '権限'
呼び出す際、末尾に_18nを付ける
# roleカラムの値が0の場合 user.role_i18n => "一般"
invert
enum_helpによって使えるヘルパー
User.roles_i18n.invert => {"一般"=>"general", "管理者"=>"admin"}
以上を踏まえてセレクトボックスについて
<%= f.select :role_eq, User.roles_i18n.invert.map{|key, value| [key, User.roles[value]]}, { include_blank: t('defaults.unspecified') }, { class: 'form-control mr-1' } %> <!-- <%= f.select :カラム名, セレクトボックス表示に使うデータ配列orハッシュ, {オプション},{HTMLオプション}%> -->
セレクトボックスの表示に使うデータ配列orハッシュ
% rails c irb(main):009:0> User.roles => {"general"=>0, "admin"=>1} irb(main):010:0> User.roles_i18n => {"general"=>"一般", "admin"=>"権限"} irb(main):011:0> User.roles_i18n.invert => {"一般"=>"general", "権限"=>"admin"} irb(main):012:0> User.roles_i18n.invert.map{|key, value| [key, User.roles[value]]} => [["一般", 0], ["権限", 1]]
↑二次元配列というやつが出来上がった。
セレクトボックスの表示に使うデータはこの二次元配列を使う。必ず配列かハッシュを指定する。
オプション
セレクトボックスのオプション
{ include_blank: true, selected: 0 }
- include_blank: 先頭を空の選択肢にする
- selected: デフォルトで選択しておきたい値を設定
include_blank: ここに文字を入れると先頭の選択肢名が入る https://i.gyazo.com/de33f577dd6d24f59976464920db3311.mp4
HTMLオプション
selectタグにid, classオプションを指定したい時に使う
{ class: 'form-control mr-1' }
※ 必ず通常のオプションを設定しない場合でも空の波括弧{},
をオプション部分に設定すること
※ これがないと、HTMLオプションが無視される
localizeメソッド (i18n主要メソッド)
日時や時刻を設定した言語のフォーマットに変換する https://pikawaka.com/rails/i18n
rails-18n
のgemをインストール済みなので、所定の翻訳を参照してくれる
irb(main):001:0> Date.today => Sat, 12 Mar 2022 irb(main):002:0> Time.now => 2022-03-12 20:44:53 +0900 irb(main):003:0> I18n.l(Date.today) => "2022/03/12" irb(main):004:0> I18n.l(Time.now) => "2022年03月12日(土) 20時45分24秒 +0900"
default以外のフォーマットを指定する場合はI18n.l()
の第二引数にformat: :○○
と指定する。
ja: activerecord: errors: messages: # 中略 date: # 中略 formats: default: "%Y/%m/%d" long: "%Y年%m月%d日(%a)" short: "%m/%d"
https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/ja.yml
irb(main):005:0> I18n.l(Date.today, format: :long) => "2022年03月12日(土)" irb(main):006:0> I18n.l(Date.today, format: :short) => "03/12" irb(main):007:0> l(Date.today, format: :short) Traceback (most recent call last): 1: from (irb):7 NoMethodError (undefined method `l' for main:Object)
※ コンソールでは先頭のi18n
の記載が必要だが、コントローラやビューファイルでは省略可◎
<%= @board.created_at %>
<%= l @board.created_at, format: :long %>
参考にしたサイト
セレクトボックス
【Rails】完全理解 formでセレクトボックスをつくるselectの使い方 | WEB屋のメモ帳
ransack
【Rails】 ransackを使って検索機能がついたアプリを作ろう! | Pikawaka
enumのi18n対応
【Rails】enumをI18n対応させるenum_helpが便利すぎた - ひよっこエンジニアの雑多な日記