エラーメモ【TypeError】

エラーの解決過程を記録しておきます。
記事投稿アプリで記事内容を未入力で投稿しようとすると以下のようなエラーが発生しました。

TypeError - no implicit conversion of nil into String:

TypeErrorとは

メソッドの引数に期待される型ではないオブジェクトや、期待される振る舞いを持たないオブジェクトが渡された時に発生します。

エラー文を翻訳

no implicit conversion of nil into Stringを翻訳すると

nilからStringへの暗黙の変換がない

となりました。
string型に変換する必要がありそうです。

該当箇所を確認

現在の該当箇所のコードは

  def build_body(controller)
    result = ''

    article_blocks.each do |article_block|
      result << if article_block.sentence?
                  sentence = article_block.blockable
                  sentence.body
                end
    end

    result
  end

こんな感じです。
簡単に言うとコンテンツがあれば<<でresultに追加する、というコードです。
コンテンツが存在すれば正常に処理され画面が表示されますが、コンテンツがないままだとエラーが発生する状態です。

string型のオブジェクトを取得したいのにnil型だから受け取れない、とのことなのでまずは実際に何型のオブジェクトか見てみます。

Image from Gyazo

sentence.body.classsentence.bodynil型だと分かりました。
これをstring型にできれば解決するはずです。

どうやって型を変えるか

初めに思いついたのはto_sメソッドでstring型に変換するやり方です。

Image from Gyazo

想定通り、string型になりました。
このやり方でもエラーは解決しましたが、||=を使って空文字を明示する書き方の方が良いそうです。

  def build_body(controller)
    result = ''

    article_blocks.each do |article_block|
      result << if article_block.sentence?
                  sentence = article_block.blockable
                  sentence.body ||= ''
                end
    end

    result
  end

このようにしてエラー解決できました。

||= は何をしているか

||=は自己代入やnilガードと呼ばれるそうです。
左辺がnilまたはfalseの場合は右辺が代入され、それ以外の場合は代入はされません。
sentence.bodyに「テスト」と入力して確認してみます。
入力されているのでnilにはならないはずです。

Image from Gyazo

nil?メソッドでfalseが返ってきたのでsentence.bodyはnilではないことがわかります。
nilではないので右辺の''(空の文字列)は代入されず「テスト」となっていますね。

書籍などで||=この書き方を見たり、nilガードという言葉自体は見たことありましたが、実際に検証して理解が深まりました。

最後に

||= これだけ見ると記号か暗号のようですが、hoge = 1のような変数に代入するコードに||という関所があって、hogeが空っぽの時は1さんに「うむ、入れてやろう」、hogeに何か入ってる時は「今hogeは満員だ。帰れ」と言ってるイメージで覚えました。知らんけど。
今回は以上です。ありがとうございました。

参考サイト

class TypeError (Ruby 3.0.0 リファレンスマニュアル)

Rubyで使われる記号の意味(正規表現の複雑な記号は除く) (Ruby 3.0.0 リファレンスマニュアル)

【Ruby】使いこなせると便利。||演算子のいろんな使い方 - Qiita

Rubyの自己代入(||=)とはこんな仕組みです - Qiita

【Ruby入門】nilのポイントまとめ(nil? empty? blank? present?) | 侍エンジニアブログ

Rubyのnilガードについて調べてみた - Qiita

パンくずリストを作ろう

今回はパンくずリストを作ってみたいと思います。

パンくずリストとは

自分が今サイトのどこにいるかわかりやすく表示したものです。
Image from Gyazo
こんなやつです。

アプリ立ち上げ

アプリの新規作成からやっていきます。

rails new gretel_app

名前はgretel_appとしておきます。
ターミナルでgretel_appに移動したら

rails g scaffold task title:string body:text

こんな感じでscaffoldして

rails db:migrate

マイグレートして

rails s

サーバーを起動します。
タスク一覧はこんな感じです。
Image from Gyazo

gem gretelの導入

gretelというgemで簡単にパンくずリストを作ることができます。
Gemfileに

gem 'gretel'

として

bundle install

さらに

rails g gretel:install

とすると、config/breadcrumb.rbが作成されます。
これで準備できました。

breadcrumb.rbに設定を記述

ではパンくずリストの設定をしていきます。

crumb :root do
  link "Home", root_path
end

# crumb :projects do
#   link "Projects", projects_path
# end

# crumb :project do |project|
#   link project.name, project_path(project)
#   parent :projects
# end

# crumb :project_issues do |project|
#   link "Issues", project_issues_path(project)
#   parent :project, project
# end

# crumb :issue do |issue|
#   link issue.title, issue_path(issue)
#   parent :project_issues, issue.project
# end

# If you want to split your breadcrumbs configuration over multiple files, you
# can create a folder named `config/breadcrumbs` and put your configuration
# files there. All *.rb files (e.g. `frontend.rb` or `products.rb`) in that
# folder are loaded and reloaded automatically when you change them, just like
# this file (`config/breadcrumbs.rb`).

こちらが先程のコマンドで作成されたファイルの中身です。

crumb :new_task do
  link 'New Task', new_task_path
  parent :root
end

これでタスクの新規作成ページにパンくずリストを表示させる設定ができました。

crumb ページ名 do
  link ビューに表示したい名前, URL
  parent 親ページ名
end

省略しましたがroutes.rbでroot 'tasks#index'としています。

viewファイルに記述

viewの表示させたい場所に記述していきます。
全てのページに表示させたい場合はapplication.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title>GretelApp</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= breadcrumbs separator: " &rsaquo; " %> <!--ここを追加 -->
    <%= yield %>
  </body>
</html>

とします。
ページの上部にある方が分かりやすいのでyieldの上に置いてます。
separator: " &rsaquo; "の部分は、親ページと子ページの間の文字を設定しています。
この場合、「>」になるんですが&rsaquo;となっているのは、「>」はHTMLタグと認識されたりする場合があるためこのように特殊文字になっています。
他にもオプションがあるのでgretelのgithubのOptionsの項を参考にしてください。

次に各ページのファイルにも記述していきます。
新規作成ページ(new_task_path)を

<h1>New Task</h1>

<% breadcrumb :new_task %> <!-- ここで呼び出し -->

<%= render 'form', task: @task %>

<%= link_to 'Back', tasks_path %>

このようにしました。
breadcrumb.rbで設定したページ名を指定することでパンくずリストを表示させます。
では確認してみましょう。
Image from Gyazo
うまく表示できました。

次は詳細ページに表示させます。
breadcrumb.rbに

crumb :task_show do |task|
  link "About #{task.title}", task_path(task)
  parent :root
end

詳細ページはIDが必要なのでブロック変数を使ってこのようにします。
viewには

<% breadcrumb :task_show, @task %>

このように記述します。

最後に編集ページです。

crumb :edit_task do |task|
  link 'Edit Task'
  parent :task_show, task
end

編集ページは一番下の階層なのでlinkにはURLは必要ありません。
parentにもtaskを渡しておきます。
viewの方も

<% breadcrumb :edit_task, @task %>

として確認してみましょう。
詳細ページ
Image from Gyazo
編集ページ
Image from Gyazo
このようにこのように階層を重ねることもできます。

最後に

無事実装できました。
関係ないですがパンくずって何のことだろうとずっと思ってたんですが由来が面白いですね。
なぜhanselではなくgretelになったのかも気になるところです。
では今回は以上です。ありがとうございました。

参考サイト

GitHub - kzkn/gretel: Flexible Ruby on Rails breadcrumbs plugin.
【Rails】 gretelを使ってパンくずリストを作成しよう | Pikawaka - ピカ1わかりやすいプログラミング用語サイト
Rails パンくずリスト gretel の使い方 - Qiita

slimを使ってみよう

RailsではデフォルトのテンプレートエンジンはERBですが、他にもslimhamlがあります。
今回はslimの記法について見て行きます。

テンプレートエンジンとは

HTML画面を直感的にわかりやすいテンプレート形式で記述することができます。

必要gemの導入

slimでhtmlを書くためのgemをインストールします。
・slim-rails
hoge.html.slimのようにビューファイルの拡張子にslimが使えるようになります。
・html2slim
既存のerbファイルをslim形式に変換してくれます。
Gemfileに

gem 'slim-rails'
gem 'html2slim'

として

bundle install

これでgemがインストールできました。
サーバーは再起動しておきます。

erbをslimに変換

変換するためにターミナルでコマンドを入力します。

bundle exec erb2slim app/views

で元のerbファイルを残したまま変換されたhoge.html.slimファイルが作成されます。
元のerbを削除するには

bundle exec erb2slim app/views -d

とします。

slimの特徴

slimの特徴として
・<>、閉じタグがいらない
・<%= %> は =
・<% %> は -
・class指定 は .
・id指定 は #
・テキストは | の後に
コメントアウトは /
が挙げられると思います。
scaffoldアプリを作ったのでどんな感じになっているか見てみましょう。

p#notice
  = notice
h1
  | Users
table
  thead
    tr
      th
        | Name
      th
        | Email
      th[colspan="3"]
  tbody
    - @users.each do |user|
      tr
        td
          = user.name
        td
          = user.email
        td
          = link_to 'Show', user
        td
          = link_to 'Edit', edit_user_path(user)
        td
          = link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' }
br
= link_to 'New User', new_user_path

Image from Gyazo

まだ慣れていないので変な感じはしますが、コード量が減ってすっきりしていますね。
同名のerbファイルが残っているとerbの方が反映されるのでslimに移行するときはerbファイルは削除する必要があります。

最後に

慣れが必要ですが、覚えてしまえばerbより書くのも読むのも早くなりそうですね。
ただ、あえてslimからerbに戻した、という記事も見つけたので現場によってはデフォルトのerbのまま、ということもありそうです。
では今回は以上です。ありがとうございました。

参考サイト

【Ruby on Rails】テンプレートエンジンの種類(ERB,Haml,Slim) | プログラミングマガジン
【爆速で習得】Railsでslimを使う方法から基本文法まで - Qiita
【Rails】 slimの書き方をマスターしよう! | Pikawaka - ピカ1わかりやすいプログラミング用語サイト

SystemSpecを書いてみよう

前回の続きで今回はSystemSpecを書いてみます。

SystemSpecとは

システムテストのことをRspecではシステムスペックと言います。
システムテストとは、実際に使用される状況と同じ設定でテストを行い、想定通りに動作するか検証することを言います。

テストの作成

spec/system/user_spec.rbを作成し、ここにユーザーに関するテストを書いていきます。

require 'rails_helper'

RSpec.describe "Users", type: :system do

end

大きく分けて
・ページ遷移
・ユーザー新規作成
・ユーザー編集
・ユーザー削除
の4つに関するテストを作ります。
describeに対象、contextに条件、itに具体的なテスト内容を記述します。

require 'rails_helper'

RSpec.describe "Users", type: :system do
  describe 'ページ遷移確認' do
  end

  describe 'ユーザー新規作成' do
  end

  describe 'ユーザー編集' do
  end

  describe 'ユーザー削除' do
  end
end

まずは対象で分けてみました。
次はdescribeの中にcontextで条件を書いていきます。

require 'rails_helper'

RSpec.describe "Users", type: :system do
  describe 'ページ遷移確認' do
    context 'ユーザーの一覧ページに遷移' do

    end

    context 'ユーザーの新規作成ページに遷移' do

    end

    context 'ユーザーの編集ページに遷移' do

    end

    context 'ユーザーの詳細ページに遷移' do

    end
  end


  describe 'ユーザー新規作成' do
    context 'フォームの入力値が正常な場合' do

    end

    context '名前が未入力の場合' do

    end
  end

  describe 'ユーザー編集' do
    context 'フォームの入力値が正常な場合' do

    end

    context '名前が未入力の場合' do

    end
  end

  describe 'ユーザー削除' do

  end
end

こんな感じになりました。
テストは正常系異常系を作成します。
正常系は、全てのフォームが入力されている場合など、想定通りの操作をしたときの挙動を確認します。
異常系は、必須項目のフォームが空の場合など、想定外の操作をしたときの挙動を確認します。
なおユーザー削除は必ず成功する想定なので正常系のみです。

ページ遷移確認のテスト

RSpec.describe "Users", type: :system do
let(:user) { create(:user) }

  describe 'ページ遷移確認' do
    context 'ユーザーの一覧ページに遷移' do
      it 'ユーザーの一覧ページへのアクセスに成功する' do
        user_list = create_list(:user, 3)
        visit users_path
        expect(page).to have_content 'ユーザー一覧'
        expect(page).to have_content user_list[0].name
        expect(page).to have_content user_list[1].name
        expect(page).to have_content user_list[2].name
        expect(current_path).to eq users_path
      end
    end

    context 'ユーザーの新規作成ページに遷移' do
      it 'ユーザーの新規作成ページへのアクセスに成功する' do
        visit new_user_path
        expect(page).to have_content 'ユーザー新規作成'
        expect(current_path).to eq new_user_path
      end
    end

    context 'ユーザーの編集ページに遷移' do
      it 'ユーザーの編集ページへのアクセスに成功する' do
        visit edit_user_path(user)
        expect(page).to have_content 'ユーザー編集'
        expect(page).to have_field 'Name', with: user.name
        expect(page).to have_field 'Gender', with: user.gender
        expect(current_path).to eq edit_user_path(user)
      end
    end

    context 'ユーザーの詳細ページに遷移' do
      it 'ユーザーの詳細ページへのアクセスに成功する' do
        visit user_path(user)
        expect(page).to have_content 'ユーザー詳細'
        expect(page).to have_content user.name
        expect(page).to have_content user.gender
        expect(current_path).to eq user_path(user)
      end
    end
  end
end

例として2つ目のテストを挙げると
visit new_user_pathユーザー新規作成ページを訪れ
expect(page).to have_content 'ユーザー新規作成'そのページに「ユーザー新規作成」と表示されていて
expect(current_path).to eq new_user_path今いるpathがnew_user_path
ならテストが通ります。

let(:user) { create(:user) }は、itの中でその都度user = create(:user)でuserを作成するのではなく、予め定義だけしておいてuserで呼び出せるようにしています。

残りのテスト

全部説明すると長くなってしまうので残り一気にいっちゃいます。
最終的なコードがこちら。

require 'rails_helper'

RSpec.describe "Users", type: :system do
let(:user) { create(:user) }

  describe 'ページ遷移確認' do
    context 'ユーザーの一覧ページに遷移' do
      it 'ユーザーの一覧ページへのアクセスに成功する' do
        user_list = create_list(:user, 3)
        visit users_path
        expect(page).to have_content 'ユーザー一覧'
        expect(page).to have_content user_list[0].name
        expect(page).to have_content user_list[1].name
        expect(page).to have_content user_list[2].name
        expect(current_path).to eq users_path
      end
    end

    context 'ユーザーの新規作成ページに遷移' do
      it 'ユーザーの新規作成ページへのアクセスに成功する' do
        visit new_user_path
        expect(page).to have_content 'ユーザー新規作成'
        expect(current_path).to eq new_user_path
      end
    end

    context 'ユーザーの編集ページに遷移' do
      it 'ユーザーの編集ページへのアクセスに成功する' do
        visit edit_user_path(user)
        expect(page).to have_content 'ユーザー編集'
        expect(page).to have_field 'Name', with: user.name
        expect(page).to have_field 'Gender', with: user.gender
        expect(current_path).to eq edit_user_path(user)
      end
    end

    context 'ユーザーの詳細ページに遷移' do
      it 'ユーザーの詳細ページへのアクセスに成功する' do
        visit user_path(user)
        expect(page).to have_content 'ユーザー詳細'
        expect(page).to have_content user.name
        expect(page).to have_content user.gender
        expect(current_path).to eq user_path(user)
      end
    end
  end


  describe 'ユーザー新規作成' do
    before { visit new_user_path }

    context 'フォームの入力値が正常な場合' do
      it 'ユーザーの新規作成が成功する' do
        fill_in 'Name', with: 'test_name'
        select '男性', from: 'Gender'
        click_button '登録する'
        expect(page).to have_content 'User was successfully created.'
        expect(current_path).to eq '/users/1'
      end
    end

    context '名前が未入力の場合' do
      it 'ユーザーの新規作成が失敗する' do
        fill_in 'Name', with: nil
        select '男性', from: 'Gender'
        click_button '登録する'
        expect(page).to have_content '1 error prohibited this user from being saved:'
        expect(page).to have_content 'Nameを入力してください'
        expect(current_path).to eq users_path
      end
    end
  end

  describe 'ユーザー編集' do
    before { visit edit_user_path(user) }

    context 'フォームの入力値が正常な場合' do
      it 'ユーザーの編集が成功する' do
        fill_in 'Name', with: 'update_name'
        select '男性', from: 'Gender'
        click_button '更新する'
        expect(page).to have_content 'User was successfully updated.'
        expect(current_path).to eq '/users/1'
      end
    end

    context '名前が未入力の場合' do
      it 'ユーザーの編集が失敗する' do
        fill_in 'Name', with: nil
        select '男性', from: 'Gender'
        click_button '更新する'
        expect(page).to have_content '1 error prohibited this user from being saved:'
        expect(page).to have_content 'Nameを入力してください'
        expect(current_path).to eq user_path(user)
      end
    end
  end

  describe 'ユーザー削除' do
    let!(:user) { create(:user) }

    it 'ユーザーの削除が成功する' do
      visit users_path
      click_link 'Destroy'
      expect(page.accept_confirm).to eq 'Are you sure?'
      expect(page).to have_content 'User was successfully destroyed.'
      expect(current_path).to eq users_path
    end
  end
end

・ユーザー新規作成のbefore { visit new_user_path }
describe 'ユーザー新規作成'内の2つのcontextは両方ともまずnew_user_pathにアクセスするところから始まります。それぞれのテストにいちいち書くのはDRYに反しますね。なのでbeforeでまとめてしまいましょう。ユーザー編集のbeforeも同様です。
・ユーザー削除のlet!(:user) { create(:user) }
先ほどのletと違い、!がついています。
2つの違いはuserが作成されるタイミングです。

・letはuserが呼び出されたときに作成され、
・let!はテスト実行前に作成されます。

はい、よくわかりません。
具体的に見てみます。ユーザー編集の正常系のテストはこのようになっています。

RSpec.describe "Users", type: :system do
let(:user) { create(:user) }

略

  describe 'ユーザー編集' do
    before { visit edit_user_path(user) }

    context 'フォームの入力値が正常な場合' do
      it 'ユーザーの編集が成功する' do
        fill_in 'Name', with: 'update_name'
        select '男性', from: 'Gender'
        click_button '更新する'
        expect(page).to have_content 'User was successfully updated.'
        expect(current_path).to eq '/users/1'
      end
    end

該当のユーザーの編集ページを訪れ
Nameに「update_name」と入力し
Genderは「男性」を選択
「更新する」ボタンをクリックすると
ページに「User was successfully updated.」と表示されていて
そのページのpathは「/users/1」になっている
とテストが成功する。

という内容です。
この最初のvisit edit_user_path(user)、ここでuserが出てきてます。このタイミングで実際にcreate(:user)されています。

次にユーザー削除のテストを見てみましょう。

  describe 'ユーザー削除' do
    let!(:user) { create(:user) }

    it 'ユーザーの削除が成功する' do
      visit users_path
      click_link 'Destroy'
      expect(page.accept_confirm).to eq 'Are you sure?'
      expect(page).to have_content 'User was successfully destroyed.'
      expect(current_path).to eq users_path
    end
  end

こちらはlet!(:user) { create(:user) }としてあります。
このテストは

users_path(ユーザー一覧ページ)を訪れ
「Destroy」というリンクをクリック
アラートで「Are you sure?」と表示され
OKすると「User was successfully destroyed.」と表示され
users_pathに遷移される

とテスト成功です。
が、itの中を見てみるとuserはどこにも呼び出されていません。
しかしそもそもユーザーが1件もないとDestroyというリンクは表示されないような実装にしているので、itの中でuserを作成する必要があります。
let(:user) { createa(:user) }と定義しているだけではuserは作成されないので、let!(:user) { create(:user) }でテスト実行前に予め作成しておかないといけません。

ユーザー削除のテストの結果をletとlet!で見比べてみます。
letの場合

Image from Gyazo

let!だと想定通り、テストが通りますが、
letの場合

Image from Gyazo

「Destroy」なんて見つからない、と言われてしまいます。
前述の通り、ユーザーが0件だと「Destroy」が表示されない、つまりユーザーが作成されていない。
削除のテストを実行するためにはやはりletではなくlet!でユーザーの作成が必要だとわかります。

最後に

簡単なCRUDアプリなのでシンプルなテストになりました。もっと多機能なアプリだとテストも複雑になりそうです。あとテストではハードコーディングしたほうがいいのかなー、と思ったんですがどうなんでしょう。テスト件数が増えるとメンテナンスが大変になりそうですがそっちの方が堅牢なテストになりそうな気もします。
今回も自分にとってしっくり来る表現をしたので厳密には間違ってたりするかもしれません。ご指摘あればコメントいただけると嬉しいです。
長くなりましたが、読んでいただきありがとうございました。

参考サイト

使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita
Module: Capybara::Node::Actions — Documentation for jnicklas/capybara (master)
システムテストとは?開発段階のテストの流れと主な種類|発注成功のための知識が身に付く【発注ラウンジ】<
RSpecの(describe/context/example/it)の使い分け - Qiita
【直感的に書ける!】RspecでRubyのテストを覚えよう! | プロぽこ
RSpec の letとlet!とbeforeの挙動と実行される順番 - Qiita

Rspecを書いてみよう

初めてテストコードを書いたのでおさらいのためにまとめてみます。
RailsはMInitestというテスティングフレームワークを備えていますが今回は
以前作った簡単なCRUDアプリ
を元にRspecを導入してテストを書いてみました。
テストが必要なほどの機能はないのですが練習のため。
2つの違いはよくわかってませんがRspecの方が利用率は高いそうです。
ではまずは導入からやっていきます。

必要gemのインストール

必要なgemを4つインストールします。

group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
end

group :test do
  gem 'capybara'
  gem 'webdrivers'
end

rspec-rails

テスティングフレームワーク
これがないと始まらない。
GitHub - rspec/rspec-rails: RSpec for Rails 5+

factory_bot_rails

テスト用のデータを簡単に作成できるgem。
GitHub - thoughtbot/factory_bot_rails: Factory Bot ♥ Rails

capybara

E2Eテスティングフレームワーク
ブラウザでテストしてくれる。
GitHub - teamcapybara/capybara: Acceptance test framework for web applications

webdrivers

調べたんですがあんまりわかってません。
capybaraとセットで使われるっぽい。
selenium-webdriverchromedriver-helperの代わりにこのwebdrivers1つでいいみたいです。
GitHub - titusfortner/webdrivers: Keep your Selenium WebDrivers updated automatically

bundle install

でインストールします。

諸々の設定

rails g rspec:install

とすると

Running via Spring preloader in process 63316
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

これらが作成されます。
.rspec--format documentationを追加するとテスト結果の表示が変わります。
お好みで設定しましょう。

rails_helper.rbの

Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

これのコメントアウトを外すとspec/support以下のファイルを読み込みます。
モジュールなどを作成する場合はsupportディレクトリを作成し、その中に置きます。
このアプリにログイン機能はまだないので以下は例です。

# support/system_helper.rb

module LoginModule
  def login(user)
    visit '/login'
    fill_in 'Name', with: user.name
    fill_in 'Email', with: user.email
    fill_in 'Password', with: '00000000'
    click_button 'Login'
  end
end

support/capybara.rbを作成して

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium_chrome_headless
  end
end

ドライバーの設定もしておきます。
selenium_chrome_headlessを指定しておくとテスト実行の度にブラウザを立ち上げずにテストしてくれます。

テスト用データの作成

rails g rspec:model user

このコマンドでmodels/user_spec.rbとfactories/user.rbが作成されます。

FactoryBot.define do
  factory :user do
    
  end
end

こちらがfactories/user.rbの中身です。
ここでテスト用のデータを作成します。 userモデルとスキーマのusersテーブルは

class User < ApplicationRecord
  validates :name, presence: true
  enum gender: { male: 0, female: 1, secret: 2 }
end
  create_table "users", force: :cascade do |t|
    t.string "name", null: false
    t.integer "gender", default: 0, null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

こんな感じです。

FactoryBot.define do
  factory :user do
    name { 'test_name' }
    gender { 'male' }
  end
end

名前と性別を設定しておきました。
テストで使うデータの初期値を設定しておく、というイメージです。
また、rails_helper.rbに

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

これを追加しておくと、テストでデータを作るときに

# 設定なし
user = FactoryBot.build(:user)
# 設定あり
user = build(:user)

このように省略できるので設定しておきます。

テストの作成

ようやくテストの作成です。
今回はバリデーションに関するテストを作ってみます。
先ほど作成したmodels/user_spec.rbに

require 'rails_helper'

RSpec.describe User, type: :model do
  describe 'バリデーション' do
    it '全てのフォームが正常な場合に成功する' do
      user = build(:user)
      expect(user).to be_valid
      expect(user.errors).to be_empty
    end

    it 'Nameが空の場合に失敗する' do
      user = build(:user, name: nil)
      expect(user).to be_invalid
      expect(user.errors[:name]).to eq ['を入力してください']
    end
  end
end

2つのテストを書いてみました。

1つ目のテストを説明すると、
・user = build(:user)
factories/user.rbの情報を元にユーザーを作成しuserに代入
・expect(user).to be_valid
userがバリデーションを通ることを期待
・expect(user.errors).to be_empty
user.errorsが空であることを期待
となります。

2つ目だと
・user = build(:user, name: nil)
初期値を元にするがnameは空にする
・expect(user).to be_invalid
userがバリデーションに引っかかることを期待
・expect(user.errors[:name]).to eq ['を入力してください']
エラー文、「を入力してください」と表示されることを期待
となります。

それではテストを実行してみましょう。

bundle exec rspec

とすると

Image from Gyazo

通りました。

最後に

ほんとに簡単なところだけですがテストを作って実行するところまでできました。実装のコードは間違っていたらエラーが出ますが、テストはそもそもテストコードをちゃんと書かないと意味を成さない気がするので気を付けたいです。何か間違いなどありましたらコメントいただけると嬉しいです。
では今回もありがとうございました。

参考サイト

E2Eテスト
RSpecとminitestのおおまかな違い - Qiita
FactoryBotでテストデータ作成する方法 - Qiita
【Rails】『RSpec + FactoryBot + Capybara + Webdrivers』の導入&初期設定からテストの書き方まで | vdeep

git, github超基礎

git、githubについてあまり分かってないまま学習してきたので、かなり初歩的な内容になると思いますがまとめてみます。

gitとは

gitとは分散型バージョン管理システムのことです。
分散型と集中型がありますが集中型についてはここでは扱いません。
バージョン管理とはいつ、誰が、どんな編集をしたか、その履歴を管理することです。
履歴ごとにセーブポイントを作って、どの地点にも戻ることができます。

githubとは

ソースコードを管理するWEBサービスです。

どのように管理するか

gitで作った履歴をgithubでオンラインで管理する、ということがわかりました。
では、もう少し具体的に見ていきたいと思います。

リポジトリ

ファイルの保管場所のことをリポジトリと言います。
プロジェクト単位でリポジトリを作ることが一般的なようです。
リポジトリ

に分かれます。
gitが用意してくれているサーバーに対してファイルをアップロード、ダウンロードすることでファイルの共有ができますが、このインターネット上に保存したリポジトリリモートリポジトリと言います。
githubアカウントでリモートリポジトリを作成し、自分のPCにコピーしたものがローカルリポジトリです。

クローン

リモートからローカルにリポジトリをコピーすることをクローンと言います。

git clone [URL]

でクローンできます。

コミット

クローンしたファイルで開発を進め、きりのいいところでセーブします。
このセーブのことをコミットと言います。

git status

というコマンドで変更したファイルの一覧が表示されるので、

git add [ファイル名]

で指定したファイルがgitの管理下に置かれます。この状態をステージングと言います。
ステージング状態ではまだセーブされていません。セーブするには

git commit -m 'コミットメッセージ '

とします。
コミットにはどんな変更を行ったかを端的に表すコミットメッセージを付けておきます。
これがないと後から戻ろうとしてもどのセーブポイントに戻ればいいのか分からなくなります。
また、コミット履歴を確認するには

git log

コマンドを実行します。

プッシュ

ローカルの変更箇所をセーブできましたが、この時点ではまだリモートリポジトリには反映されていません。
リモートに反映させることをプッシュと言います。

git push origin

originとはリモートリポジトリのことです。
このコマンドでリモートにも自分がコミットした内容が反映されます。

プル

最新のリモートリポジトリの情報をダウンロードすることをプルと言います。

git pull origin

このコマンドで、他の開発者がプッシュした内容をプルしてローカルに持ってきます。

ブランチ

ブランチとは作業場所のようなものです。
gitの変更履歴を枝分かれさせ、新しい作業場所を作ることをブランチを切る、と言います。
元となる作業場所はマスターと言います。

masterーーcommitーーcommitーーcommitーーcommitーーcommitーー
branch └ーーcommitーーcommit--commitーー

git checkout -b ブランチ名 master

checkout自体はブランチを移動するコマンドです。
-bと付けることで新しいブランチを作り、ブランチ名 master でmasterから新しいブランチに移動できます。
現在のブランチを確認するには

git branch

を実行します。

プルリクエス

ブランチを切って開発を進め、コミット、プッシュまでしたらgithubプルリクエストを行います。
プルリクエストとは、他の開発者にレビューしてもらい、変更内容がOKならマージしてください、という依頼です。

マージ

ブランチをマスターに合流させることをマージと言います。
マスターブランチにマージする際はマスターに移動して

git marge 合流させるブランチ名

とします。
マージするまではブランチのコミットはマスターには反映されません。
実務では初めのうちはマージすることはなさそうな気がしますがどうなんでしょう。
僕はまだ学習中なのでわかりませんが、マージ怖い、という印象です。

大まかな流れはこんな感じかな、と思います。

最後に

厳密には違う表現もあるかも知れませんが、大体のイメージは掴めるんじゃないかなと思います。
必須知識らしいのでちゃんと身につけたいですね。
では今回は以上です。ありがとうございました。

参考サイト

【Git入門】Git + Github使い方入門講座🐒Gitの仕組みや使い方を完全解説!パーフェクトGit入門! - YouTube

GitHubとは?覚えておくべき7つの基礎知識や使い方を解説 – IT業界、エンジニア、就活生、第二新卒、転職者、20代向け情報サイト

JavaScript DOM操作練習してみた

今回はJavaScriptのDOM操作を練習してみます。

DOMとは

DOM (Document Object Model) とは、すべての HTML または XML 文書を表現・操作する API です。 DOM はブラウザーで文書構造をノードのツリーとして読み込み、それぞれのノードを文書の一部 (例えば要素、テキスト文字列、コメント) として表します。引用元

この仕組みを使ってJavaScriptを使ってHTMLの要素を取得、操作します。

実践

早速やってみます。
まず適当にファイルを作りHTMLを書いていきます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Practice</title>
</head>
<body>

<p id="text">ここが変わります</p>
<button id="button">クリック</button>

</body>
</html>

ブラウザで開いてみるとこんな感じです。

Image from Gyazo

こうなりました。
まだhtmlしか書いてないので当然クリックしても何も起きません。

ここから、ボタンをクリックしたら「ここが変わります」の部分が変わるようにしていきます。
とりあえずJavaScriptのコードを書き込む場所を作ります。

<body>

<p id="text">ここが変わります</p>
<button id="button">クリック</button>

  <script>

  </script>
</body>

ちょっと練習する用なのでbodyタグの一番下にscriptタグを書いて直接htmlファイルに書きます。
script内をこのようにします。

  <script>
    document.addEventListener('DOMContentLoaded', () => {
      const text = document.getElementById("text");
      const button = document.getElementById("button");

      button.addEventListener('click', () => {
        text.innerText = ('変わった?');
      });
    });
  </script>

すると

Image from Gyazo

変わりましたね。
ではscriptタグの中を見ていきましょう。
documentには現在表示しているページのすべてのHTMLやCSSJavaScriptなどの文章が入っています。
試しにscript内にconsole.log(document);を入れてみると

Image from Gyazo

コンソールにファイルの内容が表示されました。
addEventListenerはイベントを実行するメソッドです。
引数のDOMContentLoadedイベントは HTML ページの読み込みが完了しHTML のパース(解析)が完了してDOM ツリーの構築が完了した時点で発生するイベントです。
正直、まだあまり理解できていないのでおまじないだと思ってます。

さらに中を見ていきます。
getElementByIdでhtml要素を取得します。
何を頼りに要素を探すかというと、その名の通りIdです。
そのためhtml要素にはidを設定している必要があります。
const text = document.getElementById("text");で、textというidが付いている要素を取得してきて、textという定数に代入しています。
const button = document.getElementById("button");も同様です。
button.addEventListener('click', () => {定数buttonをクリックしたときにイベントが起きます。
text.innerHTML = ('変わった?');定数textを`innerHTMLで要素を置き換えています。

流れとしてはこんな感じです。
他にもドットインストールで簡単なゲームを作ったりすればどんなことができるかわかりやすいと思います。

最後に

初めは何をやってるのかさっぱりでしたがなんとか動きました。基本は取得→操作の流れなのでしっかり考えれば色々遊べそうですね。
では今回は以上です。ありがとうございました。

参考サイト

【JavaScript】DOMを理解してHTMLを操作してみよう | RAKUMAオンラインスクール
JavaScript入門 | Let'sプログラミング
【JavaScript入門】addEventListener()によるイベント処理の使い方! | 侍エンジニアブログ
MDN Web Docs
はじめてのJavaScript (全11回) - プログラミングならドットインストール