【RSpec単体テストの基本】何をテストするのか、利用するgem、作業の流れ、よく使うマッチャ他
- 結局テストで何を確認すべきなのか
- 単体テストで利用するgemと導入方法
- gem を bundle install した後の流れ
- deviseをrspecで使えるようにする
- モデルのテストコードの基本構造
- コントローラーのテストコードの基本構造
- よく使う単体テストのマッチャ(随時更新)
- よく使われるFactoryBotのメソッド(随時更新)
- よく使うFaker(随時更新)
- バリデーションのテストの具体例
- ■エラー備忘録(思わぬところでつまづいた!?)
【参考記事】
https://leanpub.com/everydayrailsrspec-jp/read#leanpub-auto-section-19
https://language-and-engineering.hatenablog.jp/entry/20091023/p1
単体テストでは、①「モデル」②「コントローラー」を1つずつテストする
結局テストで何を確認すべきなのか
①モデルの場合
・DBに対して,期待通りの操作が行なえているかどうか
・モデルの全メソッドを網羅
・バリデーション等の個々の性質・挙動もテスト
・某スクールのテキストに書いていたのは(1)のみだったが、念の為。
(1)バリデーション
オブジェクトがデータベースに保存される前にオブジェクトの状態を検証するバリデーション。そのバリデーションが本当に働いているのかを確認するテスト。
【例】
●名前、メアド、パスワードがあれば有効であること
(必要情報を入力していればバリデーションをクリアできるか)
●名前・メアドがなければ無効であること
(情報が不足していればバリデーションでブロックできるか)
●メアドが重複している場合は無効であること
(情報が重複している場合バリデーションでブロックできるか)
(2)インスタンスメソッド
モデル内に、インスタンスメソッドが定義されている場合に、そのメソッドが正しく動いているのか、期待する値を返すのかをテストする。
【例】
●モデルに「姓」「名」を結合する以下のようなインスタンスメソッドが定義されている場合
def
name
[
firstname
,
lastname
].
join
(
' '
)
end
(3)クラスメソッドとスコープ
検索等が正しく動作するか、検索条件にあった内容が返されるかをテストする。
【例】
●モデルに、searchメソッドやsearchスコープが定義されている場合
②コントローラーの場合
■某スクールのカリキュラム
1.アクション内で定義されているインスタンス変数の値が期待したものになるか
2.アクションの持つビューに正しく遷移するか
■参考サイト①
・リクエストに対して,期待通りのレスポンスが返ってくるか
・publicな全アクションを網羅
・「コントローラ」と「ビュー」をテスト
■参考サイト②
・Webリクエストが成功したか
・正しいページにリダイレクトされたか
・ユーザー認証が成功したか
・レスポンスのテンプレートに正しいオブジェクトが保存されたか
・ビューに表示されたメッセージは適切か
■参考サイト④
・受信したリクエストに対して適切なレスポンスを返すか
(例)リクエストに対してHTTPレスポンスがステータスコード200を返す
・ビューで使用するのに必要なモデルオブジェクトをロードするか
(例)リクエストされたURLから必要なモデルインスタンスをロード
・レスポンスを表示するのに適切なビューを選択するか
(例)適切なテンプレートを表示している
https://blog.naichilab.com/entry/2016/01/19/011514
単体テストで利用するgemと導入方法
●RSpec
・gem
・フレームワーク(必要な一般的な機能が、あらかじめ別に実装されたもの)
・RubyやRuby on Railsで作ったクラスやメソッドを
テストするためのドメイン特化言語 (DSL)を使ったフレームワーク
・要はテスト用のプログラミング言語
・development環境と test環境にインストールする
group :development, :test do #省略 gem 'rspec-rails' end
●rails-controller-testing
・gem
・コントローラーの単体テストで利用するgem
group :development, :test do #省略 gem 'rails-controller-testing' end
●web_console
・gem
・Ruby on Railsで作られたアプリ用のデバッグツール
・Rails 4.2以降は標準装備されている
・development環境のみにインストールする
group :development do gem 'web-console' end
※もしdevelopment環境以外にもインストールされていたらGemfileの記述を移動する
・デフォルトエラーページ用のデバッギングツールで、
ブラウザ上からインタラクティブにconsoleの操作が出来る
・エラーページだけでなく、任意のviewで表示する事も出来る。
表示させるには、表示させたいviewで`console`メソッドを呼び出すだけでOK
●factory_bot(旧:factory_girl_rails)
・gem
・簡単にダミーのインスタンスを作成することができる
・development環境と test環境にインストールする
group :development, :test do #省略 gem 'factory_bot_rails' end
・使い方は、specディレクトリの下に
「factories」というディレクトリを追加し
任意の名前のファイルを作成する。
・任意のファイル(今回はusers.rb)に
ダミーのインスタンスの情報を記入しておく
FactoryBot.define do factory :user do nickname {"abe"} email {"kkk@gmail.com"} password {"00000000"} password_confirmation {"00000000"} end end
・ダミーのインスタンスを呼び出す際には以下のような記述になる
#factory_botを利用しない場合 user = User.new(nickname: "abe", email: "kkk@gmail.com", password: "00000000", password_confirmation: "00000000")
#factory_botを利用する場合 (buildはただインスタンスを作るだけ) user = FactoryBot.build(:user)
#createしたインスタンスは、DBに保存される
user = FactoryBot.create(:user)
#ダミーのインスタンスを作成した上で、少し変えることも可能
user = FactoryBot.build(:user, nickname: "shinbo")
・さらに記述を省略することも可能!
FactoryBot.build(〜)のFactoryBotさえも省略する
ー省略手順:rails_helper.rb に以下の記述を追記
RSpec.configure do |config| #下記の記述を追加 config.include FactoryBot::Syntax::Methods end
ー省略後
#fFactoryBotさえ省略 user = build(:user)
●Faker
・gem
・emailや電話番号、名前などのダミーデータを作成するためのGem
・test環境にインストールする
group :test do gem 'faker', "~> 2.8" end
・ダミーデータの生成方法
{ Faker::Internet.email } => "rodrick.wyman@rosenbaum.org"
●結論:Gemfileで以下の記述をしてbundle install
group :development, :test do gem 'rspec-rails'
gem 'rails-controller-testing'
gem 'factory_bot_rails' end
group :development do
gem 'web-console'
end
group :test do
gem 'faker'
end
※バージョンは適宜追記(例:gem 'faker', "~> 2.8")
※重複しないように注意
gem を bundle install した後の流れ
手順① RSpecの必要ファイルを作成
ターミナルで以下のコマンドを入力
rails g rspec:install
以下の必要なファイルが生成される
create .rspec create spec create spec/spec_helper.rb create spec/rails_helper.rb
■rails_helper.rb
・RailsにおいてRSpecを利用する際に共通の設定を書いておくファイル
・各テストでこれを読み込むことで、共通の設定やメソッドを適用する
■spec_helper.rb
・RailsなしでRSpecを利用する場合のこうゆうファイル
手順② .rspecに以下を追記
--format documentation
RSpecの出力をドキュメント形式で読みやすくするためのオプション。
https://relishapp.com/rspec/rspec-core/v/2-4/docs/command-line/format-option
手順② .rails_helper.rbに以下を追記
RSpec.configure do |config| #下記の記述を追加 config.include FactoryBot::Syntax::Methods end
FactoryBot.build(〜)のFactoryBotさえも省略するための記述
手順③ ディレクトリ・ファイルを作成しコードを記入
■ディレクトリの構造
上記のようにspecディレクトリの配下に
「models」「controllers」「factories」のディレクトリを作成する
■コードを書くファイル
・モデル、コントローラそれぞれで
テストコードを書く場所を分ける
・factoriesではダミーのインスタンス変数を定義する
■テストファイルの命名規則
モデルのテスト:対応するクラス名_spec.rb
コントローラのテスト:対応するクラス名_controller_spec.rb
手順④テスト実行
ターミナルに以下のコマンドを入力
bundle exec rspec
テストの結果がターミナルに表示されるので、
結果にもとづいて修正する。
また、テストが複数ある場合は、
ファイルを指定してテストをすることも可能
bundle exec rspec spec/controllers/●●●_controller_spec.rb
deviseをrspecで使えるようにする
deviseを用いた会員登録の場合、deviseをrspecでも使用できるようにする必要があります。
手順①loginメソッドを定義
まず、/spec/supportディレクトリに、controller_macros.rbを作成し、loginメソッドを定義します。
/spec/support/controller_macros.rb
module ControllerMacros def login(user) @request.env["devise.mapping"] = Devise.mappings[:user] sign_in user end end
手順②rails_helper.rbに読みこむ
その後、rails_helper.rbに、deviseのコントローラのテスト用のモジュールと、先ほど定義したControllerMacrosを読み込む記述を行います。
/spec/rails_helper.rb
RSpec.configure do |config| Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } config.include Devise::Test::ControllerHelpers, type: :controller config.include ControllerMacros, type: :controller #〜省略〜 end
※番外編※ エラーになる場合
エラーになる場合、rails_helper内の
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
の記述のコメントアウトを外す
# 修正前 # Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } # 修正後 Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
モデルのテストコードの基本構造
コントローラーのテストコードの基本構造
describe クラス名(複数形・頭文字のみ大文字)Controller do describe 'HTTPメソッド名 #アクション名' do it "インスタンス変数は期待した値になるか?" do "擬似的にリクエストを行ったことにするコードを書く" "エクスペクテーションを書く" end it "期待するビューに遷移するか?" do "擬似的にリクエストを行ったことにするコードを書く" "エクスペクテーションを書く" end end
よく使う単体テストのマッチャ(随時更新)
・eq
expect(assigns(:group)).to eq group
・include
expect(message.errors[:group]).to include('を入力してください')
・be_valid
expect(build(:message, image: nil)).to be_valid
・render_template
expect(response).to render_template :index
・be_a_new
expect(assigns(:message)).to be_a_new(Message)
・redirect_to 引数にとったプレフィックスにリダイレクトした際の情報を返す
expect(response).to redirect_to(new_user_session_path)
・change 引数が変化したかどうかを確かめる
expect{ subject }.to change(Message, :count).by(1)
よく使われるFactoryBotのメソッド(随時更新)
・build インスタンスを作る
・create インスタンスを作る(一時的にDBにも保存する)
・create_list factory_botで設定されたリソースを元に配列を作る
・attributes_for オブジェクトを生成せずにハッシュを生成する
よく使うFaker(随時更新)
バリデーションのテストの具体例
- nicknameとemail、passwordとpassword_confirmationが存在すれば登録できること
- nicknameが空では登録できないこと
- emailが空では登録できないこと
- passwordが空では登録できないこと
- passwordが存在してもpassword_confirmationが空では登録できないこと
- nicknameが7文字以上であれば登録できないこと
- nicknameが6文字以下では登録できること
- 重複したemailが存在する場合登録できないこと
- passwordが6文字以上であれば登録できること
- passwordが5文字以下であれば登録できないこと
■自分用コード備忘録
やはりコードを見た方が復習になると思いました…
■エラー備忘録(思わぬところでつまづいた!?)
■FactoryBotのダミーの作り方
【現象】
上記のような選択肢を表示し選ばせ、
DBにIDを記録するカラムのテストを行う。
ダミーデータでは、IDを選択肢として入力していた。
その結果、
ArgumentError is not a valid カラム名
というエラーが表示された
【解決策】
https://programming-beginner-zeroichi.jp/articles/225
Enumで選択肢を表示し、閲覧者が選択した者のIDをDBに保存するような場合、テストにおけるダミーデータでIDで書くとエラーになる。
【正解コード】
【補足】
上記のようにEnumではなく、viewファイルのselectで選択肢を作成している場合は、数字で書く必要がある。
今回の場合、category_idはerumではなくselectで作成していたので、category_idだけ数字表記。
【備忘録】最新のコメントのカスタムデータ属性を取得する
【現状】
・chat-spaceというチャットアプリの作成中
・各メッセージにはカスタムデータ属性「data-message-id」が付与
【目的】
最新のメッセージの「data-message-id」の値を取得する
【コード】
【ポイント①】dataメソッド
・カスタムデータ属性の値を取得するメソッド
<section id="blog" data-author="Taro">
var blog = $("#blog"); alert(blog.data('author'));
【ポイント②】$(":last")
・ :last を設定すると最後の要素を選択する
・単独では利用しない
・ $("div:last")「複数あるdiv要素のうち最後のものを取得」
参考記事:http://www.jquerystudy.info/reference/selectors/last.html
【備忘録】あいまい検索(インクリメンタルサーチで似たユーザーを表示させる)
非同期通信で、
入力された値をparamsで送る
(非同期通信のdataは、必ずハッシュ型になる)
let input = $("#user-search-field").val(); $.ajax({ type: 'GET', url: '/users', data: { keyword: input }, dataType: 'json' }) .done(function(users) { console.log("成功です"); }) .fail(function() { console.log("失敗です"); });
コントローラーで
データベースとのやりとりを記載する
・whereは 条件「〜ならば」
・where.notは 条件「〜でなければ」
User.where( ' カラム名 like ?' , ' % 検索したい文字列 % ' )
%で挟むと、「文字列のどの部分にでも検索したい文字列が含まれていればOK」という意味になる
@users = User.where(['name LIKE ?', "%#{params[:keyword]}%"] ).where.not(id: current_user.id).limit(10)
【参考記事】 Rails - LIKE句を使った文字のあいまい検索(特定の文字を含む語句を曖昧検索したい場合) - Qiita
【備忘録】IFの冗長な文章を三項演算子で省略
現状
・LINEのようなチャットアプリを作成中
・メッセージを非同期通信で保管・表示させたい
・メッセージの「SEND」ボタンを押すと非同期通信が発火
・文字のみ、画像のみ、文字と画像のそれぞれのメッセージを表示する
以下のようなjavascriptを書いたところ
buildHTML()の動作のIF分が冗長すぎるとのレビューがあったので
三項演算子で修正
$(function() { function buildHTML(message){ if (message.image.url) { if (message.body) { var html = `<div class="message"> <div class="upper-message"> <div class="upper-message__user-name"> ${message.name} </div> <div class="upper-message__date"> ${message.created_at} </div> </div> <div class="lower-meesage"> <p class="lower-message__content"> ${message.body} </p> <img class = "lower-message__image" src = "${message.image.url}" > </div> </div>` } else { var html = `<div class="message"> <div class="upper-message"> <div class="upper-message__user-name"> ${message.name} </div> <div class="upper-message__date"> ${message.created_at} </div> </div> <div class="lower-meesage"> <img class = "lower-message__image" src = "${message.image.url}" > </div> </div>` } } else { var html = `<div class="message"> <div class="upper-message"> <div class="upper-message__user-name"> ${message.name} </div> <div class="upper-message__date"> ${message.created_at} </div> </div> <div class="lower-meesage"> <p class="lower-message__content"> ${message.body} </p> </div> </div>` } return html } $('.messages').animate({ scrollTop: $('.messages')[0].scrollHeight }); $("#new_message").on("submit", function(e) { e.preventDefault(); var formData = new FormData($(this).get(0)); var url = $(this).attr('action'); $.ajax({ url : url, type : 'POST', processData: false, data : formData, dataType : "json", processData : false, contentType : false }) .done(function(data){ var html = buildHTML(data); $('.messages').append(html); $('.messages').animate({ scrollTop: $('.messages')[0].scrollHeight}); $("#new_message")[0].reset(); $('.form__submit').prop('disabled', false); }) .fail(function(){ alert("メッセージ送信に失敗しました"); $("#new_message")[0].reset(); $('.form__submit').prop('disabled', false); }) }); });
改善
buildHTMLの部分のIF文を三項演算子で修正
function buildHTML(message){ var message_body = message.body? message.body : "" ; var message_image = message.image.url? message.image.url : "" ; var html = `<div class="message"> <div class="upper-message"> <div class="upper-message__user-name"> ${message.name} </div> <div class="upper-message__date"> ${message.created_at} </div> </div> <div class="lower-meesage"> <p class="lower-message__content"> ${message_body} </p> <img class = "lower-message__image" src = "${message_image}" > </div> </div>` return html }
条件式 ? Trueの処理 : Falseの処理
【メモ①】
参考演算子の条件式は、
message.body?(メッセージという変数のbodyというカラムに値がありますか?)
というような書き方でも動く。
参考記事では、比較演算子やtrue falseで条件式を
書くサイトが多かったが上記のような条件式でも動いた!
【メモ②】
最初は、三項演算子のtrueの記述部分で${ }をつけていたためエラーに。
${ }は、変数で呼び出したあとの記述に記載しておけばOK
function buildHTML(message){ var message_body = message.body? <b>${message.body}</b> : "" ; var message_image = message.image.url? <b>${message.image.url} </b>: "" ;
【参考記事】
【JavaScript入門】条件(三項)演算子の使い方と活用例まとめ! | 侍エンジニア塾ブログ(Samurai Blog) - プログラミング入門者向けサイト
【備忘録】jqueryでformのresetが効かない
【修正点】
これを
こう
【参考記事】
https://blog.dododori.com/create/program/jquery-reset/
大変参考になりました🙇♀️
【備忘録】formDataのエラーを解消
【問題】
・どうあがいてもformDataを用いてデータベースに保存ができなかった
・$.ajax({ の data でハッシュ形式でparamsを記入し無理やり動かしていたが
画像投稿→保存で詰み
【修正①】
これを
こう
修正点: $(this).get(0)
【修正②】
これを
こう
修正点:processData: false,を追加
【参考記事】
https://qiita.com/sho012b/items/f2558db5dad97d7e1b1d
引用元が明記されていて素晴らしい記事でした🙇♀️
【備忘録】jQueryでアップロードした画像の情報を取得
【前提】
rails', ' 5.0.7.2'
carrierwaveで画像をアップロード
jQueryで非同期通信の実装中
【手順①】画像の情報をpropで取得
var file = $('画像アップロードのオブジェクト').prop('files')[0];
file という変数に、アップロードした画像の情報を格納
【手順②】取得したい情報を指定
変数.name でファイルの名前(IMG_6583.JPGなど)を取得
変数.size でファイルのサイズ(1.5 MBなど)を取得
【参考記事】
https://www.flatflag.nir87.com/prop-1812