働きながらでも技術ブログを続ける方法

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

こちらの記事で2月中の毎日投稿終了です!!!!!!!

本当にお疲れ様!自分!!!!

正直ネタが切れたりエラーに詰まったりして書くのがすごくしんどくなり、低クオリティ記事を量産してしまったかもしれません、、、。でも、めげることなく続けてきた今、ブログを効率的に書く方法だったり、自分の考えを発信する習慣が付いたりと、1ヶ月前よりも執筆力はおそらく上がったかなと思っております!働きながら、RUNTEQのカリキュラムをしながらも行えたので、相当の自信もつきました!

2月最後の記事は締めとして、28日間働きながらもブログを続けた自分の技術ブログの続け方について紹介しようかなと思います!

Notionを使おう

これだけは断言できます。

技術ブログ(はてなブログ、Qiita)を書くのであれば、下書きの記事は全てNotionで書くようにしましょう。

こちらについては今月自分が出した記事でもまとめています!

sakitadaiki.hatenablog.com

こちらの記事でも説明していますが、技術ブログを書くことにおいてNotionで下書きを書くメリットについて説明すると、

  • マークダウン記法が簡易的に書くことができる
    • コード(単語)のマークダウン(⌘ + E)

    例: userscreate

    • コード(複数行)のマークダウン(```(バッククオーテーションを3つ書く))

    例:

    def create
      user = User.find_by(email: session_params[:email])

      if user&.authenticate(session_params[:password])
        session[:user_id] = user.id
        redirect_to root_url, notice: 'ログインしました'
      else
        render :new
      end
    end
  • Notionで書いたマークダウン記法はコピペだけでブログ記事に反映される

https://i.gyazo.com/226654b7308173a45a1f4399086ab3c1.gif

正直なところ、Notionの機能は他にも様々です。ドキュメントの共有URLを発行して簡単に共有が行えたりしますし、レイアウトや仕事効率化という面ではもはやワードなどのソフトに取って変えられることも可能だと思っています。他の機能も本当はたくさん紹介したいです。

しかしながら、こと技術ブログにおいて、Notionのメリットは明らかに突出していると考えています。マークダウン記法がそのまま反映されるというだけで、マークをつける手間が格段に少なくなりますからね。

あと、勉強した内容を記事ネタとしてメモに残しておくこともできます。自分が勉強したことをあらかじめまとめておいて、隙間時間に編集することもできるので、時間がない方でもブログを書く準備ができたりもします。

https://i.gyazo.com/4efc2ef584eac66d8cc646b1985b2eb8.png

ですので、技術ブログを書く上では必ずNotionを使いましょう!本当に楽なので!

明日投稿する記事は今日書こう

ある程度の文量でブログを1記事書くためには時間は最低でも1-2時間は必要です。1-2時間ってあるように見えて実は少ないです。自分も2時間を執筆時間に設定していましたが、何度も超過して焦りました。加えて、日々ネタを探さなければいけないので、ネタ探しの圧迫感もあります。しかも、人に見られるわけですから、ある程度内容が整っていないといけません。ブログを毎日投稿するということは時間、ネタ探し、記事を見る人という3つの圧迫感を感じながら投稿しないといけません。特に時間とネタ探しの圧迫感は凄まじいもので、今日終わっても明日も書かなくてはいけないというのは、かなり焦りを感じていました。

f:id:SakitaDaiki:20210225114840p:plain

その圧迫感を少しでも軽減する方法として、明日投稿する記事は今日終わらせておくという方法があります。自分もこれを実践していなければ毎日投稿を断念していたと思います。

なぜ今日中に用意しておいた方がいいのかというと、明日の記事を執筆する時間を設けたとしても、その時間で十分足りるのかが不明であるからです。

たとえば、みなさんが自分のように働きながら技術ブログを毎日続けるとします。人によっては突然残業をしなければいけなくなったり、大幅に電車が遅延して家に着くのが遅れるということもあるかと思います。そうなった時に、ブログへの執筆時間のために他の時間を削ったり、執筆時間自体が短くなったりします。そうすると、圧迫感は増す一方です。人によっては、「今日は書かなくてもいいや」と言って、自分が立てた毎日投稿という目標を断念する人もいるかと思います。明日の予定がどうなるかはわからないので、ブログのために設けた時間がそのままフルに使えるかは前日にわからないということです。

f:id:SakitaDaiki:20210225115048p:plain

そうならないためにも、前日に記事を書いておくことはその圧迫感から逃れる方法として最適だと思います。明日何があったとしても記事はすでにできているので、朝に投稿ボタンを押すだけでもうその日の投稿は完了です。また、突然の予定などでブログの執筆時間が少なくなっても、それに合わせたネタを選べばいいのです。例えば、もともとは2時間はかかりそうな記事ネタを選んでいたが、急な予定のために執筆時間を減らさなければならなくなったとします。そうしたら、ネタの中から時間があまりかからなそうなものや、執筆途中のものなどを選べばいいのです!

前日に記事を終わらせておくことで、ブログを継続するための心の余裕を保つことが可能になるでしょう。

初学者のために書く意識

技術ブログを書き始めた当初は、「技術ブログは自分の就活のため」としか思っていませんでした。というか毎日投稿を続けたのもそれが理由です。毎日投稿を続けた記録はブログに残るので、その実績はきっと就活でアピールできるのかなと思いました。

しかし、ブログを執筆し始めて10日ほど立った頃、始めた当初に比べてブログへのモチベーションがかなり下がっていました。どうしてモチベーションが下がったのかというと、自分のことしか考えずに技術ブログを書いていたからです。

技術ブログはどのように使っていますでしょうか?それは、自分がエラーやわからないところで詰まってしまった時に利用しているのではないでしょうか?そうです、本来技術ブログは自分と同じようにプログラミングを学ぶ人たちが見るブログです。自分の記事を読む人も少ないだろうしと考え、また、就活のためという目的で書いていたために、書くことを継続しようとしていて、他人に見せることをあまり意識していませんでした。

就活のアピールのために書くこともできますが、それはあくまで主目的であってはいけません。なぜなら、自分のために書いていれば、「自分がわかればそれでいい記事」を量産してしまうからです。他人のために書くのであれば、「他人がわかるように書いた記事」を書くことができます。

自分はそれを意識し始めてから書き方を帰ることができたと思います。まず第一に、初学者がハンズオンで実装できる内容にしました。例えば、途中から現場Railsのアプリに付け加える方法で実装をしたりしました。こうすれば、初学者の方でも自分の手元で確認して実装できると思ったからです。

[現場Railsを使用し始めた記事]

sakitadaiki.hatenablog.com

さらに、Gyazoの画像やGIFを貼り付けることで、自分の画面を見せながら初学者が確認できるようにもしました。

[Gyazoを使って画面をキャプチャした記事]

sakitadaiki.hatenablog.com

技術ブログでたまに見かけるコードだけはっつけて説明されているものがありますが、自分にはとてもわかりにくいと感じていました。筆者の画面が実際にどうなっているのかを確認できたらいいのにと思っていました。なので、自分の画面のキャプチャは実装したら必ず取ってブログに貼り付けるようにしました。

こうして、就活でアピールする目的から、初学者に見せる目的にシフトさせたことで、記事を書くのに工夫を取り入れたりすることが増え、モチベーションもかなり上がりました!

終わりに

今回は28日毎日投稿の最後の記事として、毎日投稿を続けるコツを紹介しました!

さて、28日間の毎日投稿をしたからといって終わりではありません。この28日チャレンジはあくまでもブログを書く習慣を身に付けるためのものであり、これでブログをキッパリ終えてしまったら、このチャレンジの意味が無くなってしまいます。

来月からは主にポートフォリオ制作に注力します。加えて、vue.jsの学習も取り入れるつもりですので、おそらくですが、vue.jsに関する記事を多く書くのかなと個人的に思っております。これからも定期的にブログは更新していくので何卒よろしくお願いします。

【自戒】継承関係を把握しようという話。

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

2月の毎日投稿達成まで本記事を合わせて残すところ2つとなりました。

正直に言うと最近はPF課題で詰まってましたので、新しい知識が学べない状況でして、そろそろネタ切れ感がうっすらと伝わってないか心配ですw

今回の記事は「継承関係はしっかり意識しよう」です。これは自戒の意味を込めて記事にしようと思い至りました。継承関係なんて超初学者向けと思われますが、当然自分も継承関係自体は理解していました。しかし、継承関係は視野が狭いととても見えづらいものになります。

この記事を書こうと思った発端はまさにこの継承関係を把握してなかったことから起こりました。継承関係を把握しておらず簡単な実装ができない理由を探すのに1日以上詰まってしまったお話を自戒の念を込めてさせていただきます!

またAPIについてよく理解していない方はまずはAPIについて勉強してください!API知らないと理解できないので。。。

どのような実装をしたいのか

本記事ではRUNTEQの課題のコードを使用しますので、コード自体はあまり見せられませんがご容赦ください。テストする内容はユーザーの新規登録機能のAPIです。登録されたユーザーの情報をAPIとして取得します。

{
    "data": {
        "id": "1",
        "type": "user",
        "attributes": {
            "name": "sample_name",
            "email": "sample@example.com"
        }
    }
}

上記のJSONを取得するためにPostmanで送るパラメータは以下の画像です。

https://i.gyazo.com/5a55c22be135f92f9bd04b16d31f3fd5.png

しかし、nameのパラメータを消して、emailを同じままで送ると下記のようなエラーメッセージのJSONが返ってきます。

{
    "message": "Bad Request",
    "errors": [
        "Name can't be blank",
        "Email has already been taken"
    ]
}

これはUserモデルにてバリデーションが設定されているからです。

# app/model/user.rb
class User < ApplicationRecord
・
  validates :name, presence: true
  validates :email, presence: true, uniqueness: trueend

今回実装方法に苦闘したのはまさにこの部分です。自分が実装したかった内容は、「JSONのerrorsにバリデーションエラーを表示させる」ことです。

現状のエラーハンドリング

実際のところ、パラメータが誤っている時下記JSONを出力するところまでは実装できました。

{
    "message": "Bad Request",
    "errors": [
        "ActionController::BadRequest"
    ]
}

このエラーはどのように表示しているのかというと、まず間違ったパラメータが渡った時にraiseメソッドで例外を明示的に起こすようにしました。@user.savefalseであれば例外が発生します。

# app/controllers/api/v1/registrations_controller.rb
raise ActionController::BadRequest unless @user.save

ActionController::BadRequestは別ファイルでrescue_fromでエラーハンドリングをしています。

これにより、ActionController::BadRequestのエラーが発生した時に行う処理をメソッドで設定することができます。今回のファイルでいえば、rescue400ActionController::BadRequest発生時に行う処理です。

module ApiErrorHandle
  extend ActiveSupport::Concern
  included do
    rescue_from ActionController::BadRequest, with: :rescue400
  end
・
・
・
    def rescue400(exception = nil, messages = nil)
    render_error(400, 'Bad Request', exception&.message, *messages)
  end

  private

  def render_error(code, message, *error_messages)
    response = {
      message: message,
      errors: error_messages.compact
    }

    render json: response, status: code
  end

rescue400のメソッドは基本的にprivateメソッドであるrender_errorとほぼ同じです。しかし、ActionController::BadRequestが発生しているので、引数にHTTPステータスコードである400と、エラーメッセージ'Bad Request'をそれぞれ指定しています。

第三引数について、exceptionの中身は#<ActionController::BadRequest: ActionController::BadRequest>となっておりnilではないのでexception&.messageでエラーは起きず"ActionController::BadRequest"を返しています。第四引数のmessagesrescue400メソッドの第二引数デフォルト値nilを返しています。

[1] pry(#<Api::V1::RegistrationController>)> exception
=> #<ActionController::BadRequest: ActionController::BadRequest>
[2] pry(#<Api::V1::RegistrationController>)> exception&.message
=> "ActionController::BadRequest"
[3] pry(#<Api::V1::RegistrationController>)> messages
=> nil

そして、render400メソッドの中で実行されるrender_errorメソッドですが、上述した通り、第一引数は400で第二引数は'Bad Request'です。第三引数である*error_messagesexception&.messageで出力された"ActionController::BadRequest"のみですので、要素数が一つの配列で["ActionController::BadRequest"]と出力されます。

[1] pry(#<Api::V1::RegistrationController>)> code
=> 400
[2] pry(#<Api::V1::RegistrationController>)> message
=> "Bad Request"
[3] pry(#<Api::V1::RegistrationController>)> error_messages
=> ["ActionController::BadRequest"]

よって変数responseの中身は以下のようになります。

[1] pry(#<Api::V1::RegistrationController>)> response
=> {:message=>"Bad Request", :errors=>["ActionController::BadRequest"]}

render json: response, status: coderesponseがレンダーされるわけですので、現状のJSONファイルがレンダリングされるわけです。

[再掲]

{
    "message": "Bad Request",
    "errors": [
        "ActionController::BadRequest"
    ]
}

この時の自分は完全にこのエラーハンドリングのロジックを理解していました。そこまで難しいわけでもなかったですし、このエラーハンドリングの実装自体は1つ、2つ前の課題でも実装済みです。

実際、何が問題かは把握していました。現在取得しているエラーメッセージは引数で指定したものですが、これをモデルのバリデーションエラーのエラーメッセージで指定できれば、バリデーションエラーをJSON形式で表示できるはずです。

{
    "message": "Bad Request",
    "errors": [
        "Name can't be blank",
        "Email has already been taken"
    ]
}

しかしながら、それの実装方法がわからなかったのです。バリデーションエラーをどう取得していいのかがわからなかったのです。

エラーハンドリングしているモジュールはユーザー登録用のコントローラーと違うファイルにあるので、モジュール内で@user.errors.full_messagesは取得できません。ですので、コントローラー内でエラーハンドリングをする必要があるのですが、エラーハンドリングようのモジュールを設けているのでコントローラーにも別でエラーハンドリングのロジックを記述するのは汚いコードを書くことになるのでできれば避けるべきです。

この部分で完全に自分は詰まってしまいました。

継承関係を理解する

仕方なく質問したところ講師から以下の返答をもらいました。

rescue400のメソッドはこのエラーハンドラーのモジュール内だけでしか利用しない想定ですかね?直接このメソッドを利用したりできそうな気がしませんか?

ぴかーーーーーーーーーーーーーーーん!!!!!

脳に電光石火が走るぐらいピカんとしました笑

rescue400メソッドはモジュール内でしか利用できないと思い込んでいました。しかし、APIのエラーハンドリングのモジュールはBaseControllerでincluedeしてました。

module Api
  module V1
    class BaseController < ApplicationController
      include ApiErrorHandle

そして、ユーザー登録用のコントローラーはBaseControllerを継承していることがわかりました。

class RegistrationsController < BaseController

つまり、ユーザー登録用のRegistrationsControllerでもAPIのエラーハンドリングのモジュールで定義されているメソッドを使用可能ということです!!!!これがわかった時本当に気持ちよかったです!

下記のように定義していたcreateアクションを変更しました。

def create
  @user = User.new(user_params)
  raise ActionController::BadRequest unless @user.save
  json_string = UserSerializer.new(@user).serialized_json
  render json: json_string
end

↓                

def create
  @user = User.new(user_params)
  if @user.save
    json_string = UserSerializer.new(@user).serialized_json
    render json: json_string
  else
    rescue400(nil, @user.errors.full_messages)
  end
end

これで再度誤ったパラメータ(nameのパラメータなし、emailを登録されたものにする)を送信すると、期待した通りのJSONが返却されました!!

{
    "message": "Bad Request",
    "errors": [
        "Name can't be blank",
        "Email can't be blank"
    ]
}

なんで解決できないかわからなかったのですが、本当に簡単に解決できましたね。

めちゃくちゃスッキリしました笑

終わりに

今回なぜこのように詰まったのかというと、親クラスがmoduleを継承していたことを完全に失念していたことにあります。本当に深く反省しています。そのためこの記事を書きましたし。

自戒の念もしっかり込めたのでこれぐらいで勘弁していただければと思いますww

さてさて、明日はいよいよ28日目の投稿!長かった毎日投稿も明日で最後です!

明日の記事ですが、技術ブログではなくブログの続け方について書いていこうと思います!

以上、大ちゃんの駆け出し技術ブログでした!

partialについて解説

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

本記事では複数のページで共通のファイルを利用するための機能であるパーシャルについて解説をします。

基礎編を学習していた時のメモを見つけたため、初学者向けに記事を作ろうかなと思ったためです。 では早速見ていきましょう。

パーシャルとは

Ruby on Railsでは度々目にするパーシャルですが、一体どのようなものでしょうか?

Rails学習の初期段階では、「複数のページで同じレイアウト部分のファイルを共通化する方法」と説明されることが多いかと思います。

例えば、簡単なタスク管理アプリがあり、中に新規タスク登録ページとタスク編集ページがあるとします。

新規タスク登録ページ e4b93bb581673f73b2a795aaa5233fe6.png

タスク編集ページ 45a46fc86db90f64820687aba42a9e7a.png

一目瞭然ですが、レイアウトがほとんど一緒です。 わかりやすい例を見せるためにシンプル過ぎるレイアウトを使用していますが、サイトのレイアウトの統一感のために、複数のページで似たようなレイアウトになるのはよくありそうです。

実際、これら2つのページのファイルの構成も共通部分が多くなり、無駄が多いように思えます。(RubyDRY(Don't Repeat Yourself)に反していますね、、)。form_withから下は全く同じですよね。 なお、今回は参考にしたテキストでerbではなくslimが使われていたので、そのまま使用していますmm

新規タスク登録ページのファイル

h1 タスクの新規登録

.nav.justify-content-end
  / tasks_path = /tasks
  = link_to '一覧', tasks_path, class: 'nav-link'

= form_with model: @task, local: true do |f|
  .form-group
    = f.label :name
    = f.text_field :name, class: 'form-control', id: 'task_name'
  .form-group
    = f.label :description
    = f.text_area :description, rows: 5, class: 'form-control', id: 'task_name'
  = f.submit nil, class: 'btn btn-primary'

タスク編集ページのファイル

h1 タスクの編集

.nav.justify-content-end
  = link_to  "一覧", tasks_path, class: 'nav-link'

= form_with model: @task, local: true do |f|
  .form-group
    = f.label :name
    = f.text_field :name, class: 'form-control', id: 'task_name'
  .form-group
    = f.label :description
    = f.text_area :description, rows: 5, class: 'form-control', id: 'task_name'
  = f.submit nil, class: 'btn btn-primary'

パーシャルを使用して共通化

本題であるパーシャルを使った共通化をします。

まずはじめにやることは、パーシャルテンプレートとしてapp/views/tasks/_form.html.slimというファイルを作成します。 そしての新規タスク登録ページのファイルとタスク編集ページのファイルの共通部分を貼り付けます。( 2つのファイルから共通部分は削除しておきましょう!) また、インスタンス変数@tasktaskに変更してください。(後ほど理由を説明します!)

= form_with model: task, local: true do |f|
  .form-group
    = f.label :name
    = f.text_field :name, class: 'form-control', id: 'task_name'
  .form-group
    = f.label :description
    = f.text_area :description, rows: 5, class: 'form-control', id: 'task_name'
  = f.submit nil, class: 'btn btn-primary'

原則として_form.html.slimのように、テンプレートファイルの名前の先頭には_(アンダースコア)を付けるのが一般的です。

次に、すっきりした新規タスク登録ページのファイルとタスク編集ページのファイルに同じコード render partial: "form", locals: { task: @task } を記載します。

h1 タスクの新規登録

.nav.justify-content-end
  = link_to '一覧', tasks_path, class: 'nav-link'

= render partial: "form", locals: { task: @task }
h1 タスクの編集

.nav.justify-content-end
  = link_to  "一覧", tasks_path, class: 'nav-link'

= render partial: "form", locals: { task: @task } 

render partial: "form", locals: { task: @task }のオプション部分partiallocalsは一般的には省略されますが、説明のため敢えて省略しない書き方にしてあります。省略するとrender "form", { task: @task }と書きます。

まず、partial:で読み込むテンプレートファイルを指定します。 今回は同じフォルダにテンプレートファイルがあるので"form"で読み込むことができますが、別フォルダにパーシャルファイルがある場合は、フォルダも含めて読み込むファイルを指定する必要があります。

locals:は一見わかりにくいのですがざっくり説明すると、インスタンス@taskの中身をローカル変数taskに入れてパーシャルのテンプレートファイルに渡すという意味になります。 つまり、task: @taskの部分でインスタンス変数の値が変数taskに渡される。

task = @task

そしてlocals:によってローカル変数としてパーシャルにtaskを渡すといったイメージです。

これにより、パーシャル内でローカル変数taskが有効になり、先ほどパーシャル内で記述したtaskも有効になります。

これだけの記述で共通化は終了です。 レイアウトも変わらず、共通化を行うことでファイルの中身もすっきりしました。

パーシャルにローカル変数を使う理由

今回の実装方法に反してインスタンス変数のままでも問題なく実装できます。

/ インスタンス変数@taskのまま
= form_with model: @task, local: true do |f|
  .form-group
    = f.label :name
    = f.text_field :name, class: 'form-control', id: 'task_name'
  .form-group
    = f.label :description
    = f.text_area :description, rows: 5, class: 'form-control', id: 'task_name'
  = f.submit nil, class: 'btn btn-primary'
h1 タスクの新規登録

.nav.justify-content-end
  = link_to '一覧', tasks_path, class: 'nav-link'

/ ローカル変数を指定しないことでパーシャルにインスタンス変数が渡される
= render partial: "form"

ではなぜわざわざローカル変数に変える必要があるのでしょうか。

実はインスタンス変数をパーシャルで使ってしまうと、再利用性が低下するという問題が発生します。

パーシャルにコントローラーで作成されたインスタンス変数を使ってしまうと、コントローラーとパーシャルの関係が密結合となってしまいます。つまりコントローラー側での変更が直接パーシャルに影響してしまうのです。これは複数のビューから呼びだされるパーシャルの再現性を低下させます。

例えば、コントローラーのインスタンス変数の名前を変えた場合は、パーシャルのインスタンス変数も同じように変えなければなりません。それに加えて、そのパーシャルを使っている他のビューのコントローラーでも同じインスタンス変数の名前に変更しなければならないのです。再利用性が崩壊しています、、、

終わりに

いかがでしたでしょうか。

パーシャルは非常によく使われると思います。

今回の例のようなシンプル過ぎるレイアウトなら簡単ですが、本来であればここまでシンプルなページはそうないでしょう。しかし、それでもUIの統一性を考慮する場合、パーシャルが使われるケースは多いでしょう。ヘッダーやフッターなど、どのページにも共通する部分にパーシャルを使うことは明らかです。

是非パーシャルを使いこなしてみてください!

以上、大ちゃんの駆け出し技術ブログでした!

【リクエストスペック②】リクエストスペックの作成

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

1日空けてしまいましたが、本日はリクエストスペックの記述方法についてまとめていきたいと思います。

先日出した記事ではAPIの概要などを簡単に紹介しました。

sakitadaiki.hatenablog.com

本記事ではいよいよリクエストスペックを書いていきます!

テスト内容は前回の記事の中で紹介した『ログインユーザーの情報にアクセスできること』です。正しいパラメータを入力した上で、POST形式でlocalhost:3000/api/v1/authenticationにアクセスすると下記JSON形式の情報が取得できことを検証します。

{
    "data": {
        "id": "1",
        "type": "user",
        "attributes": {
            "name": "MyString1",
            "email": "MyText1"
        }
    }
}

ファイルの作成

早速スペックファイルを作成します。

他のシステムスペックやモデルスペックでもファイル作成用のコマンドがありますが、リクエストスペックにもあります。

$ bin/rails g rspec:request ファイル名

今回はapi/v1/配下のファイルをテストするため、テストファイルも同様にapi/v1/配下のフォルダに格納した方が良さそうです。どうするかというとファイル名の手前にApi::V1::を置くだけです。そして今回テストするファイルはauthenticationですので、ファイル名もそれに合わせて指定しましょう。

$ bin/rails g rspec:request Api::V1::authentication

こちらを実行すると指定したディレクトリにフォルダが作成されました。

$ bin/rails g rspec:request Api::V1::authentication
Running via Spring preloader in process 6744
      create  spec/requests/api/v1/authentications_spec.rb

無事ファイルが作成されました。

ファイルの中身を説明

作成されたフォルダの中身を見てみましょう。

# spec/requests/api/v1/authentications_spec.rb
require 'rails_helper'

RSpec.describe "Api::V1::Authentications", type: :request do
  describe "GET /api/v1/authentications" do
    it "works! (now write some real specs)" do
      get api_v1_authentications_path
      expect(response).to have_http_status(200)
    end
  end
end

いつものシステムスペックやモデルスペック作成時とはかなり見た目が違うのではないでしょうか。

まず、type: :requestの部分からわかる通り、テストの種類がリクエストであることがわかると思います。

その下のdescribeの中身は現状「GET形式で/api/v1/authenticationsにアクセスする」という記述になっていますね。これはリクエストスペックの作成時はデフォルトでGET形式が設定されているからです。今回テストするのはPOST形式ですので後ほどこちらは修正します。

その下のitの中身も目新しいかなと思います。

get api_v1_authentications_path
expect(response).to have_http_status(200)

get api_v1_authentications_pathについては直感的に理解可能かと思います。describe部分で記述している通り、「GET形式で/api/v1/authenticationsにアクセス」を行っています。こちらはHTTPリクエストの形式です。これを例えばPOST形式にする場合、post api_v1_authentications_pathを使うことができます。

次の検証部分ですが、これはHTTPステータスコードの検証をしています。HTTPステータスコードを知らないという方はMDNの説明をご覧ください。

HTTP レスポンスステータスコードは、特定の HTTP リクエストが正常に完了したどうかを示します。レスポンスは 5 つのクラスに分類されています。 1. 情報レスポンス (100199), 2. 成功レスポンス (200299), 3. リダイレクト (300399), 4. クライアントエラー (400499), 5. サーバエラー (500599)

developer.mozilla.org

have_http_status(200)とすることでステータスコードが200、つまりHTTPリクエストが正常に完了したことを検証しています。反対に200を400などにすることでクライアントエラーが発生するかどうかを検証することができます。

リクエストスペックについて少し理解ができたのではないでしょうか?

POST形式でのアクセス方法

それではPOST形式でlocalhost:3000/api/v1/authenticationに正しいパラメータでアクセスすると、JSON形式の情報が取得できことを検証するテストを書いていきます。テストのマッチャは記載せず、describecontextitのみを記載して検証ファイルの流れを作ります。

require 'rails_helper'

RSpec.describe "Api::V1::Authentications", type: :request do
  describe "POST /api/v1/authentication" do
    context '正しいパラメータが入力された時' do
      it 'JSON形式の情報を返す' do
      end
    end
  end
end

それでは正しいパラメータを入力する方法から紹介していきます。

UserのFactoryBotは現在以下のように定義されています。

FactoryBot.define do
  factory :user do
    sequence(:name)  { |n| "MyString#{n}" }
    sequence(:email) { |n| "MyText#{n}" }
    password { 'password' }
  end
end

このFactoryBotのユーザーでログインしたとき、ログインユーザーの情報を取得できるかどうかを検証します。リクエストスペックファイル内ではlet!を使用してユーザーを新規作成します。

require 'rails_helper'

RSpec.describe "Api::V1::Authentications", type: :request do
  describe "POST /api/v1/authentications" do
    let!(:user) { create(:user) }
    context '正しいパラメータが入力された時' do
      it 'JSON形式の情報を返す' do
      end
    end
  end
end

次にPOST形式でlocalhost:3000/api/v1/authenticationにアクセス方法に移ります。これは先に記述を見せてから説明します。

require 'rails_helper'

RSpec.describe "Api::V1::Authentications", type: :request do
  describe "POST /api/v1/authentications" do
    let!(:user) { create(:user) }
    let(:password) { 'password' }
    context '正しいパラメータが入力された時' do
      it 'JSON形式の情報を返す' do
        post api_v1_authentication_path, headers: { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json' }
      end
    end
  end
end

post api_v1_authentication_pathならわかると思います。POST形式でlocalhost:3000/api/v1/authenticationにアクセスしています。問題は後半部分のheaders以降になるかと思います。

headers: { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json' }

これを簡単に説明している記事を見つけましたので共有します。

qiita.com

ACCEPTとは「クライアント側がどんなデータを処理できるか」を表しています。つまり受け取るファイル形式であるJSONを許容(ACCEPT)しているということです。受け取るファイルはapplication/jsonを指定しています。そもそもこの記述がないとテストでJSON形式のファイルを取得できないという意味にも取れます。

CONTENT_TYPEとは「実際にどんな形式のデータを送信したか」を表しています。サーバー側からクライアント側へ渡すファイル形式がJSONであるということです。こちらもapplication/jsonを指定しているため、このJSONファイルを送信することを指定しています。

つまり、ACCEPTとCONTENT_TYPEに同じファイル(application/json)指定することで、JSONファイルの送受信を可能にしているということです。JSONデータを取得するリクエストスペックでは必須の記述になると思います。

次に、正しいパラメータを入力する方法です。下記画像のQuery Paramsの入力部分になります。

https://i.gyazo.com/ecf0946222a64cc5d186bc6590f59ac2.png

これはparamsオプションを使って指定することができます。

RSpec.describe "Api::V1::Authentications", type: :request do
  describe "POST /api/v1/authentications" do
    let!(:user) { create(:user) }
    let(:password) { 'password' }
    context '正しいパラメータが入力された時' do
      it 'JSON形式の情報を返す' do
        post api_v1_authentication_path, headers: { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json' }, params: { email: user.email, password: user.password }.to_json
      end
    end
  end
end

パラメータ取得部分

params: { email: user.email, password: password }.to_json

こちらもJSON形式でのリクエストという理由でto_jsonを指定しています。また、passwordのパラメータはuser.passwordでは入力できません。user.passwordは'password'をハッシュ化したものだからです。

crypted_password: "$2a$10$HO18dOf1JpzNaQ2Cq7JwHOdNe/KMEurfmt7e6sYZ.AxkfnFvF7jBa"

したがって、let(:password) { 'password' }で変数passwordをパラメータ部分に指定することでハッシュ化前の正しいパスワードを入力しています。

これでPOST形式でlocalhost:3000/api/v1/authenticationにアクセスできるようになりましたが、postメソッド部分がオプションのせいで少し頭でっかちですので、変数request_hashにまとめてしまいましょう。

require 'rails_helper'

RSpec.describe "Api::V1::Authentications", type: :request do
  describe "POST /api/v1/authentications" do
    let!(:user) { create(:user) }
    let(:password) { 'password' }
    context '正しいパラメータが入力された時' do
      it 'JSON形式の情報を返す' do
        request_hash = { headers: { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json' }, params: { email: user.email, password: password }.to_json }
        post api_v1_authentication_path, request_hash
      end
    end
  end
end

リクエストスペックのマッチャ

それでは実際にマッチャを使って検証してみましょう。

まず、POST形式でのアクセスは成功しているはずですので、HTTPステータスコードは200を返しているハズです。よって上述したhave_http_status(200)を使用してみましょう。

require 'rails_helper'

RSpec.describe "Api::V1::Authentications", type: :request do
  describe "POST /api/v1/authentications" do
    let!(:user) { create(:user) }
    let(:password) { 'password' }
    context '正しいパラメータが入力された時' do
      it 'JSON形式の情報を返す' do
        request_hash = { headers: { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json' }, params: { email: user.email, password: password }.to_json }
        post api_v1_authentication_path, request_hash
        expect(response).to have_http_status(200)
      end
    end
  end
end

テストはパスしたハズです。しかし、これだけだとアクセスは成功していることはわかりますが、期待するJSONファイルを取得できているのかがわかりません。

どのようにしてJSONファイルを取得するのかというと、JSON.parse(response.body)で取得することができます。テストをbinding.pryで止めて確認してみましょう。

JSON.parse(response.body)
=> {"data"=>{"id"=>"1", "type"=>"user", "attributes"=>{"name"=>"MyString1", "email"=>"MyText1"}}}

確かにJSON形式のファイルを取得できました。この中身を検証するためのテストを書きましょう。各キーに対する値が正しければ期待するJSONファイルが取得できているということを検証できます。

require 'rails_helper'

RSpec.describe "Api::V1::Authentications", type: :request do
  describe "POST /api/v1/authentications" do
    let!(:user) { create(:user) }
    let(:password) { 'password' }
    context '正しいパラメータが入力された時' do
      it 'JSON形式の情報を返す' do
        request_hash = { headers: { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json' }, params: { email: user.email, password: password }.to_json }
        post api_v1_authentication_path, request_hash
        expect(JSON.parse(response.body).dig('data', 'id').to_i).to eq(user.id)
        expect(JSON.parse(response.body).dig('data', 'attributes', 'name')).to eq(user.name)
        expect(JSON.parse(response.body).dig('data', 'attributes', 'email')).to eq(user.email)
        expect(response).to have_http_status(200)
      end
    end
  end
end

digメソッドについて簡単に説明します。digメソッドはネストしたハッシュから安全に値を取り出すことができるメソッドです。例えば、expect(JSON.parse(response.body).dig('data', 'id')の部分ですが、dig('data', 'id')とすることで、最初のキーである'data'の値のハッシュのキーである'id'の値である'1'を取り出しています。

{"data"=>{"id"=>"1", "type"=>"user", "attributes"=>{"name"=>"MyString1", "email"=>"MyText1"}}}

ようは、深い階層にあたるハッシュの値を取得するメソッドということになります。下記2つの文もそれぞれ深い階層にある値を取り出し、その値が期待しているものかを検証しているのです。

expect(JSON.parse(response.body).dig('data', 'attributes', 'name')).to eq(user.name)
expect(JSON.parse(response.body).dig('data', 'attributes', 'email')).to eq(user.email)

テストを実行するとパスするかと思います。

終わりに

2つの記事に分けましたがやはりボリュームが大きかったですね。

リクエストスペックは1つ目の記事にも書きましたが、APIのための統合テストです。ですので、基本的にAPIを使用しない場合はメジャーなシステムスペックを使用しましょう!

以上、大ちゃんの駆け出し技術ブログでした!

フラッシュメッセージの実装

フラッシュメッセージとは

こんにちは!大ちゃんの駆け出し技術ブログです。

ユーザー側の入力に基づいて処理が行われた際に、その処理の結果をユーザー側にわかるように表示されるメッセージのことです。例えば、アカウント登録、アカウント情報の更新の処理などに使用されています。

実装はさほど難しいものではありませんので、仕組みをしっかり理解しておきましょう。

実装方法

本記事ではアクションコントローラーでユーザ登録機能を実装することにします。 登録フォームは下の画像のようなシンプルなフォームです。

image.png

登録ボタンを押すことで新規登録のアクションであるcreateアクションが実行されます。 (フラッシュメッセージ記述済み)

class UsersController < ApplicationController
 
  def create
    @user = User.new(user_params)

    if @user.save
      flash[:success] = 'ユーザー登録が完了しました'
      redirect_to login_path 
    else
      flash.now[:danger] = 'ユーザー登録に失敗しました'
      render :new 
    end
  end

  private

  def user_params
    params.require(:user).permit(:last_name, :first_name, :email, :password, :password_confirmation)
  end
end

createアクションの最初の行でインスタンス変数@userに入力した内容が渡されます。

@user = User.new(user_params)

補足で説明すると、user_paramsはprivateメソッドとして下の行に定義されています。

private

def user_params
  params.require(:user).permit(:last_name, :first_name, :email, :password, :password_confirmation)
end

これはストロングパラメーターというもので、簡単に説明すると、permitの()内で括られた値以外は取得することを何人たりとも許可しないという意味になります。これは悪意のあるユーザーがユーザーが入力した以外の情報を取得することを防ぐセキュリティ対策で、Railsでは必須の知識です。

インスタンス変数@userは条件分岐でsaveメソッドにかけられます。

if @user.save

このsaveメソッドでユーザ登録に必要な情報が正しく入力されているかを判断し、情報に問題がなければtrueを返します。逆に、入力欄が空白であったり、確認用パスワードが入力したパスワードと違っていたりするとfalseを返します。

いよいよ本記事のトピックであるflashメッセージとのご対面です。 情報が正しく入力されていれば登録完了の分岐となります。

flash[:success] = 'ユーザー登録が完了しました'
redirect_to login_path 

flash[:success]に文字が格納され、リダイレクト先であるlogin_pathに「ユーザー登録が完了しました」、というメッセージが表示されます。本当にたったこれだけです!

ちなみに上記の文章は一文で記述することもできます。 コードはプログラマーがコードを理解する速さが重視されるのでこちらの記述が好まれるでしょう。 (しかし、本記事の最後の節に記載するadd_flash_typesを実装しないとできませんのでご注意ください、、、。)

redirect_to login_path, flash: 'ユーザー登録が完了しました'

反対に、情報が正しく入力されていなかった場合、else以降が実行されます。

flash.now[:danger] = 'ユーザー登録に失敗しました'
render :new 

こちらはレンダー先である新規登録ページでメッセージが表示されます。

flash.nowとflashの使い分け

結論から先に行ってしまうと、

成功時(リダイレクト)にはflash 失敗時(レンダー)にはflash.now

詳しく説明すると、基本的に新規登録や編集機能などではif文を使って、trueの場合はredirect_tofalseの場合はrenderが使われます。

redirect_torenderには大きな違いがあります。

redirect_to・・・アクションを経由して画面遷移 render・・・アクションを経由しないで画面遷移

そしてflashflash.nowにも違いがあります。

flash・・・1回目のアクションの経由では消えず、次のアクションまで表示させる。 flash.now・・・次のアクションに移行した時点で消える。

renderはアクションを経由せずページだけを表示させるため、もしflashを使ってしまうと表示が消えるまでに2回のアクションが必要となります。 つまり、今回の例で言えば、正しい情報が入力されログインページにて、「ユーザー登録が完了しました」、というメッセージが次に遷移するログイン後のページにも元気よく、「ユーザー登録が完了しました」と表示されてしまうのです。

逆に、redireict_toは次のアクションを経由するのでFlash.nowでは表示すらされずに、結果そのメッセージを見ることは一生ないでしょう、、、。

少しややこしかったかもしれませんが、まとめると redirect_toにはflash、renderにはflash.nowを使用する!

add_flash_typesでメッセージの色を変える

最後に補足でadd_flash_typesに触れておきます。 これはBootstrapに定義されているスタイルを読み込むことが可能になります。

実装方法は簡単でrailsにデフォルトで用意されているapplication_contrller.rbに設定を追加するだけです。

class ApplicationController < ActionController::Base
  add_flash_types :success, :info, :warning, :danger
end

これにより、メッセージの種類に合わせてメッセージの色を変えることができます。 (successは成功した感じの柔らかい色、dangerは失敗した時の感じの刺々しい色(笑))

また、後述したように、成功時のフラッシュメッセージを表示するためのredirect_toが1行で記述できるようになります。

redirect_to login_path, flash: 'ユーザー登録が完了しました'

以上、大ちゃんの駆け出し技術ブログでした!

【リクエストスペック①】APIの取得

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

今回はタイトルにもある通り、リクエストスペックについて書きたいと思います。

しかし、リクエストスペックを説明するためにはAPIの説明が必要不可欠となるため、今回はAPI情報の取得方法、次回はAPI情報が取得できているかをテストするリクエストスペックという感じで2回構成で説明していきたいと思います!

他のテストタイプとの違い

「リクエストスペックってなんぞや?」

自分も最初はこんな感じでした。

自分がRSpecを学習する上で1番最初に学習したのはそのモデルをテストするモデルスペックです。主にテストの検証内容はそのモデルのバリデーションが効いているのかをテストすることです。

例: タスクモデルのモデルスペック

# spec/models/task_spec.rb
require 'rails_helper'

RSpec.describe Task, type: :model do
  describe 'validation' do
    it 'is valid with all attributes' do
      task = build(:task)
      expect(task).to be_valid
      expect(task.errors).to be_empty
    end

    it 'is invalid without title' do
      task_without_title = build(:task, title: nil)
      expect(task_without_title).to be_invalid
      expect(task_without_title.errors[:title]).to eq ["can't be blank"]
    end
  end
end

次に学習した内容はシステムスペックでした。主にこちらのテストは自分の期待通りに画面が遷移し対象の要素が画面に表示されているのか、実装した機能が正しく動くかを検証します。圧倒的にこちらのテストを書く機会が多いですね。

例: ログイン機能のシステムスペック

require 'rails_helper'

RSpec.describe 'UserSessions', type: :system do

  let(:user) { create(:user) }

  describe 'before login' do
    before { visit login_path }
    context 'when input values in the form are all valid' do
      before do
        fill_in 'Email', with: user.email
        fill_in 'Password', with: 'password'
        click_button 'Login'
      end
      it 'login successfully' do
        expect(page).to have_content 'Login successful'
        expect(current_path).to eq root_path
      end
    end
  end
end

これら2つのテストは何度も書いたことがありました。しかし、新たに今回リクエストスペックという言葉が出てきました。これは一体何のテストなんでしょうか。

ある記事を参照すると、リクエストスペックはどうやら以下のようなテストがリクエストスペックと言えるそうです。

基本、統合テストはシステムスペックを書きましょう API系のテストがあるなら、別でリクエストスペックを書きましょう

qiita.com

リクエストスペックとはシステムスペックと同じ統合テストにあたるようです。統合テストとは、モデルスペックのように個々に対して対してテストするのではなく、システムスペックのように集合的にテストする形式です。しかし、その以外はAPI系のテストの有無にあります。

APIってなんぞや?」

みたいになるかと思いますが、詳しく説明すると大変ボリューミーになるので、詳しく知りたい方は他記事をあたってみてください。簡単に引用して説明すると下記のようなものがAPIです。

APIを、簡単に説明すると「決まった方法でアクセスをすれば決まった結果を返してくれるもの」です。先のケーススタディでいうと、URLの末尾にzipcode=郵便番号でアクセスすると、住所を決まった形式で返してくれるのです。一般的にはJSONという形式で結果が返ります。郵便番号の例もJSON形式です。どこからでもアクセスできる口を開けておき、決まった形式でアクセスを受け付けて、仕様通りの結果を返すインターフェースとなるのがAPIです。

products.sint.co.jp

よくTwitterAPIだったり、ぐるなびAPIだったりとAPIという単語自体は聞いたことはあるという人も多いのではないでしょうか。APIとはまさにそれらアプリが公開している情報のことであることを示しています。そして、その情報にアクセスすると一般的にはJSONの形式で返すようですね。例えば、下記の情報をぐるなびAPIが公開しているお店の情報に見立てることもできます。いろいろな情報がこちらで調べることなくJSON形式で手に入ります。

https://i.gyazo.com/d1913f65c35122a2d33b463d0bceece2.png

API情報の公開とは

自分が使用しているアプリを例にどのようにAPI情報へアクセスするか試してみましょう。

sorceryのログイン機能に関してアプリ内で行っているのはログインユーザーのAPI情報の公開です。詳しくみてみると下記のような状態です。

# app/controllers/api/v1/authentication_controller.rb
module Api
  module V1
    class AuthenticationController < BaseController
      def create
        @user = login(params[:email], params[:password])
        raise ActiveRecord::RecordNotFound unless @user
        json_string = UserSerializer.new(@user).serialized_json
        render json: json_string
      end
    end
  end
end

何かいつものログイン機能のファイルとは違いますね。。。

いつものsorceryのログイン機能のファイルは以下のような感じかと思います。

def create 
  @user = login(params[:email], params[:password]) 
  if @user 
    redirect_back_or_to root_path 
  else 
    render :new 
  end 
end

共通部分は下記の部分のみです。

@user = login(params[:email], params[:password])

なぜ今回使うファイルがいつものsorceryのログイン機能のファイルとは違うのかというと、今回テストするファイルはログインユーザーのAPI情報を返す仕様になっているからです。ここはかなりややこしいですね。

言い直すのならば、いつもsorceryでログイン機能を実装しているcreateアクションは、アプリ内でログインするための機能です。しかし、今回テストするcreateアクションはログインしたユーザーのデータをJSON形式で外部に公開する機能です。いつものsorceryのcreateアクションとは違い、API情報を公開する機能を持っているということです。

具体的にJSON形式で公開する方法を説明します。

まず、共通部分では同じように登録されているユーザーを取得します。

@user = login(params[:email], params[:password])

もし、ユーザーがいないのであれば例外を発生させます。

raise ActiveRecord::RecordNotFound unless @user

次の箇所は少し歪に見えるかと思います。

json_string = UserSerializer.new(@user).serialized_json

これはfast_jsonapiのgemで生成したUserSerializerを用いて、取得したユーザー情報をJSON形式に変更しています。json_stringの中身を見てみると、user情報がJSON形式で格納されているのがわかります。

5: def create
6:   @user = login(params[:email], params[:password])
7:   raise ActiveRecord::RecordNotFound unless @user
8:   json_string = UserSerializer.new(@user).serialized_json
9: 
10:   binding.pry
11:   
12:   render json: json_string
13: end

[1] pry(#<Api::V1::AuthenticationController>)> json_string
=> "{\"data\":{\"id\":\"1\",\"type\":\"user\",\"attributes\":{\"name\":\"MyString1\",\"email\":\"MyText1\"}}}"

最後にJSONファイルをレンダリングしてます。

render json: json_string

このレンダリングを行うことで、ユーザー情報を外部からのアクセスで表示させることができるのです。

API情報へのアクセス方法

実際にアクセスして指定のユーザー情報を取得してみましょう。

このAPI情報をどのように取得するのかというと、上述したようにJSON形式のデータを取得するためには決まった形式のアクセスが必要です。今回のログインユーザーの情報にアクセスするためには、ルーティングを見るとわかります。

$ rails routes
api_v1_authentication POST   /api/v1/authentication(.:format)         api/v1/authentication#create {:format=>/json/}

Post形式で/api/v1/authenticationにアクセスする必要があるようです。

実際に指定のブラウザ上でも試せますが、Postmanというアプリを使うとAPI情報へのアクセスがわかりやすくなるので、Postmanを使います。

www.postman.com

Post形式で/api/v1/authenticationを指定した画面が下記になります。

https://i.gyazo.com/11cf3d9b106db0da9ff611bfb34d7b6e.png

しかし、このままではアクセスできません。今回はログイン機能ですので、アプリで登録されているユーザーの指定のパラメーター情報を/api/v1/authenticationに加える必要があります。

@user = login(params[:email], params[:password])

Postamanで送る方法はとても簡単で、Query Paramsという項目に登録されてるユーザー情報に合致するパラメータを記載します。取得パラメーターはemailpasswordです。

gyazo.com

するとURIの部分が自動で切り替わりました。

localhost:3000/api/v1/authentication?email=MyText1&password=password

これで指定のパラメーターを送ることが可能になったので、実際に送信ボタン(Send)を押してみましょう。するとJSON形式のデータが取得できたはずです。

{
    "data": {
        "id": "1",
        "type": "user",
        "attributes": {
            "name": "MyString1",
            "email": "MyText1"
        }
    }
}

ちなみにここで間違ったパラメーター渡してみると、ユーザー情報が取得できなかったために、raise ActiveRecord::RecordNotFoundの例外処理が実行され、下記のエラーメッセージのデータがJSON形式で表示されます。エラーハンドリングされているということですね。

{
    "message": "Record Not Found",
    "errors": [
        "ActiveRecord::RecordNotFound"
    ]
}

終わりに

上述したように今回はあくまでAPIの概要や取得方法についての説明であり、本当に説明したのは次回説明するリクエストスペックについてです。今回のようにJSON形式のファイルが表示されているのかをテストする方法について発信していきますのでお楽しみに!

以上、大ちゃんの駆け出し技術ブログでした!

【RSpec②】have_selectorの画像検証

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

RSpecの紹介シリーズ第二弾!今回はhave_selectorの画像検証とあるようにhave_selectorについてです!

応用編が終わり個人的にRSpecを最近書く機会が少なくなってきたので、RSpecについてはなるべく多くアウトプットしたいと思います!!

have_selector超基礎

1つ目のRSpechave_selectorです。have_selectorは使用頻度がとても高いRSpecのマッチャだと思います。その理由はhave_selector特定のタグやCSS要素に特定の文字列が表示されていることを検証するといった、かなり使い勝手のよいマッチャだからでしょう。

例えば、タスク一覧画面にはh1タグで文字列"タスク一覧"が表示されています。

https://i.gyazo.com/5eda388a39a182f50a649cd922a32ee4.png

文字列"タスク一覧"が表示されていることを検証する場合、have_contentを使用すれば簡単に検証できることはわかると思います。

expect(page).to have_content 'タスク一覧'

しかし、ここで「h1タグでテキストが"タスク一覧"」である要素が画面にあることを検証する場合、have_contentでは検証できません。

そこでhave_selectorの出番です。

expect(page).to have_selector 'h1', text: 'タスク一覧'

これにより「h1タグでテキストが"タスク一覧"」条件を満たす要素があるか検証を行ってくれます。

画像検証の方法

上述したのはhave_selectorの基本的な使い方です。このhave_selectorで紹介したい記法は下記のような記法です。

expect(page).to have_selector("img[src$='画像ファイル名']")

これはどのような検証を行っているのかというと、対象の画像ファイルが表示されていることを検証してくれます。少し奇妙な書き方をしていますね。

例えば、下記のような画像が表示されている時に使用するとします。

https://i.gyazo.com/3bdd5eb2fb9ca8bee8145d389c48cfb8.png

ここで ディベロッパーツールを使うと表示されている画像の部分(img要素)は以下のようになります。見やすいようにわざと改行しています。

<img class="swiper-slide swiper-slide-next" 
         src="/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBHZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--b7f974b228479bd43c7aaba23cd913d9e88c77e2/haikyu.jpg" 
         role="group" 
         aria-label="3 / 3"
         style="width: 300px; transform: rotateX(0deg) rotateY(180deg) translate3d(300px, 0px, 300px); transition-duration: 0ms;">

ここである画像が表示されていることを検証する場合、その画像を表す部分はsrc属性からわかると思います。しかし、src属性は画面上ではかなり複雑なハッシュにより表されています。

src="/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBHZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--b7f974b228479bd43c7aaba23cd913d9e88c77e2/haikyu.jpg"

このsrc属性を正確に検証しようとすることはかなり手間だと思いませんか。実際にhave_selectorを使って表現すると以下のようになります。

expect(page).to have_selector("img[src$='/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBHZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--b7f974b228479bd43c7aaba23cd913d9e88c77e2/haikyu.jpg']")

長すぎますよね笑

そこで$=を記載することでこの長いハッシュを書かずに済むようにしています。

expect(page).to have_selector("img[src$='画像ファイル名']")

先ほどのsrc属性の後ろ側をみると、haikyu.jpgとあるように画像の名前はハッシュ化されずそのままです。あくまでもハッシュとして表現されているのはディレクトリの部分なのですね。なので、その長いハッシュ化されたディレクトリを無視できれば、画像ファイルが表示されているかを検証できるのです。そしてその方法が$='画像ファイル名'と書くことによって可能になります。

expect(page).to have_selector("img[src$='haikyu.jpg']")

終わりに

src$=の記法を使えば画像ファイルの検証はとても簡単に行うことができますね!ハッシュ化されたディレクトリをそのまま書かずに済むのでありがたい!

以上、大ちゃんの駆け出し技術ブログでした!

【RSpec】supportディレクトリ

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

supportディレクトと聞かれてRSpecについてのこととピンとくる方はいらっしゃいますか?supportディレクトリを一言で表すと、「RSpecの設定だったりmoduleファイルを一箇所にまとめておくための場所」です。

テストは機能、モデルの数が増えれば増えるほど数が増え複雑になります。そのため、RSpecのテスト方法をテストによって分けたり、どのテストでも必要となる機能(ログイン)などが増えます。それらをsupportディレクトリ配下に管理する取り決めをしておくことで、設定やモジュールファイルを探す手間がすごく省けるのです。実際に試してみましょう!

なお、今回使用するアプリケーションは、毎度お馴染みの現場Railsのchapter7まで終えている状態のアプリケーション「taskleaf」です。同じ状態でハンズオンで試したいという方は現場Railsのchapter7までを終わらせてから取り組んでください。ただし、今回はchapter5まででも問題ないはずです!

実装の概要

上述しましたように、supportディレクトリを作る理由はテストを行う上で必要である設定やmoduleをまとめるためにあります。

都合の良いことに、現在のtaskleafアプリケーションではテスト形式であるcapybaraの設定がspec_helper.rb内に書かれています。

# spec/spec_helper.rb
require 'capybara/rspec'
RSpec.configure do |config|
  config.before(:each, tipe: :system) do
    driven_by :selenium_chrome_headless
  end
・
・
・
・

spec_helper.rb内は現在ちょうど100行もあるファイルです。しかも、設定を追加するために追記することもあると思います。そうなった時、capybaraの設定を変更する時に上記のcapybaraの設定コードを変更するために探すのは少し手間かと思います。例えば、現在はオプションで:selenium_chrome_headlessを設定していますが、これを変更するとなると100行もあるファイルの中からcapybaraの設定を見つけなければならないので手間ですね。

このcapybaraのようなテストの設定をsupportディレクトリ配下に移しておくことで、後々変更するとなった時に、supportディレクトリ配下を探すということがわかっているので、手間はだいぶ省けることになります。

実装方法

実装方法自体はさほど難しくありません。

まず、supportディレクトリを手動で作成し、その配下にcapybara.rb(ファイル名はrubyファイルであれば任意)を手動で作成します。

# spec/support/capybara.rb

手動で作成したので何もない状態です。

ここに下記内容を記載します。RSpec.configureはspec_helper.rbファイルにも記載されているRSpecの設定を記述する箇所です。

# spec/support/capybara.rb
RSpec.configure do |config|

end

ここにspec_helper.rbに記載されているcapybaraの設定をcapybara.rbに移します。spec_helper.rb内のcapybaraの設定箇所は削除しておきましょう。

# spec/support/capybara.rb
RSpec.configure do |config|
  config.before(:each, tipe: :system) do
    driven_by :selenium_chrome_headless
  end
end

これで実装は完了ではありません。

spec/rails_helper.rbコメントアウトで記載されている下記部分のコメントアウトを外します。

# spec/rails_helper.rb
・
・
# require only the support files necessary.
#
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
・
・

コメントアウトすることでsupportディレクトリ配下にあるRSpecの設定ファイルが読み込めるようになりました。これで実装は完了です。

試しに新しく作成したcapybara.rbの設定を変更しましょう。:selenium_chrome_headless:selenium_chromeと変更してください。

# spec/support/capybara.rb
RSpec.configure do |config|
  config.before(:each, tipe: :system) do
    driven_by :selenium_chrome
  end
end

これで実際にテストを実行すると、chromeが開いた状態でテストが実行されるかと思います。supportディレクトリ配下の設定が適用されている証拠です!

終わりに

現場Railsでは学習しない内容ですが、RUNTEQで使用するアプリではこの設定は必ずされていました。自分で今回の実装をすることは少ないかと思いますが、実装方法を把握しておくだけで、設定ファイルの所在などのspecディレクトリは配下で記述を探す手間がかなり省けるかと思います!

以上、大ちゃんの駆け出し技術ブログでした!

【バリデーション①】acceptance

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

以前、バリデーションヘルパーであるnumericalityについて記事を書きました。

sakitadaiki.hatenablog.com

このバリデーションはRUNTEQのカリキュラムの応用編で使用したのですが、実際調べればすぐに導入できたバリデーションです。ですので、応用編カリキュラムを修了していたのでそれなりの知識があるだろうというおごりがあり、バリデーション自体は割と調べればすぐに出てきて導入できるのでわざわざ記事にする必要がないんじゃないの?と思っていました。

しかし、先日numericalityについて記事を書いているときにRailsガイドを参考にしていた時のこと。

「あれ?このバリデーション見たことない!」

とか

「このバリデーションそもそも知らない、、、」

という感じで、まあ、 バリデーションへの知識不足が判明してしまったというわけですwww

しかも、ネットにある記事が意外にもバリデーションの記載方法のみを紹介しており、実際にバリデーションした時にどのようなエラーメッセージが出るかだとか、正常に動いていたテストが失敗するようになるのだとかが紹介されている記事はほとんど見受けられませんでした。

これから0→1でPFを作っていくわけですから、バリデーションは当然自分で設定していきます。複雑なモデルを設定する可能性もあるので、バリデーションを使うタイミングはそれ相応にあるでしょう。こういった知識は毎回調べて導入していたらすごい手間ですから、PFの作成段階に入る前になるべく先に勉強しておいた方がいいですね。

前置きが長くなりましたが、これからの記事ではRUNTEQのカリキュラム内で自分で使用することのなかったバリデーションを他の記事に比べてかなり詳しく紹介していこうと思います。あとは記事数稼ぎです!(笑)

今回もいつもと同様に現場Railsのchapter7まで終えている状態のアプリケーション「taskleaf」です。同じ状態でハンズオンで試したいという方は現場Railsのchapter7までを終わらせてから取り組んでください。

acceptanceの概要

Railsガイドでは以下のように説明しています。

このメソッドは、フォームが送信されたときにユーザーインターフェイス上のチェックボックスがオンになっているかどうかを検証します。ユーザーによるサービス利用条項への同意が必要な場合や、ユーザーが何らかの文書に目を通したことを確認させる場合によく使われます。 (引用: Active Record バリデーション - Railsガイド)

Railsガイドは初心者が読むと何を言っているのかよくわからないことが多いと個人的には思っているのですが、この説明はめちゃくちゃわかりやすいと思っています!特に以下の部分!

ユーザーによるサービス利用条項への同意が必要な場合

よくWebサイトで利用規約へのチェックボックスを付けるのを忘れて次の画面に遷移しようとすると、次の画面に遷移することなくエラーメッセージが表示されると思います。


(画像を差し込む!!)

この利用規約に同意させてからアプリを使用するようにする設計は、小さな個人開発サービスを除けば、必ずと言っていいほど使用するかと思います!利用規約に同意させることでトラブルがあった際にアプリ開発側への責任を担保できますからね。

おそらくPF開発するときは使用しないと思っていますが(PFは企業の人が見るので利用規約への同意フローは時間を浪費させる無駄な機能なため)、将来的には個人でサービスも開発したいと思っているので、この際試して将来の自分に使い方を教えるために記事にしておきたいと思います。(あと、ユーザーが何らかの文書に目を通したことを確認させるといったフローはPFに組み込まないとは断言できないので)

acceptanceの使用例

それでは実際に使用してみましょう。

まずチェックボックスを付けるためのカラムを用意しておきましょう!!

と思ったのですが現場Railsのアプリでは既にチェックボックスの付いたカラムが存在するではありませんか!なんと素晴らしい!

https://i.gyazo.com/15242f5e5dd90c3611001fbc45ddfb52.png

表示しているページはユーザー編集ページです。

https://i.gyazo.com/4d5500b1563a8ae82184775772987343.png

表示しているビュー

.form-check
  = f.label :admin, class: 'form_check_label' do
    = f.check_box :admin, class: 'form-check-input'
    | 管理者権限

これを利用してこのカラムにacceptance: trueをつけてみましょう!

# app/models/user.rb
validates :admin, acceptance: true

あとやることは、、、、、、

以上です!!!!!!!wwwwww

これで実装できてます笑こんな実装をすることはありませんが、、

ユーザーが必ず管理者権限を付与しないとユーザーが更新されないので全ユーザー管理者権限のトンデモ状態になってしまいます笑

実際に試してみましょう。チェックをしないでユーザーを作成しようとすると、

https://i.gyazo.com/d5a027168f38b9c9de20c7af20a76710.gif

"Admin must be accepted"と表示されています。これによって、チェックなしではユーザー登録が完了しない奇妙な仕様となってしまいました。

ちなみに、Admin must be acceptedのエラーメッセージはバリデーションの設定で容易に変えられます。

# app/models/user.rb
validates :admin, acceptance: { message: 'でないとユーザーは登録できません' }

表示されるエラーメッセージが変わることを確認してみてください。

https://i.gyazo.com/eba407092b45a2eccc53c37c6e649523.png

以上でacceptanceの紹介を終わります!

ほんとうに簡単な実装で終わってしまいましたね💦

終わりに

本記事で紹介したバリデーションは知っていて損はないと思います。バリデーションは調べればすぐにどのようなバリデーションなのかをすぐに把握することができますが、理解が難しくないことを理解しないまま放置しておくのはあまり賢明ではないですね。今回実装した内容をもとに自分も0→1でPFを作る際は自分の手で実装できるようになった気がします!といっても、念のため確認するためにどうせRailsガイドは開くとは思うんですけどね(笑)

以上、大ちゃんの駆け出し技術ブログでした!

【RSpec①】within

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

本日はかなりの短いアウトプットとなります。ど平日で仕事終わりに書いておりますが、少し体調が悪く疲れているのです!!!(言い訳ですが、、、)

本日はRUNTEQのカリキュラムの中で、これまでに学んだRSpecのマッチャやテスト方法を紹介します!というか、これから出すいくつかの記事でRSpecで使えると思ったものを紹介しようかなと思ってます。ただ、visitfill_inなどはよく使われており調べれば簡単に出てくるため、本記事で紹介はしません。これからのRSpecの記事で紹介するものは1つのテストを書いているときに使うタイミングもあれば使わないタイミングもあると思うようなものに限定します。

今回使うアプリケーションは、現場Railsのchapter7まで終えている状態のアプリケーション「taskleaf」です。同じ状態でハンズオンで試したいという方は現場Railsのchapter7までを終わらせてから取り組んでください。

使うタイミング

第1回目は記事のタイトルにあるとおりwithin!これはかなり使えるなと思いました。特に、画面にたくさんのボタンやリンクが表示されている画面でテストをする際はおそらく必須になると思います。つまり、シンプルな設計がなされているページでは使う必要がありません。

どういうタイミングで使えるのかというと、本アプリのタスク一覧画面のような設計で使えます。

https://i.gyazo.com/6c57d45629104770c080f10f7bfa412e.png

ん?すごくシンプルな画面じゃん。

そうですね。チュートリアルの画面として最適のような画面です。ではこのタスク一覧画面の中で「指定のタスクを削除する」というスペックを書きましょう。タスク一覧画面のテストなので、既に作成されているspec/system/tasks_spec.rbを使用しましょう!

# spec/system/tasks_spec.rb
describe "タスク管理機能", type: :system do
  let(:user_a) { FactoryBot.create(:user, name: 'ユーザーA', email: 'a@example.com') }
  let(:user_b) { FactoryBot.create(:user, name: 'ユーザーB', email: 'b@example.com') }
  let!(:task_a) { FactoryBot.create(:task, name: '最初のタスク', user: user_a) }
  before do
    # 作成者がユーザーAであるタスクを作成しておく
    FactoryBot.create(:task, name: '最初のタスク', user: user_a)
    # 共通ログイン
    visit login_path
    fill_in 'メールアドレス', with: login_user.email
    fill_in 'session[password]',   with: login_user.password
    click_button 'ログインする'
  end

  describe 'タスク削除機能' do
    let(:login_user) { user_a }
    context '削除ボタンを押したとき' do
      binding.irb
      before { click_on '削除' }
      it 'タスクが削除される' do
        expect(current_path).to eq(root_path)
                expect(Task.all.count).to eq(1)
      end
    end
  end
・
・
・
・
end

タスク削除機能のテストではuser_aとしてログインしているため、ログイン後の画面であるタスク一覧が表示されています。

ログイン時画面

f:id:SakitaDaiki:20210218185506p:plain

削除ボタンが見えますね。ですのでタスクを1つ削除するとタスクの数は一つとなるのでexpect(Task.all.count).to eq(1)がパスするという想定です。ですが、、、

ターミナル

$ bundle exec rspec spec/system/tasks_spec.rb
2021-02-18 18:12:42 WARN Selenium [DEPRECATION] Selenium::WebDriver::Chrome#driver_path= is deprecated. Use Selenium::WebDriver::Chrome::Service#driver_path= instead.
Capybara starting Puma...
* Version 3.12.6 , codename: Llamas in Pajamas
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:61796
F

Failures:

  1) タスク管理機能 タスク削除機能 削除ボタンを押したとき タスクが削除される
     Failure/Error: before { click_on '削除' }
     
     Capybara::Ambiguous:
       Ambiguous match, found 2 elements matching visible link or button "削除"
     
     [Screenshot]: tmp/screenshots/failures_r_spec_example_groups_nested_nested_nested_タスクが削除される_422.png

     
     # ./spec/system/tasks_spec.rb:20:in `block (4 levels) in <top (required)>'

Finished in 5.21 seconds (files took 2.19 seconds to load)
1 example, 1 failure

テストが失敗していますね。何故でしょうか。

その理由は同じ文字のボタンが複数個表示されているからです。

ログインすると2つのタスクが作成されていることは上述しました。しかし、そのタスク削除ボタンの文字はお互いに「削除」です。エラーメッセージを見てみると、

Ambiguous match, found 2 elements matching visible link or button "削除"

Ambiguousとあまり見ない英単語ですね。意味は「曖昧な」。つまり、Ambiguous matchとは「曖昧な一致状態」というエラーになります。そこから表示されているエラーメッセージは、「表示されているリンクまたはボタンに文字列”削除”と一致する2つの要素が見つかりました」。

この文から分かるとおり、「"削除"という文字のボタンが2つあるので、どっちのボタンを指定しているのかが曖昧です」とエラーメッセージを返しているわけです。

Withinの使用方法

では上記エラーを解消しましょう。解消する方法はわりとシンプルです。

before do
  within '#task-1' do
    click_on '削除'
  end
end

各タスクのtrには固有のidが設定されています。

https://i.gyazo.com/f66bb0f60b18088487a2f0f295f8aee7.png

つまり、そのタスクのidを指定すればその中の削除ボタンを「idが〜の削除ボタン」と固有のものにできます。within '#task-1'で指定している#task-1の箇所はCSSを指定する時と同じように、idまたはクラスを指定することができます。そして、指定した要素の内部にある要素のみにRSpecのマッチャの対象を指定することができます。

これにより2つの削除ボタンを差別化することができ、テストをパスすることができるというわけです!

終わりに

かなり短めで申し訳ないです!もっと時間を取れるときにたくさん抹茶を紹介できればと思いますので多めに見てくれるとありがたいです!

以上、大ちゃんの駆け出し技術ブログでした!