バックエンド

【Ruby on Rails CRUD作成】Partialとテストコード実装

reisuta

Webエンジニア | 20代中盤 | 大学時代はGmailすら知らないIT音痴でプログラミングとは無縁の生活を送る → 独学でプログラミングを学ぶ → Web系受託開発企業にエンジニアとして就職 → Web系自社サービス企業に転職 | 実務未経験の頃からVimを愛好しており、仕事でもプライベートでも開発はVimとTmuxを使っているので、VSCodeに疎いのが最近の悩み。何だかんだでやっぱりRubyが好き。

CRUDとは?

CRUDとは、データの作成(Create)、読み出し(Read)、
更新(Update)、削除(Delete)の頭文字を繋げたもので、
ソフトウェアの基本機能のことを言います。
(※本記事では、Delete機能はまだ実装しません)

例えば、TODOアプリとかだと、
タスクを作成して、その作ったタスクを確認して(読み出し)、
タスクの期限を修正して(更新)、タスクを終えたら削除するというように、
ほとんどのアプリケーションには、こうしたCRUDがあります。

Ruby on Railsでは、ドキュメントにあるような、
resourcesの7つのアクションがまさに、
これらのCRUDのどれかに該当しているので、
RailsだとCRUDはわかりやすいかもしれません。

一般的に、CRUDの実装は、
Web開発の基本とされており、
first taskにされることが多かったりします。

本記事では、このCRUDの実装をPartialという共通化を
行って実装します。

その際、テストコードも実装します。

Ruby on Railsの開発環境をまだ構築していない人は、
こちらの記事で構築してから本記事を読むと、
一層理解が深まるかと思います。

Partialとは?

さて、CRUD実装に際し、
まず、Partialについて触れます。

Partialは、簡単に言えばテンプレートのことであり、
辞書で調べたら部分的と訳されていました。

Ruby on Railsにおける、Partilaは、
主にviewファイルで使い回すことができる、
テンプレートであり、
共通化された部分が記述されたファイルです。

Partilaファイルを呼び出すには、
renderという関数を使って、

<%= render partial: "form", locals: {action: 'new'} %>

という風にします。

Rails Guideも詳しいです。
https://railsguides.jp/layouts_and_rendering.html

この場合は、actionという変数に、
'new'という文字列を代入しています。

Rails Guideの場合は、
form_withのmodelにわたす変数を
ローカル変数にすることで、
インスタンス変数への依存をなくしています。

form_with

railsのview層をjbuilderとかではなく、
上記のようなhtml.erbとかは使う場合は、
データの作成を画面上で行うことが想定されるため、
そうしたデータを入力できるformが必要になるでしょう。
(ちなみに、jbuilderはjsonを返すGemでして、
主にRailsをapiサーバーとして使う際に使用します。)

そこで、使用頻度が高いメソッドが、form_withです。
使用例としては、次のような感じです。

<%= form_with(model: @aim, local: true) do |f| %>
  <%= render "shared/error_messages", model: @aim %>
  <div class="form-group mb-3">
    <%= f.label :title, '目標名', class: 'form-label background-color' %>
    <%= f.text_field :title, class: 'form-control', readonly: action==='show' %>
  </div>

  <div class="form-group mb-3">
    <%= f.label :reason, '理由', class: 'form-label background-color' %>
    <%= f.text_area :reason, class: 'form-control', readonly: action==='show' %>
  </div>

  <div class="form-group mb-3">
    <%= f.label :advantage, '得られるもの', class: 'form-label background-color' %>
    <%= f.text_area :advantage, class: 'form-control', readonly: action==='show' %>
  </div>

  <div class="form-group mb-3">
    <%= f.label :difficulty, '難易度', class: 'form-label background-color' %>
    <%= f.text_field :difficulty, class: 'form-control', readonly: action==='show' %>
  </div>

  <div class="form-group mb-3">
    <% if action==='show' %>
      <%= link_to '編集', edit_aim_path(@aim.id), class: 'btn btn-primary' %>
    <% else %>
      <%= f.submit "#{ @aim.new_record? ? '新規作成' : '更新' }", class: 'mt-3 btn btn-primary' %>
    <% end %>
  </div>
<% end %>

 

form_withのちょっと癖のある挙動としては、
一行目の、

<%= form_with(model: @aim, local: true) do |f| %>

の(model: @aim)の部分があります。

これは、@aimのデータを作成したりしたい場合に指定しますが、
@aimの中身が空の場合は、
自動的にcreateアクションに飛ばされ、
中身が存在する場合は、updateアクションに飛ばされるという点です。

そのため、newやeditによって、
インスタンス変数を分ける必要がないのは、
メリットである反面、よしなに処理してくれている部分なので、
直感的ではないかもしれません。

フォームの各々は、

  <%= f.label :title, '目標名', class: 'form-label background-color' %>
  <%= f.text_field :title, class: 'form-control', readonly: action==='show' %>

のようにします。

fは、一行目のform_withをdoで回している部分で、
それにtitleのラベルとして、目標名を指定しています。
classは、ちょっとしたデザインを付けているだけです。

text_fieldは、その名の通りtext_fieldを生成してくれて、
それと@aimのtitleが紐付いています。

そのため、@aim.titleが存在する場合は、その値が初期値として入力されている。
readonlyオプションは、編集や入力をできなくします。

この場合、先ほどのpartilaのローカル変数actionがshowという値だったときのみ、
readonlyオプションが適用されます。

こんな感じになる。

昨今の開発では、
フロントエンドはReact、バックエンドはRailsという、
SPA構成が多いので、このようにerbを使ってRailsだけで
フロントエンドもバックエンドも完結させるという構成は少なくなっている印象です。

そのため、以前ほどは、
form_withの出番は減っている気もしますが、
erbなどを使う際は、かなり使用頻度が高いので、
大まかにでも覚えておくと役に立つと思います。

テストコード

Railsでは、TDD(テスト駆動開発)が推奨されているため、
ベストプラクティスとしては、上記のCRUD処理のコードを書く前に、
テストを書くことでしょう。

テストを書く方法としては、
デフォルトのminitestというライブリを使う方法もありますが、
実務では、これではなくRspecというGemを使うケースが多いので、
本記事でもこちらを使用して解説します。

Rspec

早速Rspecでのテストコードを書いていきます。

aimのcreateやupdateのformを作ったので、
そちらのテストを書いてみます。

テストの簡単なコード例としては、
次のような感じです。

require 'rails_helper'

RSpec.describe 'Aims', type: :request do
  let!(:aim1) { create(:aim) }
  let(:aim_params) { { title: 'aim1', reason: 'test', advantage: 'test2' } }

  describe 'GET /new' do
    it 'returns http success' do
      get "/aims/new"
      expect(response).to have_http_status(:success)
    end
  end

  describe 'GET /edit' do
    it 'returns http success' do
      get "/aims/#{aim1.id}/edit
      expect(response).to have_http_status(:success)
    end
  end

  describe 'POST /create' do
    it 'returns http success' do
      post "/aims", params: { aim: aim_params }
      expect(response).to have_http_status(302)
    end

    context '正常系' do
      it 'レコードの数が一つ増える' do
        expect do
          post "/aims", params: { aim: aim_params }
        end.to change { Aim.count }.by(1)
      end

      it '一覧画面にリダイレクトされる' do
        post "/aims", params: { aim: aim_params }
        expect(response).to redirect_to(aims_path)
      end
    end

    context '異常系' do
      before do
        aim_params[:title] = ''
      end

      it 'レコードの数が増えない' do
        expect do
          post "/aims", params: { aim: aim_params }
        end.to change { Aim.count }.by(0)
      end

      it '新規作成画面にレンダリングされる' do
        post "/aims", params: { aim: aim_params }
        expect(response).to render_template :new
      end
    end
  end

  describe 'PATCH /update' do
    it 'returns http success' do
      patch "/aims/#{aim1.id}", params: { aim: aim_params }
      expect(response).to have_http_status(302)
    end

    context '正常系' do
      it 'レコードの値が変更されている' do
        patch "/aims/#{aim1.id}", params: { aim: aim_params }
        expect(aim1.reload[:title]).to eq 'aim1'
      end

      it '一覧画面にリダイレクトされる' do
        patch "/aims/#{aim1.id}", params: { aim: aim_params }
        expect(response).to redirect_to(aim_path(aim1.id))
      end
    end

    context '異常系' do
      before do
        aim_params[:title] = ''
      end

      it 'レコードの値が変更されていない' do
        patch "/aims/#{aim1.id}", params: { aim: aim_params }
        expect(aim1.reload[:title]).to eq 'Golang取得'
      end

      it '新規作成画面にレンダリングされる' do
        patch "/aims/#{aim1.id}", params: { aim: aim_params }
        expect(response).to render_template :edit
      end
    end
  end
end

 

これは、割と簡素なパターンだが、
レコードの値が、リクエストを送ることによって、
変化しているかどうかを主にテストしています。

expectというマッチャーと、
postやpatchというリクエストを送るメソッドを使って、
テストをしているという感じです。

Controllerのコード

さて、上記のようなテストがあるとしたら、
最初は、controllerをまだ実装していないので、
Redになるでしょう。

このテストをGreenにしていこうと思います。

作成処理としては、
こんな感じのコードになります。。

class AimsController < ApplicationController
  before_action :set_aim, only: [:show, :edit, :update]

  def index
    @q = Aim.ransack(params[:q])
    @aims = @q.result.page(params[:page]).per(5)
  end

  def show
  end

  def new
    @aim = Aim.new
  end

  def create
    @aim = Aim.new(aim_params)
    if @aim.save
      flash[:success] = '目標を作成しました'
      redirect_to aims_path
    else
      render 'new', status: :unprocessable_entity
    end
  end

  def edit
  end

  def update
    if @aim.update(aim_params)
      flash[:success] = '目標を更新しました'
      redirect_to aim_path(@aim.id)
    else
      render 'edit', status: :unprocessable_entity
    end
  end

  private
    def set_aim
      @aim = Aim.find(params[:id])
    end

    def aim_params
      params.require(:aim).permit(:title, :reason, :advantage, :difficulty)
    end
end

 

before_actionによって、
それぞれ、show, edit, updateアクションが実行される前に、
該当メソッド、この場合privateメソッドのset_aimが呼ばれます。

showアクションと、editアクションが空なのは、
このbefore_actionのset_aim関数の処理だけで足りているからです。

残りの、createやupdateは、
ほとんどRails Tutorialと同じようなコードだが、
aim_paramsを割り当てて、
DBへの保存が成功したら、
flash messageを出し、
失敗したら、それぞれの作成画面に戻し、
status 422を返すという感じです。

ストロングパラメータ

さて、上記のコードの
aim_paramsの部分だが、
これは、俗に言うストロングパラメータの部分です。

ストロングパラメータとは、
送られてくるparamsの値で
どれを許可するのか設定することです。

https://railsguides.jp/action_controller_overview.html

例えば、userモデルの作成において、
adminという、管理者かどうかというカラムが存在した場合、
ストロングパラメータで、許可するパラメータを指定しないと、
誰でも管理者になることができてしまうという、
脆弱性につながります。

そのため、ストロングパラメータで、
許可するパラメータを指定する必要があるのですが、
その書き方は、上記のように、

 def aim_params
   params.require(:aim).permit(:title, :reason, :advantage, :difficulty)
 end

のように、permitを使って、
この場合、title, reason, advantage, difficultyだけを許可するという感じです。

  • この記事を書いた人
  • 最新記事

reisuta

Webエンジニア | 20代中盤 | 大学時代はGmailすら知らないIT音痴でプログラミングとは無縁の生活を送る → 独学でプログラミングを学ぶ → Web系受託開発企業にエンジニアとして就職 → Web系自社サービス企業に転職 | 実務未経験の頃からVimを愛好しており、仕事でもプライベートでも開発はVimとTmuxを使っているので、VSCodeに疎いのが最近の悩み。何だかんだでやっぱりRubyが好き。

おすすめ記事はこちら

Vim/Neovimプラグイン 1

プラグインをどれだけ入れるかは、その人の思想なども関係するので、一概にこれがいいというのはないかもしれません。 プラグインを全く入れない人もいれば、100個以上入れる人もいます。 ただそれでも、これだ ...

VimとNeovimの比較 2

本記事では、VimとNeovimの違いについて、解説します。 VimとNeovimの違いについては、普段頻繁にVimなどを使う方でなければ、正直、あまり気にしなくてもいいかなと思います。 ただ、Vim ...

Ruby変数やすべてがオブジェクトについて 3

本記事は、Rubyの基礎文法である、変数や真偽値、論理演算子に触れると同時に、「すべてがオブジェクト」というRubyの特徴的な思想についても解説します。 この思想は、Rubyの文法の根幹になっているの ...

4

エンジニアにおすすめの技術書 書籍学習は、エンジニアの嗜みみたいなところがありますが、 良書というものは、意外とそこまで多くもありません。 そこで本記事では「技術書マニアの筆者が厳選した技術書20選」 ...

5

エンジニアになるには? プログラミングは、専門性が高く自分一人で勉強するのが大変に感じることも多いですよね。 そこで本記事では「おすすめのプログラミングスクール5選」を特徴と、現役エンジニア目線で優れ ...

-バックエンド