二足のわらじ

〜プログラミングとSEOの勉強をはじめたミーハー26歳のメモ帳〜

【RSpec単体テストの基本】何をテストするのか、利用するgem、作業の流れ、よく使うマッチャ他

 

 

【参考記事】

https://leanpub.com/everydayrailsrspec-jp/read#leanpub-auto-section-19 

https://language-and-engineering.hatenablog.jp/entry/20091023/p1

https://y-yagi.tumblr.com/post/96428108880/rails%E3%81%AEweb-console%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

 

単体テストでは、①「モデル」「コントローラー」を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

フレームワーク必要な一般的な機能が、あらかじめ別に実装されたもの

RubyRuby 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の記述を移動する

 

https://y-yagi.tumblr.com/post/96428108880/rails%E3%81%AEweb-console%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

・デフォルトエラーページ用のデバッギングツールで、

 ブラウザ上からインタラクティブにconsoleの操作が出来る

・エラーページだけでなく、任意のviewで表示する事も出来る。
 表示させるには、表示させたいviewで`console`メソッドを呼び出すだけでOK

 

 

●factory_bot(旧:factory_girl_rails

・gem

・簡単にダミーのインスタンスを作成することができる

・development環境と test環境にインストールする

 

group :development, :test do
  #省略
  gem 'factory_bot_rails'
end

 

・使い方は、specディレクトリの下に

「factories」というディレクトリを追加し

 任意の名前のファイルを作成する。

f:id:vamnasocana:20200125211535p:plain

・任意のファイル(今回は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さえも省略するための記述

 

手順③ ディレクトリ・ファイルを作成しコードを記入

f:id:vamnasocana:20200125221312p:plain

ディレクトリの構造

上記のように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 }

 

 

モデルのテストコードの基本構造

require 'rails_helper'
 
describe クラス名(単数形・頭文字のみ大文字) do
 describe 'アクション名(#index等)' do
 it "検証する内容の説明" do
  〜〜↓検証の具体的な記述↓〜〜
   user = User.new(nickname: "", email: "kkk@gmail.com", password: "00000000", password_confirmation: "00000000")
   user.valid?
   expect(user.errors[:nickname]).to include("can't be blank")
  〜〜↑検証の具体的な記述↑〜〜
  end
 end
end

 

コントローラーのテストコードの基本構造

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(随時更新)

password = Faker::Internet.password(min_length: 8)
name {Faker::Name.last_name}
email {Faker::Internet.free_email}
name {Faker::Team.name}
 

 

 

 バリデーションのテストの具体例

  1. nicknameとemail、passwordとpassword_confirmationが存在すれば登録できること
  2. nicknameが空では登録できないこと
  3. emailが空では登録できないこと
  4. passwordが空では登録できないこと
  5. passwordが存在してもpassword_confirmationが空では登録できないこと
  6. nicknameが7文字以上であれば登録できないこと
  7. nicknameが6文字以下では登録できること
  8. 重複したemailが存在する場合登録できないこと
  9. passwordが6文字以上であれば登録できること
  10. passwordが5文字以下であれば登録できないこと

 

■自分用コード備忘録

やはりコードを見た方が復習になると思いました…

https://www.evernote.com/client/web?login=true#?anb=true&b=68f5244c-58cb-4e81-92d2-0b0440acd3ca&n=46cedf10-e453-418f-91e7-1a68392eec09&s=s485&search=v4&

 

 

 ■エラー備忘録(思わぬところでつまづいた!?)

■FactoryBotのダミーの作り方

【現象】

f:id:vamnasocana:20200126174952p:plain

上記のような選択肢を表示し選ばせ、

DBにIDを記録するカラムのテストを行う。

ダミーデータでは、IDを選択肢として入力していた。

f:id:vamnasocana:20200126174742p:plain

 

その結果、

ArgumentError is not a valid カラム名

というエラーが表示された

 

【解決策】

https://programming-beginner-zeroichi.jp/articles/225 

Enumで選択肢を表示し、閲覧者が選択した者のIDをDBに保存するような場合、テストにおけるダミーデータでIDで書くとエラーになる。

f:id:vamnasocana:20200126174511p:plain

【正解コード】

f:id:vamnasocana:20200126174641p:plain

【補足】 

f:id:vamnasocana:20200126174430p:plain

 上記のようにEnumではなく、viewファイルのselectで選択肢を作成している場合は、数字で書く必要がある。

今回の場合、category_idはerumではなくselectで作成していたので、category_idだけ数字表記。

【備忘録】最新のコメントのカスタムデータ属性を取得する

 

【現状】

・chat-spaceというチャットアプリの作成中

・各メッセージにはカスタムデータ属性「data-message-id」が付与

f:id:vamnasocana:20191207165031p:plain

 

 

【目的】

最新のメッセージの「data-message-id」の値を取得する

 

【コード】
 
last_message_id = $('.message:last').data('message-id');
console.log(last_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) - プログラミング入門者向けサイト

【備忘録】formDataのエラーを解消

 

【問題】

・どうあがいてもformDataを用いてデータベースに保存ができなかった

・$.ajax({ の data でハッシュ形式でparamsを記入し無理やり動かしていたが

 画像投稿→保存で詰み

 


$("#new_message").on("submit", function(e) {
e.preventDefault();
var formData = new FormData(this);
console.log(formData);
var url = $(this).attr('action');
formData.append('body', $('.form__message').val());
 
$.ajax({
url : url,
type : 'POST',
data : { "message" : { "body" : $('.form__message').val() } },
dataType : "json",
// processData : false,
// contentType : false
})
.done(function(data){
var html = buildHTML(data);
$('.messages').append(html);
$('.form__message').val('');
$('.form__submit').prop('disabled', false);
})
.fail(function(){
alert('error');
})
});

 

 

 

【修正①】

var formData = new FormData(this);

これを

var formData = new FormData($(this).get(0));

こう

修正点: $(this).get(0)

 

 

【修正②】

これを

$.ajax({
url : url,
type : 'POST',
data : { "message" : { "body" : $('.form__message').val() } },
dataType : "json",
processData : false,
contentType : false
})

こう

$.ajax({
url : url,
type : 'POST',
processData: false,
data : formData,
dataType : "json",
processData : false,
contentType : false
})

 

修正点:processData: false,を追加

 

 

【参考記事】

https://qiita.com/sho012b/items/f2558db5dad97d7e1b1d

引用元が明記されていて素晴らしい記事でした🙇‍♀️

 

 

 

 

 

 

 

 

 

【備忘録】jQueryでアップロードした画像の情報を取得

 

【前提】

rails', ' 5.0.7.2'

carrierwaveで画像をアップロード

jQueryで非同期通信の実装中

 

 

【手順①】画像の情報をpropで取得

 

var file = $('#message_image').prop('files')[0];

 

var file = $('画像アップロードのオブジェクト').prop('files')[0];

file という変数に、アップロードした画像の情報を格納

 

 

【手順②】取得したい情報を指定

 

console.log(file.name);

変数.name  でファイルの名前(IMG_6583.JPGなど)を取得

 

console.log(file.size);

変数.size  でファイルのサイズ(1.5 MBなど)を取得

 

 

 【参考記事】

https://www.flatflag.nir87.com/prop-1812