【RSpec】Feature specを調べた・書いた

環境

OS X Yosemite

Ruby 2.3.1

Rails 5.0.0

SQLite3

どう便利

書くことで、実装しなければいけないことが明確になります。
また、仕様からコードに落とし込むので、あいまいな点が早い段階でわかります。
さらに、後から参画するエンジニアにとって「あ、こんな機能あるんだ」という感じで、みんなハッピーです。

書いた

まず、specファイルを用意します。
spec/features/xxxxx_spec.rb
ファイルの末尾に`_spec`がついていれば良いです。

これからFeature specを書きますという宣言をします。

require 'rails_helper'

RSpec.feature '物件', :type => :feature do
end

次にどのようなユーザーの操作をシミュレートするか書きます。
scenarioはitに相当します。

require 'rails_helper'

RSpec.feature '物件', :type => :feature do
  # 追加
  scenario '編集ボタンを押したときに既存のデータが入力されている' do
  end
end

次にどのユーザーがどのURLにアクセスするか書きます

require 'rails_helper'

RSpec.feature '物件', :type => :feature do
  scenario '編集ボタンを押したときに既存のデータが入力されている' do
    # 追加
    visit edit_room_path
  end
end

`edit_room`は`rails routes`を実行するとわかります。

edit_room_pathはpath内に:idを要求してくるので、編集するオブジェクトを作成・取得します。

require 'rails_helper'

RSpec.feature '物件', :type => :feature do
  before do
    # ユーザーの作成
    Room.create(id: 1, name: 'ほげハイツ', price: 50000, address: '渋谷区神泉', building_age: 32, note: '備考')
  end
  # ユーザーの取得
  let(:room) { Room.find(1) }

  scenario '編集ボタンを押したときに既存のデータが入力されている' do
    # 引数にRoomオブジェクトを渡す
    visit edit_room_path room
  end
end

beforeの部分はbackgroundとも書けます。むしろそちらの方が良いです。
これでデータを編集するページにアクセスできるようになりました。
あとは期待する結果を書きます。

require 'rails_helper'

RSpec.feature '物件', :type => :feature do
  before do
    Room.create(id: 1, name: 'ほげハイツ', price: 50000, address: '渋谷区神泉', building_age: 32, note: '備考')
  end
  let(:room) { Room.find(1) }

  scenario '編集ボタンを押したときに既存のデータが入力されている' do
    visit edit_room_path room

    # 追加
    expect(page).to have_field '物件名', with: 'ほげハイツ'
    expect(page).to have_field '賃料', with: 50000
    expect(page).to have_field '住所', with: '渋谷区神泉'
    expect(page).to have_field '築年数', with: 32
    expect(page).to have_field '備考', with: '備考'
  end
end

let は given とも書けます。givenの方が良いです。

expect(page).to have_field '物件名', with: 'ほげハイツ'

はpage内に、「物件名」というラベルがあり、そのinputタグのvalue属性には'ほげハイツ'が入力されている
という意味になります。

次はFactoryGirlでテストに使用する共通のデータを書きます。

【Rails】RSpecでフォームから登録したデータをテストする

環境

OS X Yosemite

Ruby 2.3.1

Rails 5.0.0

SQLite3

やること

RSpecでデータが正しく登録できたか確認する。
RSpecの導入は省略。
INSERTされたデータが想定通りに登録されたか、確認する

1 Gemfileに追記する

2017年5月29日時点ではこんな感じ。
WARNINGが出るけどとりあえず無視する。

group :development, :test do
  gem 'rspec-rails', '~> 3.5'
  gem 'factory_girl_rails', '~> 4.2.1'
end

group :test do
  gem 'faker', '~> 1.1.2'
  gem 'capybara', '~> 2.2.0'
  gem 'database_cleaner', '~> 1.0.1'
  gem 'launchy', '~> 2.3.0'
  gem 'selenium-webdriver', '~>2.45.0'
end

2 rspec-railsのドキュメント通りに進める

着実に、公式ドキュメント通りに進めましょう。
GitHub - rspec/rspec-rails: RSpec for Rails-3+

3 データをPOSTする処理を書く

post :create, params: {
          room: {
              name: '神泉ハイツ',
              price: '40000',
              address: '渋谷区神泉',
              building_age: 12,
              note: '備考'
          }
      }

4 インサートしたデータを取得して確認する

expect(Room.last.name).to eq '神泉ハイツ'

他にもいい方法がありそうだけど、とりあえずここまで!

【Rails】カラムのデータ型を変える

環境

OS X Yosemite

Ruby 2.3.1

Rails 5.0.0

SQLite3

動機

何も考えずにテーブルを設計してしまい、カラムのデータ型を変更したい。

やること

Userテーブルのageカラムのデータ型をstringからintegerにしたい。

1 migrationファイルを用意します

rails g migration xxxxxxx <- 適切なクラス名

2 migrationファイルにupメソッドとdownメソッドを定義します

upメソッドに変更後の内容、downメソッドに変更前の内容を書きます。

rails db:migrate

でupメソッドの変更が実行されます。

rails db:rollback

でdownメソッドの変更が実行されます。

class ChangeAgeOnUsers < ActiveRecord::Migration[5.0]
  def up
    change_table :users do |t|
      t.change :age, :integer
    end
  end

  def down
    change_table :users do |t|
      t.change :age, :string
    end
  end
end

3 rails db:migrateを実行する

migrationファイルのupメソッドの内容が反映されているか、確認します。

sqlite> .schema users
CREATE TABLE "users" (
  "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  "name" varchar,
  "age" integer,
  "created_at" datetime NOT NULL,
  "updated_at" datetime NOT NULL
);

念のため、rails db:rollbackしてみて、ロールバックが期待通りに動作するか、確認します。

sqlite> .schema users
CREATE TABLE "users" (
  "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  "name" varchar,
  "age" varchar,
  "created_at" datetime NOT NULL,
  "updated_at" datetime NOT NULL
);

いい感じですね。

【Rails】プロジェクトの初期データを用意する

環境

OS X Yosemite

Ruby 2.3.1

Rails 5.0.0

動機

自分のプロジェクトをgit cloneした際に、今まで使っていたDBのデータがなくてつらさを感じた。
なんとかして、用意しておきたい。

どうするのか

Railsが提供しているコマンドにrails db:seedがあり、これを利用します。

1 seeds.rbファイルを用意する

db/seeds.rbを作成します。
当環境ではrails newした際に作成されていました。

2 seeds.rbファイルに初期データを記述する

User.create(
  name: "John", email: john@example.com
)
User.create(
  name: "Henry" email: henry@example.com
)

3 rails db:seed を実行する

エラーがあれば、親切なエラーメッセージが発生します。

【Ruby】Ruby BitsからRubyらしいコードを学ぶ - Expressions編 -

PythonからRubyに転向しようとしている私ですが、Pythonを始めた時にも"Pythonic"な書き方に悩まされました。

というわけで、CodeSchoolさんのRubyBitsからRubyらしいコードの書き方を学んでいきます。

1. if式でネガティブなコンディションをtrueとする場合はunlessを使う

flag = false

if !flag
    puts "condition true"
else
    puts "condition false"
end

これくらいなら読みにくくありませんが、condition部分は常にtrueになっていてほしいものです。
なので、unlessを使います。

flag = false

unless flag
    puts "condition true"
else
    puts "condition false"
end

少し読みやすくなったような気がします。

2.conditions(if/unless)を使用する際、1行で書けるなら1行で書く

flag = false

unless flag
    puts "condition true"
end

このように1行で書けるそうなものは1行で書きます。

flag = false

puts "condition true" unless flag

行数は減りましたが、若干読みづらい気がします。

3.if式のconditionにnilを使わない

RubynilPythonでのNoneだと認識しています。

input = ""

if input != nil
    puts "condition true"
else
    puts "condition false"
end

nilを使わないようにします。

input = ""

if input
    puts "condition true"
else
    puts "condition false"
end

読みやすくなりましたね。

4.ショートサーキットを利用した変数代入

# この前処理でユーザーからの入力を受け取る可能性があることが前提
user_input ||= ""

if user_input
    puts user_input
else
    puts "please your input"
end

なかなか使いどころが難しそうです。

5.変数の代入に、if式を利用する

Rubyのifは式なので、値を返します。その性質を利用してを変数の代入に利用します

flag = true

if flag
  result = "condition true"
else
  result = "condition false"
end

puts result

これはこうなります。

flag = true

result = if flag
  "condition true"
else
  "condition false"
end

puts result

resultへの代入が1つの式で済んでいるので、スマートになった気がします。

6.メソッド内でreturnをわざわざ明記しない

これはうっかりやってしまいそうです。

flag = true

def get_condition(flag)
  return_value = "condition init"
  if flag
    return_value = "condition true"
  else
    return_value = "condition false"
  end
  return return_value
end

puts get_condition(flag)

これはこうなります。

flag = true

def get_condition(flag)
  if flag
    "condition true"
  else
    "condition false"
  end
end

puts get_condition(flag)

すっきりして読みやすくなりました。

7.ショートサーキットを利用して、コードを短くする

A || B はAがtrueならAを返して、そうでなければ Bを返します。
その性質を利用するとコードが短くなります。

adventure_target = "hoge island"

def result_adventure(treasure_target)
  result = adventure_result(treasure_target)
  if result
    result
  else
    "treasure not found"
  end
end

puts result_adventure(adventure_target)
adventure_target = "hoge island"

def result_adventure(treasure_target)
  adventure_result(treasure_target) || "treasure not found"
end

puts result_adventure(adventure_target)

行数は減りましたが、またもや少し読みづらくなったような…

現在の所感

Rubyはなるべく変数を定義せず、1行当たりの処理量を増やすのかな、といったイメージです。
まだ続きのRubyBitsコースはあるのですが、有料なので、それをネタに記事を書いていいのか悩む…
いや、よくない! フリーコンテンツを探しに行こう!
僕のRuby道はこれからだ!!

【Rails】カスタムバリデーションを設定する

環境

Windows 8.1(けっこうつらい)

Ruby 2.3.1

Rails 5.0.0

動機

「xxxを入力してください」というエラーメッセージが切ない。
UXを向上させるため、人間味のあるエラーメッセージを設定したい。

どうするのか

こんな感じのバリデーションを設定していたとします。

class User < ApplicationRecord
  validates :name,
             presence: true
  validates :age,
             presence: true,
             numericality { only_integer: true, greater_than_or_equal_to: 0 }
end

カスタムバリデーションを行うメソッドを追加します

class User < ApplicationRecord
  validates :name,
             presence: true
  #validates :age,
  #           presence: true,
  #           numericality { only_integer: true, greater_than_or_equal_to: 0 }
  
  # カスタムバリデーションを設定
  validate  :validate_age

  # カスタムバリデーションを定義
  def validate_age
    @errors[:age] = "年齢には0以上の数字を入力してください" if age < 0
  end
end

実際にはこんな感じになります。
https://github.com/tarunama/rails-exam/pull/2/files

【Rails】Model.newにparamsを渡してしまうのは危険

こんなコードは危ない

# controllers/users_controller.rb
def create
  @user = User.new(params[:user])
  ...
end

ユーザー情報すべてを渡してしまっているため、
管理者フラグを含めるカラムが存在する場合、
リクエストにadmin=1相当のデータを含めることで、
意図しないユーザーに意図しない権限を渡してしまう。

どう防ぐのか

Strong Parametersで意図的に、リクエストのどのデータを渡すか明示する。

# controllers/users_controller.rb
def create
  # Strong ParametersをUser.newに渡している
  @user = User.new(user_params)
  ...
end

# 外部から変更されたくないのでprivateメソッドにする
private
  def user_params
    params.require(:users).permit(:name, :email)
  end