20200310のRailsに関する記事は25件です。

正規表現

正規表現とは

特徴的なのは
電話番号のハイフンを取り除く(文字列の一部分を置換する)
パスワードに英数字8文字以上という制約を設定する(文字列が制約を満たしているか調べる)
メールアドレスからドメインの部分のみ抽出する(文字列の一部分を抽出する)
などがあります。
正規表現にはsubメソッドとmatchメソッドがあります。

subメソッド

文字列の指定した部分を別の文字列に置き換えるためのメソッドです。
【例】ターミナル

$ irb
irb(main):001:0> str = "水を飲む"
=> "水を飲む"

irb(main):002:0> str.sub(/水/,"お茶")
=> "お茶を飲む"

操作したい文字列は/で囲みます。

matchメソッド

引数に指定した文字列がレシーバの文字列にあるかを確認するためのメソッドです。ある場合は、指定した文字列がMatchDataオブジェクトの返り値になります。ない場合は、返り値としてnilになります。
【例】ターミナル

rb(main):001:0> str = "おはよう, おやすみ"
=> "おはよう, おやすみ"

irb(main):002:0> str.match(/おはよう/)
=> #<MatchData "おはよう">

irb(main):003:0> str.match(/こんにちわ/)
=> nil

MatchDataオブジェクト

マッチした文字列等はMatchDataオブジェクトで返されます。MatchDataオブジェクトから文字列等を取り出してみます。
【例】ターミナル

irb(main):001:0> str = "おはよう, おやすみ"
=> "おはよう, おやすみ"

irb(main):002:0> md = str.match(/おはよう/)
=> #<MatchData "おはよう">

irb(main):003:0> md[0]
=> "おはよう"

ハイフンの取り除きかた

subメソッドでは最初のハイフンしか置換されないのでgsubメソッドを使います。
【例】

irb(main):001:0> tel = '090-1234-5678'
=> "090-1234-5678"

irb(main):002:0> tel.sub(/-/,'')
=> "0901234-5678"

irb(main):003:0> tel.gsub(/-/,'')
=> "09012345678"

このgがは、指定した文字が複数ある場合、その全てを置換します。

パスワードに英数字の制約

【例】matchメソッドを使用します。

rb(main):001:0> pass = 'Hoge1234'
=> "Hoge1234"

irb(main):002:0> pass.match(/[a-z\d]{8,}/i)
=> #<MatchData "Hoge1234">

ここでは今までとかなり違う部分が出てきました。意味合いは
・[a-z]: 角括弧で囲まれた文字のいずれか1個にマッチ
・\d: 数字にマッチ
・{n, m}: 直前の文字が少なくともn回、多くてもm回出現するものにマッチ
・i: 大文字・小文字を区別しない検索
・.: どの1文字にもマッチ
・+: 直前の文字の 1 回以上の繰り返しにマッチ

[a-z]

角括弧で囲まれた文字の1つがマッチするかをチェックしています。また、-で範囲を設定することができます。指定なので[a-z]だけではなく[a-c]みたいにもできます。
【例】

irb(main):001:0> 'dog'.match(/[a-c]/)
=> nil
irb(main):002:0> 'dog'.match(/[a-d]/)
=> #<MatchData "d">

となります。

\d

\dは数字にマッチするので、[a-z\d]は「英数字のいずれか1つにマッチ」という意味になります。
【例】数字のみ抽出します

irb(main):001:0> 'abc 3 xyz'.match(/\d/)
=> #<MatchData "3">

{n, m}

直前の文字が少なくともn回、多くてもm回出現するものにマッチします。例えば{4,8}は、直前の文字が少なくとも4回出現、直後の文字が最大8回出現にマッチという意味になります。
【例】

irb(main):001:0> '12345678'.match(/\d{4,6}/)
=> #<MatchData "123456">

irb(main):002:0> '123'.match(/\d{4,6}/)
=> nil

i

大文字・小文字を区別しない検索します。iを付けない場合だと、[a-z]と小文字で記述しているので大文字にマッチしません。
【例】

irb(main):001:0> 'Dog'.match(/dog/)
=> nil

irb(main):002:0> 'Dog'.match(/dog/i)
=> #<MatchData "Dog">

.

ハイフンやピリオドなど含めたどの1文字にもマッチします。
【例】

irb(main):001:0> 'abc'.match(/./)
=> #<MatchData "a">

+

直前の文字が1回以上の繰り返しにマッチします。
【例】

irb(main):001:0> 'aaabbc'.match(/a+/)
=> #<MatchData "aaa">

.と+は何かしらの文字が1回以上繰り返すものにマッチします。例えばメールアドレスみたいに先頭に@を付けることで@から始まり、何かしらの文字が1回以上繰り返すものにマッチします。
【例】

irb(main):001:0> mail = 'hoge@takashi.com'
=> "hoge@takashi.com"

irb(main):002:0> mail.match(/@.+/)
=> #<MatchData "@takashi.com">
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on rails で星を散らす

はじめに

railsで、リロードする度に見え方が変わる星空を再現してみます
star-sky.jpg

作成したWebアプリにも採用しています。

実行

最初に次のファイルを用意します。

index.html.haml
.sky
.stars
star_sky.css
.sky{
background-color: #014; //黒っぽければなんでもいい
}

次に、星を表すオブジェクトを用意します。

index.html.haml
.sky
.stars
  .star
star_sky.css
.sky{
background-color: #014; //黒っぽければなんでもいい
}

.star{
  border-radius: 50%;
  position: fixed;
  z-index: 0;
  -ms-filter: blur(6px);
  filter: blur(6px);
}

星が一つだけなのもおかしいので、複数表示させます。
また、リロードする度に、数が変わるようにもしてみます。
使用するのはrandメソッドtimesメソッドです。

index.html.haml
.sky
.stars
  - number = rand(100..200)
  - number.times do
    .star

これだけだと、同じ場所にひたすら100個以上の.starが作られるだけです。
そのため、times.starが一つ生成される度に、位置がランダムで指定されるようにします。
それにはCSSを適用すれば良いですが、CSSはHamlのコードにも直接指定できるので、これと変数展開を利用します。

index.html.haml
.sky
.stars
  - number = rand(100..200) //100から200までの間の数字をランダムで
  - number.times do
    - top   = rand(100)
    - left  = rand(100)
    .star{style: "top: #{top}%; left: #{left}%; height: #{size}px;}

次に星の色と大きさを指定します。
折角なので、全て違う色と大きさにしてみます。

index.html.haml
.sky
.stars
  - number = rand(100..200)
  - color  = ["red","blue","green","yellow","purple","white","pink","orange"]
  - number.times do
    - top   = rand(100)
    - left  = rand(100)
    - size = rand(10..20)
    - cnum = rand(0..7)
    .star{style: "top: #{top}%; left: #{left}%; height: #{size}px; width: #{size}px; background-color: #{color[cnum]}"}

これで完了です。
試しに画面をリロードしてみると、星の配置が変わると思います。
star-sky2.jpg

今回はほぼrandメソッドを利用しましたが、
頑張って数式を適用すれば、もっと幾何学的で美しい背景を作ることができそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アプリケーションを作ってみます

自分用メモ

new コマンド実行
rails 5.0.7.2 new Put.Memo -d mysql
データベースを作成
rails db:create

作るのは単語帳アプリ
最初にデータベース設計をしてみて自分のしてみたいことを整理してみた
機能として
---アカウント機能
---投稿機能
---いいね機能
---検索機能
---意味の部分にurl,画像を描けるようにする
---ランキング機能

82486AC1-7B32-4DAC-8A93-1D76A94AED99_1_105_c.jpeg

次にフロント実装

topページには
:スクロール機能
:ランキング機能

↑とりあえずこれだけ実装したい
できればタグごとのランキング,ページネーション機能をつけたい

ビューはこんな感じ
単語、意味とurlと画像
で一つの投稿としてこれをランキングで複数並べる形にしたい
Put.Memoのところをクリックするとでtopページに戻るようにした
ヘッダーだけだけどまぁ機能としては上だけで十分だと思う
https://gyazo.com/429eb62d3c07605ff058e91be4f43712

後で
タイトルのところに作った人のアカウント名を付けようと思う

とりあえず今日はここまで

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

中間テーブルとは 〜DB設計〜

概要

アソシエーションの中で多対多の関係性で使われる中間テーブル。

今回、中間テーブルとは何か? 自分の復習用に記事を執筆しようと思います。

そもそもアソシエーションとは?

Railsで使われているテーブルとテーブルを関連づける機能。

例えば、TwitterではUserとtweetがあるとして、

二つはUserがtweetすることでtweetが生じる関連付いた関係性である。

アソシエーションを組まないとどうなるのか?

AさんがtweetしたtweetがAさんと関連づけされなくなる。

AさんのtweetもBさんのtweetも一緒くたに扱われるため、外部キーのIDを取得する必要が出てきます。

一対多、一対一の関係性

アソシエーションの中には一対多、一対一

最後に中間テーブルが使われる多対多の関係性が存在する。

一対多

最初に一対多とは親モデルが子モデルに対して多数の要素を持つこと。

Twitterの場合、一つのUser(親モデル)は沢山のtweet(子モデル)をします。

Userモデル(親モデル)

ID Username
1 tarou
2 jirou
3 saburou
4 sirou
5 gorou

Tweetモデル(子モデル)

ID User_id text
1 1 あいうえお
1 1 たろうです
2 2 かきくけこ
3 3 さしすせそ
4 4 たちつてと
5 5 なにぬねの

railsでUserモデル(親モデル)に

User.rb
has_many :tweet

has_manyの記述を加えます。

Tweetモデル(子モデル)に

Tweet.rb
belongs_to :user

belongs_toの記述を加えます。

一対一

一対一は関連するモデルの立場が対等です。

Userモデル

ID Username
1 tarou
2 jirou
3 saburou
4 sirou
5 gorou

Profileモデル

ID User_id text
1 1 よろしく
2 2 じろうです
3 3 さぶろうです
4 4 しろうです
5 5 ごろうです

Userモデルに対するProfileといった具合に同等の関係を示す場合に使われます。

railsでUserモデル(親モデル)に

User.rb
has_one :profile

has_oneの記述を加えます。

Profileモデル(子モデル)に

Tweet.rb
belongs_to :user

belongs_toの記述を加えます。

中間テーブルとは?

二つのモデルが多対多の関係性の時に使われるモデル。

ChatアプリのDB設計をする際、UserモデルとGroupモデルがあるとします。

Userは複数のチャットGroupに属し、チャットGroupも複数のUserが所属している。

この場合、中間テーブルが必要になってきます。

なんで中間テーブルを使う必要があるの?

例えば、Userが1と3のChatグループに属していたとします。

中間テーブルを挟まない場合、Groupに所属することを示すカラムが2つ必要になります。

Userが1と2と3のChatグループに属していたら、3つのカラムが必要になります。

際限なくカラムが多くなってくるので、それを防ぐために中間テーブルが必要になります。

Memberモデル(中間テーブル)

ID User_id group_id
1 1 1
2 1 2
3 2 1
4 2 3
5 3 2

この様に中間テーブルを挟むことでカラムを増やす必要がなくなります。

User.rb
has_many :group, through: :member
group.rb
has_many :user, through: :member

モデルファイルに以上の記述を行えば、多対多のアソシエーションが設定されます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【RSpec】System Specが動作するRails+Docker開発環境を構築する

はじめに

Rspec3.7以降で実装された"System Spec"を動作できるRails+Docker環境の作り方をまとめました。

基本コピペで進められるように書きましたので、初学者の方であっても「まず動く」状態まで持っていけると思います。

この記事では、Docker、docker-composeがすでにインストールされた状態からスタートします。私は、自PC(Mac)のホストOSに「Docker for Mac」をインストールしています。

著者動作環境における、Docker、docker-composeのバージョンは以下の通りです。

$ docker --version
Docker version 19.03.5, build 633a0ea
$ docker-compose -v
docker-compose version 1.25.4, build 8d51620a

DockerでのRails環境の構築

まずは、Docker公式が用意してくれているRals用のクイックスタート「Quickstart: Compose and Rails」をベースに、Railsが動作するDocker環境を作成していきます。

一部、公式のファイル内容に変更を加えていますので、ご注意ください。

早速始めていきましょう。作業ディレクトリ用意し、cdで移動します。

$ mkdir sample_app
$ cd sample_app

Quickstartに則り、sample_app内に下記5ファイルを作成します。

/sample_app
$ touch Dockerfile docker-compose.yml Gemfile Gemfile.lock entrypoint.sh
ディレクトリ構造
/sample_app
 ├── Dockerfile
 ├── Gemfile
 ├── Gemfile.lock
 ├── entrypoint.sh
 └── docker-compose.yml

各ファイルを下記の通り編集します。5ファイルのうち、docker-compose.yml以外は、Quickstart公式のコピペです。vim等、お好きなテキストエディタをお使いください。

Dockerfile
FROM ruby:2.5
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]
Gemfile
source 'https://rubygems.org'
gem 'rails', '~>5'
Gemfile.lock
(空のまま)
entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
docker-compose.yml
version: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      - "POSTGRES_USER=xxxx" # 追記
      - "POSTGRES_PASSWORD=xxxx" # 追記
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db
    environment:
      - "SELENIUM_DRIVER_URL=http://selenium_chrome:4444/wd/hub" # 追記
  selenium_chrome:
    image: selenium/standalone-chrome-debug # 追記

上記5ファイルのうち、docker-compose.ymlのみ、編集で手を加えています。具体的には、
・postgresにユーザー名、パスワードを指定
・System Specを動作させるためのコンテナイメージ「selenium/standalone-chrome-debug」を追加

ファイルを作成したら、Railsアプリを新規作成します。

$ docker-compose run web rails new . --force --no-deps --database=postgresql
/sample_app
$ ls
Dockerfile              README.md               bin                     db                      lib                     public                  tmp
Gemfile                 Rakefile                config                  docker-compose.yml      log                     storage                 vendor
Gemfile.lock            app                     config.ru               entrypoint.sh           package.json            test

rails newに伴い、Gemfileにデフォルトのgemがインストールされました。

Gemfileに編集を加えます。具体的には、

・"rspec-rails"を追加
・"chromedriver-helper"を削除

Gemfile
# 省略

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails' # 追記
end

group :test do
  # Adds support for Capybara system testing and selenium driver
  gem 'capybara', '>= 2.15'
  gem 'selenium-webdriver'
  # Easy installation and use of chromedriver to run system tests with Chrome
  # gem 'chromedriver-helper' # 削除
end

#省略

Gemfileを更新したため、再ビルドします。

$ docker-compose build

これで、新しいGemfileでbundle installが実行されました。

続いて、configディレクトリ内のdatabase.ymlを編集します。

config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  username: xxxx
  password: xxxx
  pool: 5

development:
  <<: *default
  database: myapp_development


test:
  <<: *default
  database: myapp_test

DBを作成します。

$ docker-compose run web rails db:create
Starting sample_app_db_1 ... done
Created database 'myapp_development'
Created database 'myapp_test'

コンテナを起動します。このタイミングで、selenium/standalone-chrome-debugのプルが実行されます。

$ docker-compose up -d
Pulling selenium_chrome (selenium/standalone-chrome-debug:)...
latest: Pulling from selenium/standalone-chrome-debug
.
.
.

イメージプルが終了しましたら、docker-composeが正常に立ち上がっているか、確認してみましょう。

$ docker-compose ps
            Name                           Command               State           Ports         
-----------------------------------------------------------------------------------------------
sample_app_db_1                docker-entrypoint.sh postgres    Up      5432/tcp              
sample_app_selenium_chrome_1   /opt/bin/entry_point.sh          Up      4444/tcp, 5900/tcp    
sample_app_web_1               entrypoint.sh bash -c rm - ...   Up      0.0.0.0:3000->3000/tcp

web,db,selenium_chromeの3つが立ち上がっていれば、OKです。ついでに、http://localhost:3000 にアクセスし、いつもの「Yay! You’re on Rails!」が表示されているか、確認しましょう。

また、RSpecが正確にインストールされているかも確認してみましょう。

$ docker-compose run web rspec -v
Starting sampleApp_db_1 ... done
RSpec 3.9
  - rspec-core 3.9.1
  - rspec-expectations 3.9.0
  - rspec-mocks 3.9.1
  - rspec-rails 3.9.0
  - rspec-support 3.9.2

各バージョンは状況により異なるかと思いますが、RSpec 3.7以上のバージョンであれば、System Specを動作させることができます。

RSpecの初期設定

無事、必要なgemが揃った状態のDockerコンテナを用意できたので、ここからは中身の諸々の設定をいじっていきます。

まず、Railsにデフォルトで配置されているtestディレクトリは使用しないため、削除してしまいましょう。

$ rm -rf test

RSpecをインストールします。

$ docker-compose run web rails generate rspec:install

これにより、以下のディレクトリ構造に対して、計3ファイルが生成・配置されます。

ディレクトリ構造
/sample_app
  ├── .rspec
  └── spec
       ├── rails_helper.rb
       └── spec_helper.rb

.rpsecファイルを編集します。2行目のコードを追記してください。これにより、テスト結果の表示形式が変更されます。

.rspec
--require spec_helper
--format documentation

デフォルトでは、”失敗したテスト”のみがターミナル上に出力されるのですが、この変更を行うことにより、”成功・失敗両方のテスト”の結果を出力してくれるようになります。必須ではありませんが、設定しておくことをオススメします。

続いて、spec/rails_helper.rbについて、25行目あたりに書かれている下記コードのコメントアウトを外してください。

spec/rails_helper.rb
# 省略

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

# 省略

これにより、後ほど定義するCapybara用の設定ファイルを読み込めるようになります。

specファイルの自動生成機能OFF

デフォルトでは、controllerやviewを作成したタイミングで、対応するspecファイル(=テストコードを書くためのファイル)が自動生成されます。

今回は初学者にとっての混乱を防ぐため、自動生成はOFFにしておきます。config/application.rbに下記コードを追加します。

config/application.rb
equire_relative 'boot'

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module Myapp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.2

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.

    ### 追記 ######################################
    config.generators do |g|
      g.test_framework :rspec,
                       view_specs: false,
                       helper_specs: false,
                       controller_specs: false,
                       routing_specs: false,
                       request_specs: false
    end
    ##############################################
  end
end

Capybaraの初期設定

Capybaraに設定を加えるため、capybara.rbファイルを作ります。

$ mkdir spec/support
$ touch spec/support/capybara.rb
spec/support/capybara.rb
require 'capybara/rspec'

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium, using: :headless_chrome, options: {
      browser: :remote,
      url: ENV.fetch("SELENIUM_DRIVER_URL"),
      desired_capabilities: :chrome
    }
    Capybara.server_host = 'web'
    Capybara.app_host='http://web'
  end
end

これで、SystemSpecが動作する環境が整いました。

System Spec実践

それでは実際に、静的ページを用いてSystemSpecを使用してみましょう。

今回は、

・rootページにアクセスし、"Hello World!"が表示されていることを検証
・rootページに、"Helpページ"へのリンクが置かれていることを検証
・リンク先のHelpページに、"This is the help page."が表示されていることを検証

という流れのテストを書いてみたいと思います。

下準備として、home,helpを持ったStaticPagesコントローラーを作成します。

$ docker-compose run web rails generate controller StaticPages home help

ルートとビューをいじります。

config/routes.rb
Rails.application.routes.draw do
  root 'static_pages#home'
   get '/help', to: 'static_pages#help'
end
app/views/static_pages/home.html.erb
<h1>StaticPages#home</h1>
<p><%= link_to "Help", help_path %></p>
app/views/static_pages/help.html.erb
<h1>This is the help page.</h1>

下準備が終わったため、specファイルを書いていきます。specディレクトリ下にsystemディレクトリを作成し、その中に"homes_spec.rb"を配置します。

$ mkdir spec/system
$ touch spec/system/homes_spec.rb

spec/system/homes_spec.rbを編集します。

spec/system/homes_spec.rb
require 'rails_helper'

RSpec.describe 'Home', type: :system do
  it 'shows greeting' do
    # root_pathへアクセス
    visit root_path
    # ページ内に'Hello World!'が含まれているかを検証
    expect(page).to have_content 'Hello World!'
    # 'Help'文字列をクリック
    click_on 'Help'
    # ページ内に'This is the help page.'が含まれているかを検証
    expect(page).to have_content 'This is the help page.'
  end
end

いよいよ動かします!今回用意した環境でSystem Specを動かすためには、webコンテナ内に入る必要があります。

$ docker-compose exec web bash

rspecは、"rails spec"コマンドで実行できます。

webコンテナ内
$ rails spec
/usr/local/bin/ruby -I/usr/local/bundle/gems/rspec-core-3.9.1/lib:/usr/local/bundle/gems/rspec-support-3.9.2/lib /usr/local/bundle/gems/rspec-core-3.9.1/exe/rspec --pattern spec/\*\*\{,/\*/\*\*\}/\*_spec.rb

Home
Capybara starting Puma...
* Version 3.12.4 , codename: Llamas in Pajamas
* Min threads: 0, max threads: 4
* Listening on tcp://web:45873
  shows greeting

Finished in 2.33 seconds (files took 8.35 seconds to load)
1 example, 0 failures

無事、テストが通ればOKです!!

スクリーンショット保存機能

ちなみにですが、System Specには、「失敗時の画面をスクリーンショットで自動保存する」という便利機能があります。

ビューをいじって、わざとテストを失敗させてみましょう。

app/views/static_pages/home.html.erb
<h1>StaticPages#home</h1>
<p><%= link_to "Hel", help_path %></p>  # "Help"をタイポしてしまったケース
webコンテナ内
$ rails spec

.
.
.

Home
Capybara starting Puma...
* Version 3.12.4 , codename: Llamas in Pajamas
* Min threads: 0, max threads: 4
* Listening on tcp://web:44173
  shows greeting (FAILED - 1)

Failures:

  1) Home shows greeting
     Failure/Error: click_on 'Help'

     Capybara::ElementNotFound:
       Unable to find link or button "Help"

     [Screenshot]: tmp/screenshots/failures_r_spec_example_groups_home_shows_greeting_612.png
.
.
.

「'Help'が見つかりません!」とメッセージが出され、tmp/screenshotsにエラーが発生したタイミングでのブラウザ画面が保存されています。

failures_r_spec_example_groups_home_shows_greeting_612.png

"Help"と書いたつもりのところが"Hel"になってしまっているのが、確認できます。

最後に

以上です!不明な点、間違っている点などがありましたら、私のTwitter(@ddpmntcpbr)のDMまで連絡頂けると幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

prefixを正確に調べた

prefixの認識レベル

ミニブログを作っている最中、ふとprefixについて考えた時に
「パスを代入する変数」というレベルでしか認識していなかったので
詳しく理解するために調べてみた。

【詳しく調べたかったポイント】

①prefixを使用するメリット(必要性。パス記入の方が分かりやすくない?)
②普通にパスを記入する事との使い分け

調べてわかった事

まず第一に、prefixはRailsが推奨している記述方法らしい。

   tweet_path

でパスの指定完成。

①prefixを使用するメリット(必要性。パス記入の方が分かりやすくない?)について

→結論あまりないらしい。
 1,Railsが推奨している
 2,シンプルで直感的に分かりやすくなる(?)

というのが使用している理由らしい。

②普通にパスを記入する事との使い分け

→開発現場による。「こんな時はprefix!!」みたいなのはないっぽい。

結論

僕は普通にパスを書く方が好き。

「私はprefix派!」「prefixにはこんなメリットがあるぞ!」
というご意見があれば教えて下さい...!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Sass::SyntaxError in Devise::Registrations#new 解決方法 

前提条件

・gemfileに使いたいパッケージを記述している

Gemfile
 gem 'devise'

・ターミナルでbundle installが済んでいる

エラー内容

Screenshot from Gyazo

どうやら@import "modules/user"でうまく'_user.scss'が読み込めてないみたいです

_user.scssを動かしてみます

Screenshot from Gyazo

ここから

Screenshot from Gyazo

modulesディレクトリの中へ入れてみました

Screenshot from Gyazo

無事に動くようになりました

ここで考えたこと

@import "modules/user"と書いてあったけど、
これをよく読むと、
modulesディレクトリの中にある、_user.scssを読み込んでね(@import
ってことだから、

試しに
@import "user"に変えて
_user.scssのファイルの位置を元に戻したら、
ちゃんと動きましたとさ

めでたしめでたし

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsからデータベースを参照(集計)するときの書き方

前置き?

ビューを作成する前に、railsコントローラにおいて、データーベースの値を取ってくる場合にどのように記述するのか。今回はgroup by してからのカウントした値を取得する場合のRailsの書き方を調べるのに随分苦労したので、残しておく。

前提条件

JavaScriptで作成したタイピングゲームをRuby on Railsにのせようとしている。
集計が必要か?ってなったのは、タイピングした結果をランキング表示させようと思い立った時に、問題別のランキング表示をする前に、問題別にどれだけチャレンジャーがいたのか、っていうのを先に一覧表示した後で、ランキング表示をしようか、と思ったから。

テーブル

Qfiles
id
title
category
results_count
その他
Results
id
user_id
qfile_id
その他

出そうとした値

  1. 「Qfile辺りのResultを残したuser_id数」
  2. 「Qfile辺りのResultを残した数」

まぁ、単純だしすぐに終わると思いつつ、先にクエリを作成。

Select
    results.`qfile_id`,
    qfiles.`title`,
    count(distinct results.`user_id`),
    count(*)
from
    results
    inner join
        qfiles
    on  results.`qfile_id` = qfiles.`id`
group by
    results.`qfile_id`,
    qfiles.`title`
;

パンケーキ(Sequel Pro)で実行させて望み通りの値が表示されてることを確認。さあRailsに直すぞ、と意気込む。

調べる

ポイントは3点。
1. テーブルの結合はどう書くんだ?
2. group by はどう書くんだ?
3. countはどう書くんだ?(ついでにdistinctも)

とにかくググって調べたコードをrails cで片っ端から実行。うまく実行できず、よく分からない時は「こう書けば動くんじゃね?」という勘に頼ったテストもした!笑

とりあえずページ保存

Rails controllerに書く記述をRails cでテスト

  • テーブル結合はjoinsで行う。この時selectはいらないが、書かなかった場合、主にしたモデルのカラム(Qfiles.*)しか参照できない。
Qfile.joins(:results).select("results.*, qfiles.*").first.id

ここでもう一つ、先頭に記述するモデルは、親子関係の親の方のモデルでないといけない。これは先頭に記述するモデルを実際に変えてみて、サーバーログに表示されるクエリを見てたらわかる。

  • 単純カウントは、最後に.count入れるだけ
  • distinctカウントは、selectした後に.countを入れて、さらにcountオプション内でdistinctを記述する
Qfile.joins(:results).group("results.qfile_id").select("results.qfile_id").count("distinct results.user_id")

どうやって一文で実行するのか?

色々調べてテストして分かったのは、Countを2ついっぺんには無理じゃね!?ってこと。ひょっとしたら何か方法があるのかもしれないが、もうギブアップした。

カウントの片一方だけなら合わせることができた。

Qfile.joins(:results).group("results.qfile_id").group("qfiles.category").select("results.qfile_id").order("qfiles.id ASC").count("distinct results.user_id")

もう一個のカウントも一緒にやりたいんだよ・・・。

もう面倒い。生クエリ実行できるんじゃね?

find_by_sql で Rails から生 SQL クエリを直接実行する

ここの通りに実行できた。でもあれ?rails cで実行したらカウント要素が入ってなくね?どうゆうこと???

Railsのfind_by_sqlで取得できるモデルからは、select句で指定した名前で値が取れる

取得してるのに表示されないんか〜〜〜い!

これが分かればなんとか値を参照できるから、あとはビューに渡すだけ!!!

でもやっぱり生クエリは邪道な気がする

一生懸命探しました。ふとしたきっかけで新しいキーワードを用いてググったところ、ありました。

Railsで関連レコード数の集計(カウンターキャッシュ)

ふむ。「Qfile辺りのResultを残した数」はこれで保存しときゃいいじゃん。
gem 'counter_culture'を採用。これで作成したカラムが、Qfiles.results_count

ここまでで作成した実行文

Qfile.joins(:results).group("results.qfile_id").group("qfiles.category").group("results_count").select("results.qfile_id").order("qfiles.id ASC").count("distinct results.user_id")

まぁ、なんとか値が出たから使えるんだけども。なんか参照しづらいハッシュ値で取得してしまう。group by の副作用っぽい。

=> {[1, "英語-単語", 4]=>1, [2, "英語-単語", 1]=>1, [4, "英語-単語", 1]=>1, [7, "英語-文章", 4]=>1}

group by 使わなかった時みたいに、@モデル.カラムで参照させて欲しい!!

Rails ActiveRecordでgroup_by countによる集計結果をrelationとして取得する

なるほど、selectにまとめて入力できるのか!!!最終的に以下になった。

Qfile.joins(:results).select("qfiles.id, qfiles.title, qfiles.results_count, COUNT(distinct results.user_id) AS count_distinct_results_user_id").group("results.qfile_id, qfiles.category, results_count").order("qfiles.id ASC")
=> [#<Qfile:0x00007f88586419b8 id: 1, title: "abide - certification 200語", results_count: 4>,
 #<Qfile:0x00007f8858641878 id: 2, title: "certify - drill 200語", results_count: 1>,
 #<Qfile:0x00007f8858641738 id: 4, title: "induction - painter 200語", results_count: 1>,
 #<Qfile:0x00007f88586415d0 id: 7, title: "英語例文 200件 1", results_count: 4>]

上記にdistinct countが含まれてないが、select文中に指定した名前で参照できる。
Qfile.joins・・・・.count_distinct_results_user_id

selectの記述が長くなってしまうのでなんかイヤな書き方ではあるけども、参照時にインデックス番号で指定しなければなくなるよりはマシか?と思った。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者】長めのモデル名(スネークケース)からfindとかをする

コンソールでモデルから値を取得しようとしたとき

知らずにスネークケースでモデルを指定してエラーになっていたので調べました。
おそらく初心者や始めたばかりの頃はUserモデルとかPostモデルなどしか扱わないので悩まないと思います。。
そもそもスネークケース、キャメルケースとは?という方へ。

UserとかPostとかの短いモデル名のとき

User.find(1)

簡単に取得できますね。

長めのモデル名のとき

キャメルケースとスネークケースとは

CustomerOrder

キャメルケース(大文字部分がラクダの背中っぽい)
CとOが大文字

customer_order

スネークケース(ヘビっぽい)
単語と単語のつなぎ目に_が使われている

モデルから値を取得

CustomerOrder.find(1)

で取れます。(キャメルケース)

Customer_order.find(1)

スネークケースでは不可。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

環境変数設定メモ

完全に個人的めも

基本的にrails アプリケーションにおいて環境変数を設定する際はターミナル上で.bash_profileを作成して管理をして行くのが一般的である。

vim ~/.bash_profile コマンドで展開

i でinsertモード
ESCキー 
:wq 保存
:q! 保存せずに閉じる

環境変数の記述は

export KEY = '*******************'
export SECRET_KEY ='***********************'

の形で定義

ここに記載した、環境変数を

ENV['KEY']
ENV['SECRET_KEY']
の形で使用する。

source ~/.bash_profile コマンドにて.bash_fileを読み込み

もう一つの環境編すの構成方法
Dotenvという.envファイルに記述する方法。

rails ではGEMをインストール
gem 'dotenv-rails'

gemfileなどが存在するrootディレクトリに.envファイルを作成
作成した.envファイルがgithubにアップされないように.gitgnoreを編集する。

/.env
を追加

これで環境変数をgithubにアップすることなく使用できる。
環境変数の使用方法は同じ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Sorceryでログイン機能を実装する

Sourceryでログイン機能を実装してみました。
Deviseよりカスタマイズしやすそうで、自分で実装するより手軽なので結構好印象です。
フレンドリーフォワーディングが簡単に実装できるのが良いですね。

セットアップ

Gemfile
gem 'sorcery'
terminal
$ bundle install
$ rails g sorcery:install

Userモデルとマイグレーションファイル、設定ファイルなどもろもろ生成されます。

terminal
$ rails db:migrate

モデルにバリデーションを設定

model/user.rb
class User < ApplicationRecord
  authenticates_with_sorcery!
  validates :email, uniqueness: true, presence: true
  validates :password, presence: true
end

application_controller.rbにbefore_actionとメソッドを定義

application_controller.rb
class ApplicationController < ActionController::Base
  before_action :require_login #sorceryが作成するメソッド。ログインしてない時not_authenticatedメソッドを発火する

  protected

  def not_authenticated
    redirect_to login_url
  end
end

新規ユーザー登録

users_controller.rb
class UsersController < ApplicationController
  skip_before_action :require_login, only: [:new, :create]

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to root_url
    else
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:email, :password)
  end
end
new.html.erb
<%= form_with model: @user, local: true do |f| %>
  <%= f.label :email, 'メールアドレス' %>
  <%= f.text_field :email  %>

  <%= f.label :password, 'パスワード' %>
  <%= f.password_field :password  %>  

  <%= f.label :password_confirmation, 'パスワード確認' %>
  <%= f.password_field :password_confirmation  %>     

  <%= f.submit '登録' %>  
<% end %>
route.rb
resources :users, only: %i[index create]

ログイン

login:見つかったユーザーでログインする
logout:ログアウトする
redirect_back_or_to:保存されたURLがある場合そのURLに、ない場合は指定されたURLにリダイレクトする。(フレンドリーフォワーディング)

sessions_controller.rb
class UsersController < ApplicationController
  skip_before_action :require_login, only: [:new, :create]

  def index
    @user = User.new
  end

  def create
    if @user = login(params[:email], params[:password])
      redirect_back_or_to(hoge_url, notice: 'ログインしました')
    else
      flash[:alert] = 'ログイン失敗'
      render :new
    end
  end

  def destory
    logout
    redirect_to(root_url, notice: 'ログアウトしました')
  end
end
new.html.erb
<%= form_with url: sessions_path, local: true do |f| %>
  <%= f.label :email, 'メールアドレス' %>
  <%= f.text_field :email  %>

  <%= f.label :password, 'パスワード' %>
  <%= f.password_field :password  %>  

  <%= f.submit "ログイン" %>        
<% end %>
routes.rb
resources :contacts, only: %i[index create destroy]

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails, jQ]インクリメンタルサーチ

インクリメンタルサーチ

本記事では前回作成したユーザーの名前検索機能を使用して実装していきます。
以下を使用しています。

  • ruby 2.5.1
  • rails 5.2.4.1
  • gem 'jquery-rails'
  • gem 'devise'

なお、使用するビューは以下を使用します。

index.html.erb
<%= form_with(url: users_searches_path, local: true, method: :get, class: "search_form") do |f| %>
  <%= f.text_field :keyword, placeholder: "Name", class: "search_input" %>
  <%= f.submit "Search", class: "search_btn" %>

<div class="contents">
  <% @users.each do |user| %>
    <div class="user_content">
      <p class="user_name">
        user.name
      </p>
    </div>
  <% end %>
</div>

準備

以下が記入されていなければ記入します。
app/assets/javascripts/application.js
//= require jquery

フォーマット毎に処理を分ける

フォーマット毎に処理を分けるためコントローラーのindexアクションを編集します。

controllers/users/searches_controller.rb
  def index
    @users = User.search(params[:keyword])
    respond_to do |format|
      format.html
      format.json
    end
  end

respond_to

アクションの中でHTMLとJSONなどのフォーマット毎にhtmlかjsonかを条件分岐することができます。

jbuilderファイルの作成、編集

index.json.jbuilderを新規作成し内容を編集します。

app/views/tweets/searches/index.json.jbuilder
json.array! @users do |user|
  json.id user.id
  json.name name.name
end

jbuilderという拡張子を持つテンプレートでは、JSONという名前のJbuilderオブジェクトが自動的に利用できるようになります。
arrayメソッドはその内の一つでJavaScript側に配列で値を送ることができます。

search.jsの作成、編集

検索フォームの値を取得

app/assets/javascripts/search.js
$(function() {
  $(".search_input").on("keyup", function() {
    var input = $(".search_input").val();
  });
});

keyupイベントを使用して文字が入力される度に発火するようにします。

JSON形式で値を返す

app/assets/javascripts/search.js
$(function() {
  $(".search_input").on("keyup", function() {
    var input = $(".search_input").val();
//---以下を追記---
    $.ajax({
      type: 'GET',
      url: '/users/searches',
      data: { keyword: input },
      dataType: 'json'
    })
//---以上を追記---
  });
});

Ajax通信を実現するためには、上記のように$.ajaxメソッドを使用します。
また。上記のコードは
HTTPメソッドはGETで、/users/searchのURLに{ keyword: input }を送信。サーバーから値を返す際は、JSON。
という意味を持ちます。JSON形式の場合は、app/views/users/searches/index.json.jbuilderが読まれ,該当する投稿情報はjbuilderによってJSONに変換されてJavaScriptのファイルに返されます。

レスポンス結果によって処理を分ける

app/assets/javascripts/search.js
$(function() {
  $(".search_input").on("keyup", function() {
    var input = $(".search_input").val();
    $.ajax({
      type: 'GET',
      url: '/users/searches',
      data: { keyword: input },
      dataType: 'json'
    })
//---以下を追記---
    .done(function(users) {
      $(".contents").empty();
      if (users.length !== 0) {
        users.forEach(function(user){
          appendUser(user);
        });
      } else {
        appendErrMsgToHTML("一致するユーザーはいません");
      }
    })
    .fail(function() {
      alert('error');
    });
//---以上を追記---
  });
});

レスポンスが成功した場合は、ユーザーが表示される親要素の中身を都度空っぽにします。そしてusersが空ではない場合usersの中身の数だけappendUser関数を呼び出します。
該当ユーザーがいない場合は”一致するツイートがありません”という引数を与え、appendErrMsgToHTML関数を呼び出します。
また、レスポンスに失敗した場合はアラートを表示させます。

empty()メソッド

指定したDOM要素の子要素のみを削除するメソッドです。
指定したDOM要素自体を削除するremoveメソッドとは異なります。

forEachメソッド

forEachは、与えられた関数を配列に含まれる各要素に対して一度ずつ呼び出します。

検索に該当ユーザーいた場合、いない場合の関数を定義

app/assets/javascripts/search.js
$(function() {
//---以下を追記---
  var search_list = $(".contents");

  function appendUser(user) {
    var html = `
               <div class="user_content">
                 <p class="user_name">
                   #{user.name}
                 </p>
               </div>
               `
    search_list.append(html);
   }

  function appendErrMsgToHTML(msg) {
    var html = `
          <div class="user_content">
                  <p class="user_name">
                    ${ msg }
                  </p>
                 </div>
         `
    search_list.append(html);
  }
//---以上を追記---
  $(".search_input").on("keyup", function() {
    var input = $(".search_input").val();
    $.ajax({
      type: 'GET',
      url: '/users/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(users) {
      search_list.empty();
      if (users.length !== 0) {
        users.forEach(function(user){
          appendUser(user);
        });
      } else {
        appendErrMsgToHTML("一致するユーザーはいません");
      }
    })
    .fail(function() {
      alert('error');
    });
  });
});

検索に該当ユーザーがいた場合
変数htmlにユーザー情報を表示する要素を代入し、appendメソッドで親要素の一番下に追加します。
検索に該当ユーザがいない場合
変数htmlに"一致するユーザーはいません"を表示する要素を代入し、appendメソッドで親要素の一番下に追加します。

おわり

これでインクルメンタルサーチが実装できました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gonを使ってJavaScriptへの直書き環境変数を防ぐ

まず環境変数を設定する

Dotenvを用いて環境変数を設定していきます。

1 gemのインストール

Gemfile
gem 'dotenv-rails'
bundle install

2 envファイルの作成
appファイルやcofigファイル、Gemfileがあるルートディレクトリに「.env」というファイルを作成して下さい。

スクリーンショット 2020-03-10 15.36.12.png

私は一瞬迷ったので画像載せておきます。
歯車マークが目印です。

3 環境変数の設定

|.env|
ACCESS_KEY='*******'

4 .gitignoreの編集
環境変数をGitのトラッキングの対象外にする為記述します。

|.gitignore|
.env

これでgonを使うための下準備は完了です。

gonのインストール

Gemfileに追記して下さい。

Gemfile
gem 'gon'
bundle install

JSファイルでRails環境変数を扱う

1 コントローラーの編集
JavaScriptを呼び出しているViewに対応させてコントローラーのメソッド内に以下を記述します。
例えば、new.html.hamlでjsを呼び出しているのであればnewメソッド内に追記します。

gons_controller.rb
def new
  gon.xxx_access_key = ENV['ACCESS_KEY']
end

2 renderメソッド
application.html.haml内のheadタグに以下を記述して下さい。

application.html.haml
= Gon::Base.render_data

3 JSファイルに記述
jsファイル内にコントローラーで設定した変数を記述します。

gon.js
gon.xxx_access_key

これでjavascriptとrailsの連携が出来ました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】ransackで検索機能を実装

ransackというgemを使って、検索、ソートの機能を実装したのでまとめます。
手軽にできて良い感じでした。

基本的な使い方

Gemfile
gem 'ransack'
terminal
$ bundle install
users_controller.rb
def index
  @q = User.ransack(params[:q])
  @users = @q.result(distinct: true)
end
index.html.erb
<!-- 検索フォーム -->
<%= search_form_for @q do |f| %>
  <%= f.label :name, "名前" %>
  <%= f.search_field :name_cont %>
  <%= f.label :email, "メールアドレス" %>
  <%= f.search_field :email_cont %>
  <%= f.submit "検索" %>
<% end %>

<!-- ユーザー一覧 -->
<table>
  <tr>
    <th>名前</th>
    <th>メールアドレス</th>
  </tr>
  <% users.each do |user| %>
  <tr>
    <td><%= user.name %></td>
    <td><%= user.email %></td>
  </tr>
  <% end %>
</table>

ソート機能

index.html.erb
<!-- ユーザー一覧 -->
<table>
  <tr>
    <th><%= sort_link(@q, :name, "名前") %></th>
    <th><%= sort_link(@q, :email, "メールアドレス") %></th>
  </tr>
.
.

検索条件を組み合わせる

ステータスがアクティブなユーザーの中から検索して、ページネーションをつける

users_controller.rb
def index
  @q = User.ransack(params[:q])
  #後ろに自由に条件を追加できる
  @users = @q.result(distinct: true).where(status: 1).page(params[:page])
end

様々な検索方法

検索フォームの中で:name_contなどと記載していた部分は、他にもいろんな指定ができます。

検索方法 意味(英語) 意味
*_eq equal 等しい
*_not_eq not equal 等しくない
*_lt less than より小さい
*_lteq less than or equal より小さい(等しいものも含む)
*_gt grater than より大きい
*_gteq grater than or equal より大きい(等しいものも含む)
*_cont contains value 部分一致(内容を含む)

その他:activerecord-hackery/ransack: Object-based searching.

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】新規投稿機能、編集機能、削除機能の実装

新規投稿機能、編集機能、削除機能の実装

本記事でのモデルケース:
・Photoモデル
・photosコントローラー
・カラム名:image,title,memo
なので、記事をご覧の皆さんのモデル名やコラム名などに応じて書き換えていってもらえれば実装できると思います!

新規投稿機能の実装

ルーティングの設定

ここではnewアクションとcreateアクションを定義する

routes.rb
Rails.application.routes.draw do
  # ここから
  resources :photos, only: %i(new create)
 # ここを追加
end

この記述によって
スクリーンショット 2020-03-10 13.29.28.png
以上のルーティングが生成される

コントローラーの設定

photos_controller.rb
class PhotosController < ApplicationController
    # ここから
    def new
        @photo = Photo.new
    end

    def create
        @photo = Photo.new(photo_params)
        if @photo.save
          redirect_to action: :index
        else
          redirect_to action: :new
        end
    end

    private
      def photo_params
        params.require(:photo).permit(:image,:title,:memo)
      end
    # ここまでを追加
end

投稿ページの設計

まず、viewsファイルの中でedit.html.erbを作成してください!

new.html.erb
<%= form_for(@photo, :url => { controller:'photos', action:'create'})do |f| %>
 <div class="field">
   <%= f.label :image %>
   <br>
   <%= f.file_field :image %>
   <br>
   <%= f.label :title %>
      <br>
   <%= f.text_field :title %>
   <br>
   <%= f.label :memo %>
   <br>
   <%= f.text_field :memo %>
 </div>
 <div class="action">
   <%= f.submit %>
 </div>
<% end %>

投稿一覧ページの実装

まず、viewの中でindex.html.erbファイルを作成ください!

photos_controller.rb
class PhotosController < ApplicationController
    #ここから
    def index
        @photos = Photo.all
    end
    #ここまでを追加

    def new
        @photo = Photo.new
    end

    def create
        @photo = Photo.new(photo_params)
        if @photo.save
          redirect_to action: :index
        else
          redirect_to action: :new
        end
    end

    private
      def photo_params
        params.require(:photo).permit(:image,:title,:memo)
      end
end
index.html.erb
<% @photos.each do |photo| %>
 <div>
   <%= image_tag photo.image_url %>
   <p><%= photo.title %></p>
   <p><%= photo.memo %></p>
 </div>
<% end %>

投稿編集機能の実装

ルーティングの実装

routes.rb
Rails.application.routes.draw do
  # edit,updateをroutes.rbに追加
  resources :photos, only: %i(new create edit update)
end

コントローラーの設定

photos_controller.rb
class PhotosController < ApplicationController
    def new
        @photo = Photo.new
    end

    def create
        @photo = Photo.new(photo_params)
    if @photo.save
          redirect_to action: :index
        else
          redirect_to action: :new
        end
    end

    def edit
      @photo = Photo.find_by(id: params[:id])
    end

    def update
      @photo = Photo.find_by(id: params[:id])
      if @photo.update_attributes(photo_params)
        redirect_to "/"
      else
        render action: :edit
      end
    end

    private
      def photo_params
        params.require(:photo).permit(:image,:title,:memo)
      end
end

編集ページの設計

edit.html.erb
<%= form_with model: @photo, url: { action: :update }, html: { class: 'listForm' }, local: true do |f| %>
  <%= f.label :title %>
  <%= f.text_field :title, class: "form-control listName", placeholder: "タイトル" %>
  <%= f.label :memo %>
  <%= f.text_field :memo, class: "form-control listName", placeholder: "詳細" %>
  <div class="text-center"><%= f.submit "編集する", class: "submitBtn" %></div>
<% end %>

一覧ページから投稿ページへの遷移のリンク実装

index.html.erb
<% @photos.each do |photo| %>
 <div>
   <%= image_tag photo.image_url %>
   <p><%= photo.title %></p>
   <p><%= photo.memo %></p>
   <%= link_to "編集", edit_photo_path(photo) %> # この1行を追加
 </div>
<% end %>

投稿削除機能の実装

ルーティングの設定

routes.rb
Rails.application.routes.draw do
  # destroyを追加
  resources :photos, only: %i(new create edit update destroy)
end

コントローラーの設定

photos_controller.rb
class PhotosController < ApplicationController
    def new
        @photo = Photo.new
    end

    def create
        @photo = Photo.new(photo_params)
    if @photo.save
          redirect_to action: :index
        else
          redirect_to action: :new
        end
    end

    def edit
      @photo = Photo.find_by(id: params[:id])
    end

    def update
      @photo = Photo.find_by(id: params[:id])
      if @photo.update_attributes(photo_params)
        redirect_to "/"
      else
        render action: :edit
      end
    end

    # ここから
    def destroy
      @photo = Photo.find_by(id: params[:id])
      @photo.destroy
      redirect_to :root
    end
    # ここまで追加する

    private
      def photo_params
        params.require(:photo).permit(:image,:title,:memo)
      end
end
index.html.erb
<% @photos.each do |photo| %>
 <div>
   <%= image_tag photo.image_url %>
   <p><%= photo.title %></p>
   <p><%= photo.memo %></p>
   <%= link_to "編集", edit_photo_path(photo) %>
   <%= link_to ”削除”, photo, method: :delete, data: { confirm: "#{photo.title}を削除して大丈夫ですか?" } %>
   # 上の1行を追加
 </div>
<% end %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】外部キー制約とreference型について

外部キー制約とreference型

Railsで外部キーのカラムを追加するときにreference型を使用することがあります。
その際のreference型を使用した書き方について記録します。

※外部キー制約:他のテーブルのデータに参照(依存)するようにカラムにつける制約

reference型を使用しない場合

reference型なしで外部キー制約のついたカラムを作成します。

制約がないカラムの作成

2020_create_tweets.rb
class CreateTweets < ActiveRecord::Migration[5.0]
  def change
    create_table :tweets do |t|
      t.string   :text
      t.integer  :user_id
    end
  end
end

上記はインデックスが貼られない、外部キー制約もつかないカラムを作成しているコードになります。

インデックスを貼るカラムの作成

2020_create_tweets.rb
class CreateTweets < ActiveRecord::Migration[5.0]
  def change
    create_table :tweets do |t|
      t.string    :text
      t.integer   :user_id, index: true #indexオプション
    end
  end
end
2020_create_tweets.rb
class CreateTweets < ActiveRecord::Migration[5.0]
  def change
    create_table :tweets do |t|
      t.string    :text
      t.integer   :user_id
    end
    add_index :tweets, :user_id
    # add_index :対象のテーブル名, インデックス対象のカラム名
  end
end

インデックスを張る場合は上記の2つの記載方法がある。

外部キー制約がついているカラムの作成

2020_create_tweets.rb
class CreateTweets < ActiveRecord::Migration[5.0]
  def change
    create_table :tweets do |t|
      t.string   :text
      t.integer  :user_id
    end
    add_foreign_key :tweets, :users
    # add_foreign_key :対象のテーブル名, :指定先のテーブル 
  end
end

reference型を使用しない場合はforeign_key: trueでは外部キー制約にならないので注意が必要です。
また、外部キー制約をつける場合は、インデックスは自動で付与されるので、先ほどのindex: trueは不要になります。

reference型を使用する場合

■reference型を使用するメリット
userではなくuser_idというカラム名で作成してくれる。
・インデックスを自動で張ってくれる。

2020_create_tweets.rb
class CreateTweets < ActiveRecord::Migration[5.0]
  def change
    create_table :tweets do |t|
      t.string    :text
      t.references :user
    end
  end
end

しかし、t.reference :userだけでは外部キー制約はつきません。
下記のようにreference型は外部キー制約をつけるときは、foreign_key: trueを記載して使えるようにします。

2020_create_tweets.rb
class CreateTweets < ActiveRecord::Migration[5.0]
  def change
    create_table :tweets do |t|
      t.string     :text
      t.references :user, foreign_key: true
    end
  end
end

下記のようにadd_foreign_keyでも可能。

2020_create_tweets.rb
class CreateTweets < ActiveRecord::Migration[5.0]
  def change
    create_table :tweets do |t|
      t.string     :text
      t.references :user
    end
    add_foreign_key :tweets, :users
  end
end

参照URL

https://qiita.com/ryouzi/items/2682e7e8a86fd2b1ae47

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Cancancanを使っているときのtest

Cancancanとは?

この記事を見て下さっているということは既にご存知だと思いますが、 権限管理のgem です。

Cancancanを使っているときのテストはどうしたらいいのか?

権限があるときは、権限を持つテストユーザーでログインして

test "test#indexにmanager権限を持つユーザーがアクセスして、正常レスポンスが返されること" do
  # manager権限を持つユーザーでログイン
  login(:yama_p)

  get tests_path
  assert_response :success
end

正常に処理ができることを確認すればまぁいいかも知れませんが、 アクセスできない(権限がない)ことを確認する にはどうしたらよいでしょうか?
というのも、上記で assert_response :errors などとしてもcancancanで弾かれたときにエラーが返ってくるわけではなくそもそもアクセスできないので

CanCan::AccessDenied: You are not authorized to access this page.

とテストが落ちてしまいます。

権限を持つかどうかをテストする

Cancancanのwikiにテストについての記述があります。

test "test#indexにmember権限を持つユーザーはアクセス権限を持たないこと" do
  # member権限を持つユーザー
  user = users('fan_bingbing')
  ability = Ability.new(user)
  assert ability.cannot?(:manage, Test.new)
end

上記のような感じで、ユーザーが権限を持たないことをテストすればOK!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

deviseとActive Storage を使ってavatar(アイコン)登録する

Active Storageの場合 (Rails 5.2以降の場合)

アクティブストレージは保存場所を信頼性の高いamazon, google, microsoftが提供するクラウドに保存できる。またバックアップもできるので、データが消えた場合などに非常に重宝されます。

設定方法も非常に簡単です。

概要
Active StorageによるRails ファイルアップロード
Active Strageを使用してユーザーのアバターを登録、表示する
参考になるページ
【Rails 5.2】 Active Storageの使い方

ターミナル
$ rails active_storage:install
$ bundle exec rake db:migrate

rails active_storage:installコマンドを実行すると、active_storage_blobs と active_storage_attachmentsテーブルが生成されるのでこちらをモデルに反映させるためrails db:migrateする

作成されたテーブルは下記の役割となります。

テーブル 機能
active_storage_blobs アバター画像の保存先
active_storage_attachments userとavatorの中間テーブル

ローカル環境、テスト環境、本番環境で保存先を変更する。
storage.ymlで設定を変えていく

config/storage.yml
test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
#   service: S3
#   access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
#   secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
#   region: us-east-1
#   bucket: your_own_bucket

# Remember not to checkin your GCS keyfile to a repository
# google:
#   service: GCS
#   project: your_project
#   credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
#   bucket: your_own_bucket

# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
# microsoft:
#   service: AzureStorage
#   storage_account_name: your_account_name
#   storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
#   container: your_container_name

# mirror:
#   service: Mirror
#   primary: local
#   mirrors: [ amazon, google, microsoft ]

すでにamazonの記述はあります。なので,amazonのs3で保存する場合はコメントアウトを解除して下記のようにします。

config/storage.yml
test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, "YOUR ACCESS_KEY_ID") %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, "YOUR SECRET_ACCESS_KEY") %>
  region: ap-northeast-1 # 東京の場合(海外の場合異なります)
  bucket: バケット名

config/environments/production.rb
# :localから:amazonに変更することで、先ほど定義したamazon:を読み込みさせる。
config.active_storage.service = :amazon

AWS用のGemを追加します

Gemfile
gem 'aws-sdk-s3', require: false

userモデルとavatorを紐づける

user.rb
class User < ApplicationRecord
  #Include default devise modules. Others available are:
  #:confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_one_attached :avatar # 追加
end

このアクセサに、Active Strageがファイルをアタッチしてくれます。
なお、レコードとファイルが1対1の場合は has_one_attached ですが、1対多の場合は has_many_attached になります。

form
= f.file_field :avatar

これで登録可能です。

編集の場合

今回はgem "devise"を利用してアイコンを登録する場合です。

deviseのparamsを指定するために、application_controllerでaccount_updateの記述を追加します

application_controller.rb
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [ :avatar] )
    devise_parameter_sanitizer.permit(:account_update, keys: [ :avatar])
  end

これでparamsにavatarが読み取れます。
つづいて、active_strageで保存する場合、

親要素.イメージ名.attach(params[:key]

とする必要があります。
例えば,既存のuserにavatarを付与するには

User.avatar.attach(params[:avatar])

とします。
.attachメソッドで追加/更新しなければいけないのがポイントです。
ですから、通常通りupdateするだけでは更新されません。
update処理に.attachを追加します。今回はdeviseということなので、deviseのregistration_controller.rbに処理を追加します。コメントアウトを解除して、下記の処理にします。

registration_controller.rb
  def update
    super
    if account_update_params[:avatar].present?
      resource.avatar.attach(account_update_params[:avatar])    
    end
  end

これでavatarが付与/更新されます。

参考資料

[Rails][devise] devise + active_recordで画像を編集する
バリデーションの設定

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

シンボルとは

シンボルとは

Symbol とは、Ruby が内部でメソッド名などの識別に使っている数値で、任意の文字列に対して異なった値が割り当てらる。

なるほど、よく分かりませんね。

つまり、文字列だけど数値。みたいなものです。ハッシュのキーや文字列自体がデータでは無い物に使うことが吉です。

前コロンと後コロンの違い

シンボルとは主に文字列にコロン記号「:」を前置して定義したものです。

それにより、文字列を””で囲む必要がなくなります。

コロン記号「:」が、文字列記号「””」の代わりに、「これはシンボルだよ」とRubyに知らせています。

上記のように、コロン記号「:」を文字列に前置するとシンボルになります。

たとえば、ハッシュのキーとしてシンボルを使う際や、キーワード引数を使う際に、コロン記号「:」を後置します。

シンボルはオブジェクトの一つ

メソッドなどの名前を識別するためのラベルをオブジェクト化

samurai   /*文字列
:samurai /*シンボル

ハッシュのキーとして利用する

よく使われるのが、ハッシュのキーだと思います。

hash_symbol = { tsuma: "sazae", otto: "masuo", kodomo: "tarao" } 

取り出しは

puts hash_string[:tsuma]  # "sazae" と表示
puts hash_string[:otto]   # "masuo" と表示
puts hash_string[:kodomo] # "tarao" と表示

なり、キーをシンボルで定義しています。

ハッシュはデータを持たない

最初に記述した通り、ハッシュは「文字列だけど数値。」なのでデータを持ちません。
そのため、文字列そのもののデータを必要としない場合にシンボルが使われています。

シンボルのメリット

シンボルを使うことでコードが短くなり可読性が上がったり、処理が早くなったり、メモリ消費が少なくなったりと、いいことばかりらしいので、一緒に勉強しましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsで作ったアプリをAWSでデプロイ① 〜アカウント作成編〜

こんにちは
AWSをつかってRailsアプリをデプロイしたいと思います

まず、AWSアカウントを作成します
スクリーンショット 2020-03-09 14.16.07.png

注意点として一度使ったメールアドレスは、削除すると二度と使えなくなるそうです。

次の画面、僕はパーソナルです。

スクリーンショット 2020-03-09 14.20.47.png

この後、クレジットカード→本人認証画面なので割愛します
プランはベーシックです。

晴れてAWSアカウント作成完了!
早速EC2インスタンスを作りたかったのですが、どうやらアカウント作成直後はできないみたいです。
少し待ちます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DB接続がない状態でassets:precompileを行う

本番とは違う環境だったり、Dockerfile内でassets:precompileを行ったりするときにDB接続でエラーになるときがある。これを回避する方法ってあるのかなと思ったので調べてみた

activerecord-nulldb-adapterを使う

github
https://github.com/nulldb/nulldb

gem 'activerecord-nulldb-adapter'
config/database.yml
default: &default
  adapter: <%= ENV['DB_ADAPTER'] ||= "mysql2" %>

database.ymlに環境変数でDB_ADAPTERを指定する。

$ DB_ADAPTER=nulldb bundle exec rake assets:precompile

上記を実行すればDB接続なしでprecompileできる

参考

Rails × ECS でオートスケーリング&検証環境の自動構築
https://tech.medpeer.co.jp/entry/2018/06/20/080000

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS ECSのビルド〜デプロイをCodePipelineで自動化した

Docker + ECS + RailsのプロジェクトでCodePipelineを使用してデプロイまでを自動化したので、その知見をまとめたい。(ブルーグリーンデプロイではなく、通常のデプロイ時の方法)

RailsのプロジェクトだけれどCodePipeline基本的な使い方は、他の言語でもそれほど変わらないと思う

デプロイの流れ

githubにpushすれば自動でデプロイが開始される。デプロイは以下の流れで行うように作った。

  1. GitHubの特定のブランチ(masterなど)にpushする
  2. pushされたことがCodepipelineに通知されビルドが開始
    1. docker-composeを利用して、Dockerをbuildする
    2. Dockerイメージタグにコミット番号を付与して一意にする
    3. ビルドが完了したらECRにpushする
  3. ビルド完了後にECSにデプロイ通知がいく

CodePipelineの設定

CodePipelineはソース管理、ビルド、デプロイをパーツのようにつなげてCD/CIを管理することができるAWSのサービス。以下のサービスをつなぎ合わせて連携することができる

  • CodeCommit
  • CodeBuild
  • CodeDeploy

スクリーンショット_2020-03-09_14_55_31-2.png

CodeCommit

スクリーンショット_2020-03-09_15_24_30.png

まずはGithubで特定のリポジトリにpushされたときに検知できるようにする。ここではmasterがpushsされたときにビルドされる設定した。

ちなみにCodeCommitはGithub以外にも、ECRやS3などと連携することもできる。CodeCommit自体にコードを管理させることも可能。

CodeBuild

スクリーンショット_2020-03-09_15_29_41.png

CodeBuildではビルドプロジェクトというものを作成する。このビルドプロジェクトはOS環境や、ビルドコマンドを記載するbuildspec.ymlのパスを設定していく。ようはビルドの設定を管理している感じだ。

スクリーンショット 2020-03-09 15.38.01.png

Ubuntuでイメージが最新バージョンのものを使っておけば特に問題はないかと思う。buildspec.ymlはGithubにあげたプロジェクトに入れておく。そのパスをビルドプロジェクトで設定すればOK

buildspec.yml

ビルドするコマンドをyamlに書いていく。ビルドは以下のような流れで行う。

  1. ECRからDockerのイメージを取得
  2. コミットハッシュを取得する(コミットハッシュはDockerイメージタグとして使用する)
  3. .envに環境変数を追加していく
  4. docker-composeを利用してビルドする
  5. dbのmigrateを行う
  6. デプロイを通知する
version: 0.2

phases:
  install:
    runtime-versions:
      docker: 18
  pre_build:
    commands:
      - echo -------- Logging in to Amazon ECR... --------
      - aws --version
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
      - REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/hogehoge
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
  build:
    commands:
      - echo -------- Build started on `date` --------
      - echo -------- Building the Docker image... --------
      - echo AWS_ACCESS_KEY=$AWS_ACCESS_KEY >> .env
      - echo AWS_SECRET_KEY=$AWS_SECRET_KEY >> .env
      - echo ECS_ENV_NAME=$ECS_ENV_NAME >> .env
      - docker-compose -f docker-compose-$ECS_ENV_NAME.yml build
      - docker-compose -f docker-compose-$ECS_ENV_NAME.yml run --name hogehoge-image web sh -c "bundle exec rake db:create && bundle exec rake db:migrate"
      - docker commit hogehoge-image $REPOSITORY_URI:latest
      - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
  post_build:
    commands:
      - echo -------- Build completed on `date` --------
      - echo -------- Pushing the Docker images... --------
      - docker push $REPOSITORY_URI:latest
      - docker push $REPOSITORY_URI:$IMAGE_TAG
      - echo [\{\"name\":\"hogehoge\",\"imageUri\":\"$REPOSITORY_URI:$IMAGE_TAG\"\}] > imagedefinitions.json
artifacts:
  files: imagedefinitions.json

buildspec.ymlはpre_build、build、post_buildという3段階で処理を行う。一個ずつ分解して説明していく。

pre_build

ビルドする前の下準備。 GithubのURLとか、コミットハッシュとかをあとで使うので変数に入れている。ちなみにコミットハッシュはDockerイメージタグとして後で使う。

build
  - echo AWS_ACCESS_KEY=$AWS_ACCESS_KEY >> .env
  - echo AWS_SECRET_KEY=$AWS_SECRET_KEY >> .env
  - echo S3_BUCKET=$S3_BUCKET >> .env

環境変数を.envに書き込むようにしている。僕のRailsプロジェクトでは.envで環境変数を管理しており、CodeBuildでもdocker-composeが使用したいという理由でこの形にしている。このやり方がベストプラクティスではないような気がするので、もっと良い方法を見つけたい。

ちなみに環境変数はSystem Managerで管理している。環境変数についてはのちほどもう少し詳しく記載する。

  - docker-compose -f docker-compose-$ECS_ENV_NAME.yml build
  - docker-compose -f docker-compose-$ECS_ENV_NAME.yml run --name hogehoge-image web sh -c "bundle exec rake db:create && bundle exec rake db:migrate"
  - docker commit hogehoge-image $REPOSITORY_URI:latest
  - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:

あとはdocker-composeでビルドして、migrateして、Dockerイメージをcommitしているだけ。

ちなみに僕はDockerfile内にassets:precompileを行っているため、buildspec.ymlにはコマンドが書いていない。

post_build

ECSにデプロイするためには最終的にimagedefinitions.jsonというファイルを作成する必要がある。

  - docker push $REPOSITORY_URI:latest
  - docker push $REPOSITORY_URI:$IMAGE_TAG
  - echo [\{\"name\":\"hogehoge\",\"imageUri\":\"$REPOSITORY_URI:$IMAGE_TAG\"\}] > imagedefinitions.json
artifacts:
  files: imagedefinitions.json

imagedefinitions.jsonはnameとimageUirを関連付けたjsonを書いていく。複数環境あるときは当たり前だけど複数書いていく。

  • name
    • ECSのコンテナ名
  • imageUri
    • ECRのURL

ECSのタスク定義との関連は以下のようになる。

スクリーンショット_2020-03-09_16_34_33-2.png

補足

環境変数について

環境変数は秘匿化する必要があるためSystem Managerでパラメータを管理するようにした。

68017af5-fa1b-4fb2-8307-aadc174d004a-960x690r.png

安全な文字列を選択してパラメータを設定する。「名前」欄で設定した値をCodeBuildで使用する。

d9424b69-0d44-4230-afa8-c769f4a2e55a-960x573r-2.png

System Managerで設定した値をbuildspec.ymlで使用するために、CodeBuildに環境変数として設定する必要がある。これを設定しておくとbuildspec.ymlの中で$HOGEHOGEという値で使用できるようになる。

  • 名前
    • buildspec.ymlで使用する環境変数名
    • System Managerで設定した名前
  • 入力
    • 『パラメータ』を選択する
- echo AWS_ACCOUNT_ID=$AWS_ACCOUNT_ID >> .env
- echo AWS_ACCESS_KEY=$AWS_ACCESS_KEY >> .env
- echo AWS_SECRET_KEY=$AWS_SECRET_KEY >> .env

これでbuildspec.ymlにファイルの中でCodeBuildで設定した環境変数が利用できるようになる

デプロイしたときにタスク定義のバージョンは更新されていく

0ad3ec1b-5711-4351-a838-654d76ef1f81-960x596r.png

デプロイされるとimagedefinitions.jsonで設定したコンテナ名とイメージのURLでタスク定義のイメージが変更され、リビジョンが新しく更新されていく。タスクをリビジョンで管理するメリットとして「切り戻しが簡単になる」という点がある。

もしも本番で障害が発生したとき場合にもリビジョンを戻すだけで動作する。ただしDBのカラム変更などしているときは、DBをロールバックする必要もあるので注意が必要。

CodePipelineからデプロイを実行する

なんらかの理由でソースコードをpushせずにデプロイしたい場合は、Codepipelineの画面から直接行うことができる

c7177b94-b6e1-408f-84c4-de5ea8b817ad-960x598r.png

終わり

ECSは少人数の開発にこそ向いていると思う。ECSでスケールアップから障害復旧までまかせ、CodePipelineでデプロイを自動化しておけばインフラの運用をそれほど考慮しなくて済むようになる。アプリケーション層に集中して開発ができるようになる。

まだまだECSやCodePipelineに対しての知見が足りないので、また気づきがあったら書いていきたい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails + Docker + ECSのビルド〜デプロイをCodePipelineで自動化した

Docker + ECS + RailsのプロジェクトでCodePipelineを使用してデプロイまでを自動化したので、その知見をまとめたい。(ブルーグリーンデプロイではなく、通常のデプロイ時の方法)

RailsのプロジェクトだけれどCodePipeline基本的な使い方は、他の言語でもそれほど変わらないと思う

デプロイの流れ

githubにpushすれば自動でデプロイが開始される。デプロイは以下の流れで行うように作った。

  1. GitHubの特定のブランチ(masterなど)にpushする
  2. pushされたことがCodepipelineに通知されビルドが開始
    1. docker-composeを利用して、Dockerをbuildする
    2. Dockerイメージタグにコミット番号を付与して一意にする
    3. ビルドが完了したらECRにpushする
  3. ビルド完了後にECSにデプロイ通知がいく

CodePipelineの設定

CodePipelineはソース管理、ビルド、デプロイをパーツのようにつなげてCD/CIを管理することができるAWSのサービス。以下のサービスをつなぎ合わせて連携することができる

  • CodeCommit
  • CodeBuild
  • CodeDeploy

スクリーンショット_2020-03-09_14_55_31-2.png

CodeCommit

スクリーンショット_2020-03-09_15_24_30.png

まずはGithubで特定のリポジトリにpushされたときに検知できるようにする。ここではmasterがpushsされたときにビルドされる設定した。

ちなみにCodeCommitはGithub以外にも、ECRやS3などと連携することもできる。CodeCommit自体にコードを管理させることも可能。

CodeBuild

スクリーンショット_2020-03-09_15_29_41.png

CodeBuildではビルドプロジェクトというものを作成する。このビルドプロジェクトはOS環境や、ビルドコマンドを記載するbuildspec.ymlのパスを設定していく。ようはビルドの設定を管理している感じだ。

スクリーンショット 2020-03-09 15.38.01.png

Ubuntuでイメージが最新バージョンのものを使っておけば特に問題はないかと思う。buildspec.ymlはGithubにあげたプロジェクトに入れておく。そのパスをビルドプロジェクトで設定すればOK

buildspec.yml

ビルドするコマンドをyamlに書いていく。ビルドは以下のような流れで行う。

  1. ECRからDockerのイメージを取得
  2. コミットハッシュを取得する(コミットハッシュはDockerイメージタグとして使用する)
  3. .envに環境変数を追加していく
  4. docker-composeを利用してビルドする
  5. dbのmigrateを行う
  6. デプロイを通知する
version: 0.2

phases:
  install:
    runtime-versions:
      docker: 18
  pre_build:
    commands:
      - echo -------- Logging in to Amazon ECR... --------
      - aws --version
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
      - REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/hogehoge
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
  build:
    commands:
      - echo -------- Build started on `date` --------
      - echo -------- Building the Docker image... --------
      - echo AWS_ACCESS_KEY=$AWS_ACCESS_KEY >> .env
      - echo AWS_SECRET_KEY=$AWS_SECRET_KEY >> .env
      - echo ECS_ENV_NAME=$ECS_ENV_NAME >> .env
      - docker-compose -f docker-compose-$ECS_ENV_NAME.yml build
      - docker-compose -f docker-compose-$ECS_ENV_NAME.yml run --name hogehoge-image web sh -c "bundle exec rake db:create && bundle exec rake db:migrate"
      - docker commit hogehoge-image $REPOSITORY_URI:latest
      - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
  post_build:
    commands:
      - echo -------- Build completed on `date` --------
      - echo -------- Pushing the Docker images... --------
      - docker push $REPOSITORY_URI:latest
      - docker push $REPOSITORY_URI:$IMAGE_TAG
      - echo [\{\"name\":\"hogehoge\",\"imageUri\":\"$REPOSITORY_URI:$IMAGE_TAG\"\}] > imagedefinitions.json
artifacts:
  files: imagedefinitions.json

buildspec.ymlはpre_build、build、post_buildという3段階で処理を行う。一個ずつ分解して説明していく。

pre_build

ビルドする前の下準備。 GithubのURLとか、コミットハッシュとかをあとで使うので変数に入れている。ちなみにコミットハッシュはDockerイメージタグとして後で使う。

build
  - echo AWS_ACCESS_KEY=$AWS_ACCESS_KEY >> .env
  - echo AWS_SECRET_KEY=$AWS_SECRET_KEY >> .env
  - echo S3_BUCKET=$S3_BUCKET >> .env

環境変数を.envに書き込むようにしている。僕のRailsプロジェクトでは.envで環境変数を管理しており、CodeBuildでもdocker-composeが使用したいという理由でこの形にしている。このやり方がベストプラクティスではないような気がするので、もっと良い方法を見つけたい。

ちなみに環境変数はSystem Managerで管理している。環境変数についてはのちほどもう少し詳しく記載する。

  - docker-compose -f docker-compose-$ECS_ENV_NAME.yml build
  - docker-compose -f docker-compose-$ECS_ENV_NAME.yml run --name hogehoge-image web sh -c "bundle exec rake db:create && bundle exec rake db:migrate"
  - docker commit hogehoge-image $REPOSITORY_URI:latest
  - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:

あとはdocker-composeでビルドして、migrateして、Dockerイメージをcommitしているだけ。

ちなみに僕はDockerfile内にassets:precompileを行っているため、buildspec.ymlにはコマンドが書いていない。

post_build

ECSにデプロイするためには最終的にimagedefinitions.jsonというファイルを作成する必要がある。

  - docker push $REPOSITORY_URI:latest
  - docker push $REPOSITORY_URI:$IMAGE_TAG
  - echo [\{\"name\":\"hogehoge\",\"imageUri\":\"$REPOSITORY_URI:$IMAGE_TAG\"\}] > imagedefinitions.json
artifacts:
  files: imagedefinitions.json

imagedefinitions.jsonはnameとimageUirを関連付けたjsonを書いていく。複数環境あるときは当たり前だけど複数書いていく。

  • name
    • ECSのコンテナ名
  • imageUri
    • ECRのURL

ECSのタスク定義との関連は以下のようになる。

スクリーンショット_2020-03-09_16_34_33-2.png

補足

環境変数について

環境変数は秘匿化する必要があるためSystem Managerでパラメータを管理するようにした。

68017af5-fa1b-4fb2-8307-aadc174d004a-960x690r.png

安全な文字列を選択してパラメータを設定する。「名前」欄で設定した値をCodeBuildで使用する。

d9424b69-0d44-4230-afa8-c769f4a2e55a-960x573r-2.png

System Managerで設定した値をbuildspec.ymlで使用するために、CodeBuildに環境変数として設定する必要がある。これを設定しておくとbuildspec.ymlの中で$HOGEHOGEという値で使用できるようになる。

  • 名前
    • buildspec.ymlで使用する環境変数名
    • System Managerで設定した名前
  • 入力
    • 『パラメータ』を選択する
- echo AWS_ACCOUNT_ID=$AWS_ACCOUNT_ID >> .env
- echo AWS_ACCESS_KEY=$AWS_ACCESS_KEY >> .env
- echo AWS_SECRET_KEY=$AWS_SECRET_KEY >> .env

これでbuildspec.ymlにファイルの中でCodeBuildで設定した環境変数が利用できるようになる

デプロイしたときにタスク定義のバージョンは更新されていく

0ad3ec1b-5711-4351-a838-654d76ef1f81-960x596r.png

デプロイされるとimagedefinitions.jsonで設定したコンテナ名とイメージのURLでタスク定義のイメージが変更され、リビジョンが新しく更新されていく。タスクをリビジョンで管理するメリットとして「切り戻しが簡単になる」という点がある。

もしも本番で障害が発生したとき場合にもリビジョンを戻すだけで動作する。ただしDBのカラム変更などしているときは、DBをロールバックする必要もあるので注意が必要。

CodePipelineからデプロイを実行する

なんらかの理由でソースコードをpushせずにデプロイしたい場合は、Codepipelineの画面から直接行うことができる

c7177b94-b6e1-408f-84c4-de5ea8b817ad-960x598r.png

終わり

ECSは少人数の開発にこそ向いていると思う。ECSでスケールアップから障害復旧までまかせ、CodePipelineでデプロイを自動化しておけばインフラの運用をそれほど考慮しなくて済むようになる。アプリケーション層に集中して開発ができるようになる。

まだまだECSやCodePipelineに対しての知見が足りないので、また気づきがあったら書いていきたい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails】Google Books APIを叩く際の5つのTips

想定読者

  • Railsで読書系ポートフォリオを作っている方
$ ruby -v                     
ruby 2.6.5
$ rails -v
Rails 5.2.4.1

その1.APIを叩くロジックはcontrollerから切り分ける

まず以下記事(私の前記事です)のようにAPIを叩くわけですが、これをcontrollerに書いたらあっという間にFat controllerになりました。
Ruby on RailsでGoogle Books APIを叩く

APIを叩くロジックは、以下を参考にmodlueとしてapp/libに置きました。
Rails 5 で自作のモジュールを読み込む方法

APIを叩く自作module

app/lib/google_books_api.rb
module GoogleBooksApi
  def get_json_from_url(url)
    JSON.parse(Net::HTTP.get(URI.parse(Addressable::URI.encode(url))))
  end

  # ①検索するAPIを叩く
  def url_from_keyword(keyword)
    "https://www.googleapis.com/books/v1/volumes?q=#{keyword}&country=JP&maxResults=20"
  end

  # ②IDから本の情報を取得するAPIを叩く
  def url_from_id(googlebooksapi_id)
    "https://www.googleapis.com/books/v1/volumes/#{googlebooksapi_id}"
  end
end

controllerにincludeする

app/controllers/books_controller.rb
class BooksController < ApplicationController
  include GoogleBooksApi
 ()
end

books_controller内で、GoogleBooksAPIというmoduleをincludeしたので、そのmoduleの関数が使えます。

app/libというディレクトリが自作なので、
「そもそもgoogle_books_api.rbを読み込んでくれるの?」
と不安に思うかもしれません。

しかし、実はRailsでは自動的にapp/〇〇(ディレクトリ)/〇〇.rbを読み込んでくれるという仕様があるようです。
※ さらに階層が深くなるとNGみたいです。

その2.APIの構造のうち、itemを理解する

Google Books APIからは下記のように色々な情報が入ってるので、APIに慣れていないと混乱するかもしれません。
https://www.googleapis.com/books/v1/volumes?q=Rails

先に結論を書きます。
https://www.googleapis.com/books/v1/volumes?q=#{keyword}では以下が取り出せます。
{ (他のハッシュ),
"items" => [{ item1 }, { item2 }, { item3 }, ....] }

https://www.googleapis.com/books/v1/volumes/#{googlebooksapi_id}
では以下が取り出せます。
{ item }

①本を検索するAPIも、②IDから本情報を取得するAPIも、結局itemを取り出せるわけです。
(このitemの中に、本1つの情報が入っています)
このitemに対してのロジックを書くだけで、①本を検索するAPIに対しても、②IDから本情報を取得するAPIに対しても、共通のロジックを使うことができます。
よって①本を検索するAPIに対しては、以下のように処理するのが良いと思われます。

(検索するAPIから得たjson文字列)["items"].each do |item|
  (itemに対するロジック)
end

その3.検索時にはActiveRecordのオブジェクトを作らないようにする

これは私がやってしまったアンチパターンです。

最終的には検索した本をActionViewで使う際に、
render @books
あるいは
@books.each do |book|
のような繰り返し処理を書きたいかと思います。

その際に、以下のようにActiveRecordのオブジェクトを大量生成するロジックを作ってしまいました。

ActiveRecordから@booksを作る(アンチパターン)

books_controller
def search
  @books = []
  (items).each do |item|
    (中略)
    @books << Book.new(
      author: (itemから引っ張ってきた著者)
      title: (itemから引っ張ってきたタイトル)
      (その他 )
    )
  end
end

Fat controllerになる以外にも、これの問題点は2つあります。

  1. ActiveRecordのインスタンス生成(Book.new)はコストが高いのに、それを繰り返し処理させている
  2. モデルと同じattirbuteを使わなくてはいけない

1の解説

実際にやってみるとわかります。
検索し始めてから結果が表示されるまで5〜10秒くらいかかってて、UXが悪かったです。

参考(理解はできてません笑):
ActiveRecordのパフォーマンス・チューニング

2の解説

例えば検索結果としては詳細な情報が表示できるようにしたいが、アプリのDBに保存させるつもりは無い、というような場合があります。
パッと思いつくのは、
averageRating: 4.0
amount: 3960.0
みたいな情報でしょうか。レーティングや値段はその時々で変わるので、DBに保存しようとは思わないでしょう。

こういう場合は検索結果表示のときだけbook.averageRatingでレーティングを返せるようにし、DBには保存はしない、という設計が思いつきます。
しかし、ActiveRecordを使うとDBにも同一カラムが存在する必要がある、というわけです。

2つの問題点の原因

実は1,2とも原因は共通していて、要はActiveRecordはO/Rマッパーであるからです。

  • 検索結果はDBに保存するわけではありません(=DBを使いません)
  • ActiveRecordはView <-> DBの仲介役(O/Rマッパー)です。
  • よってActiveRecordを使う必要はありません。(少なくともそういう設計になってません)

ということになります。

その4.APIの情報は、オレオレクラス内に格納する

その3に対して、では具体的にどうするかをお伝えします。
結論から言うと、以下のようなオレオレクラスを作成しました。

オレオレクラス

app/lib/google_book.rb
class GoogleBook
  attr_reader :googlebooksapi_id, :author, :buy_link, :description, :image, :published_at, :title

  class << self
    include GoogleBooksApi

    def new_from_id(googlebooksapi_id)
      url = url_of_creating_from_id(googlebooksapi_id)
      item = get_json_from_url(url)
      new(item)
    end

    def search(keyword)
      url = url_of_searching_from_keyword(keyword)
      json = get_json_from_url(url)
      books = []
      if items = json['items']
        items.each do |item|
          books << GoogleBook.new(item)
        end
      end
      books
    end
  end

  def initialize(item)
    @item = item
    @volume_info = @item['volumeInfo']
    retrieve_attribute
  end

  def retrieve_attribute
    @googlebooksapi_id = @item['id']
    @author = @volume_info['authors'].first
    @buy_link = @item['saleInfo']['buyLink']
    @description = @volume_info['description']
    @image = @volume_info['imageLinks']['smallThumbnail']
    @published_at = @volume_info['publishedDate']
    @title = @volume_info['title']
  end
end

重要なポイントはitemを引数にinitializeができるようにすることです。
その2でも述べたように、itemに対して同じ処理をするように心がければ、共通のロジックを用いることができます。

使用例

これにより、以下のようにオブジェクト志向っぽく扱えるようになります。

book = GoogleBook.new_from_id("axicQgAACAAJ")
book.title
=> "影響力の武器"
book.author
=> "ロバート・B. チャルディーニ"

books = GoogleBook.search("影響力の武器")
=> [ book1, book2, book3, .... ]()
book = books.first
book.id
=> "axicQgAACAAJ"
book.title
=> "影響力の武器"

このクラスはインスタンス生成にかかるコストは大したことはありません。
よってその3のような、ActiveRecordのインスタンスを複数生成時に発生していたコストも解消できています。

注意点

books = GoogleBook.search
のbooksのクラスは、ただのArrayです。そのままではgem kaminariによるpaginateとか、render @booksとかが出来ません。
以下のように続けて書くことで、kaminariのpaginateを利用できます。

@books = Kaminari.paginate_array(books).page(params[:page])

その5.リソース登録時にはモデルにロジックを書く

上記で作ったGoogleBookクラスを使って、いざDBに本を保存するロジックを書こうとすると、これまたFat controllerになりがちです。

app/controllers/books_controller.rb
def create
  google_book = GoogleBook.new_from_id(取ってきたid)
  @book = Book.new(
    author: google_book.author
    title: google_book.title
    (その他 )
  )
  if @book.save
  (以下略)
end

controllerというのは、DBの情報を知りすぎない、というのが良い設計らしいです。上のような書き方は「DBにこれとこれが入るんでしょ」って言ってしまっています。

実装

app/controllers/books_controller.rb
def create
  google_book = GoogleBook.new_from_id(取ってきたid)
  @book = current_user.books.build
  @book = @book.substitute_for_googlebook(google_book)
  if @book.save
  (以下略)
end
app/models/book.rb
def substitute_for_googlebook(google_book)
  self.author = google_book.author
  self.description = google_book.description
  self.googlebooksapi_id = google_book.googlebooksapi_id
  self.published_at = google_book.published_at
  self.title = google_book.title
  self.buy_link = google_book.buy_link
  self.image = google_book.image
  self
end

割とcontrollerはスッキリできたのでは無いでしょうか。

といいながら実はその5はあんまり自信無いです笑
もうちょっと上手くできる気がします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.10 - TDD & Test Automation -

はじめに

記念すべき第10回目(ドドーン)。

前回まででサインアップ、サインインの機能を作っていきました。
ただどんどんアプリケーションを作っていくうちに、テスト、面倒になってきましたね。デグレも気になるし...

ということで今回は少しアプリ開発を離れまして、TDD(Test Driven Development)、そしてテスト自動化を体得していきましょう!

前回のソースコード

前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。

TDD

まずはTDDをご紹介。
特にアジャイル開発をやったりすると耳にする単語ですよね。
Test Driven Development、日本語だとテスト駆動開発です。
なんちゃら駆動開発って色々あるんですが、まぁ『なんちゃら』部分を第1に考えた開発って感じで、TDDの場合はテストファーストで開発をしていくって意味ですね。

TDDでは『レッド』『グリーン』『リファクタリング』の3つのフェーズを辿ります。

レッド

『レッド』はテストが通らないフェーズです。
TDDではまず機能をコーディングする前に期待動作のテストコードを書きます。
当然、機能をコーディングしていないのですからテストは通りません。この状態が『レッド』です。
TDDでは、この『レッド』のフェーズから機能をコーディングしていって『グリーン』のフェーズをめざします。

グリーン

『グリーン』はテストがとりあえず通ったフェーズです。
『レッド』フェーズからアプリをコーディングしていってテストコードが全て通った状態が『グリーン』フェーズです。

リファクタリング

『リファクタリング』はテストが通っていてコードとしても良い状態になったフェーズです。
『グリーン』フェーズは、『とりあえず動く』という状態です。
この『グリーン』な状態を保ちつつ、コードをより可読性高く、効率的に変更していくフェーズが『リファクタリング』です。
TDDではテスト自動化を行いますが、これがあるからこそ『動く』状態をキープしながらよりよいコードをめざせます。

TDDでは、機能やユーザーストーリー単位にこれらの3つのフェーズで開発を進めていくことで、デグレなくよいコードを作り上げることができます。

BDD

TDDの派生として、BDD(Behavior Driven Development)があります。
日本語では『振る舞い駆動開発』と言われています。

これは、TDDよりもより要望に近いテストを記述するフォーマットのようなものと覚えてもらえればよいかと思います。
開発の進め方はTDDと同じで、テストコードを記述して『レッド』『グリーン』『リファクタリング』をしていきます。

BDDでは、GivenWhenThenの3つをテストケースとして定義します。

  • Given: 前提条件です。「サインイン済みのユーザーが」って感じです。
  • When: 操作や入力です。「プロフィールページでサインアウトリンクをクリックしたとき」って感じです。
  • Then: 期待結果です。「未サインイン状態になりトップページに遷移すること」って感じです。

はい、例を出してますが、BDDにのっとると例えば

サインイン済みのユーザーが、プロフィールページでサインアウトリンクをクリックしたとき、未サインイン状態になりトップページに遷移すること

というようなテスト項目を作れます。このようにBDDはよりユーザー目線の振る舞いをテスト項目として定義する考え方です。

このハンズオンでは、このBDDを使ってテストを定義し、テストコードを書いていきます。

テスト自動化

さて、アジャイル開発やTDD/BDDを採用する場合、テスト自動化が必須です。
というよりも一回作ったらもう絶対に追加で開発をしないシステム(どんなシステム?)以外は必須だと思っています。
リファクタリングをしていったり新しい機能を作っていくときに、全てのテストケースをいちいち人間の手でやっていくのは、時間的にも人員的にも不可能だとわかりますよね。

そこで、このハンズオンでもテスト自動化を導入していきます。

Rails(Ruby)には使いやすいテストフレームワークRSpecがあります。
またプログラムでWeb画面を操作するSeleniumとこれらをよりコーディングしやすくラッピングしてくれるCapybaraというフレームワークがあります。
このハンズオンでは、この3つのツールを使って実際にユーザーが操作しているのと同じ状態をテストするE2E(End to End)テストを自動化してみます。

必要なライブラリ類をインストールする

まずはテスト自動化を実行するために必要なライブラリ類をDockerイメージやRailsアプリにインストールしていきます。
最初に、Dockerコンテナ内でブラウザを立ち上げられないとE2Eテストが行えないので、Dockerfileを編集してDockerイメージにブラウザをインストールします。
今回はChromeを立ち上げてテストできるようにします。

Dockerfile
  FROM ruby:2.6.5-alpine3.11

  ENV HOME="/app"
  ENV LANG=C.UTF-8
  ENV TZ=Asia/Tokyo

  WORKDIR $HOME

  RUN apk update && \
      apk upgrade && \
      apk add --no-cache \
          gcc \
          g++ \
          less \
          libc-dev \
          libxml2-dev \
          linux-headers \
          make \
          nodejs \
          postgresql \
          postgresql-dev \
          tzdata \
          yarn && \
+     apk add --no-cache \
+         chromium \
+         chromium-chromedriver \
+         dbus \
+         mesa-dri-swrast \
+         ttf-freefont \
+         udev \
+         wait4ports \
+         xorg-server \
+         xvfb \
+         zlib-dev && \
    apk add --virtual build-packs --no-cache \
      build-base \
      curl-dev

COPY Gemfile $HOME
COPY Gemfile.lock $HOME

RUN bundle install && \
    apk del build-packs

COPY . $HOME
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]

これらのパッケージを追加することでDockerコンテナ内でChromeブラウザを起動させてテストすることができるようになります。

次にRailsアプリに必要なライブラリを追加していきます。

Gemfile
  ...
  group :development, :test do
    gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
    gem 'pry-rails'
+
+   # For test automation
+   gem 'rspec-rails', '~>3.9'
+   gem 'capybara'
+   gem 'selenium-webdriver'
  end
  ...

ここまでで一度Dockerイメージを再ビルドしましょう。

$ docker-compose build

必要なライブラリ類がDockerイメージにインストールされた状態になります。

RSpecの初期設定をする

次にRailsアプリにRSpecの初期インストールをしていきます。
まず、ビルドしたDockerイメージからDockerコンテナを立ち上げて、rails g rspec:installコマンドを実行します。

$ docker-compose up -d
$ docker-compose exec web ash
# rails g rspec:install
Running via Spring preloader in process 131
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

色々とファイルが出来上がりました。それぞれの役割は以下の通りです。

  • .rspec: RSpecを実行するにあたりimportする設定ファイルを定義している
  • spec/spec_helper.rb: RSpecの設定ファイル
  • spec/rails_helper.rb: RSpecのRails要素をプラスした設定ファイル

rails_helper.rbspec_helper.rbをオーバーライドしている間柄ですね。

ここで.rspecを少し編集します。

.rspec
- --require spec_helper
+ --require rails_helper

これでデフォルトでrails_helper.rbが設定ファイルとして読み込まれるようになりました。
rails_helper.rbにRSpecで利用するWebドライバーの設定をしていきます。

spec/rails_helper.rb
  require 'spec_helper'
  ENV['RAILS_ENV'] ||= 'test'

  require File.expand_path('../config/environment', __dir__)

  abort("The Rails environment is running in production mode!") if Rails.env.production?
  require 'rspec/rails'

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

  begin
    ActiveRecord::Migration.maintain_test_schema!
  rescue ActiveRecord::PendingMigrationError => e
    puts e.to_s.strip
    exit 1
  end
+
+ Capybara.register_driver :selenium_chrome_headless do |app|
+   options = ::Selenium::WebDriver::Chrome::Options.new
+   options.add_argument('--no-sandbox')
+   options.add_argument('--headless')
+   options.add_argument('--disable-gpu')
+   options.add_argument('--disable-dev-shm-usage')
+   options.add_argument('--window-size=1680,1050')
+   Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
+ end

  RSpec.configure do |config|
+   # Driver setting for system tests.
+   config.before(:each, type: :system) do
+     driven_by :selenium_chrome_headless
+   end
+ 
+   config.before(:each, type: :system, js: false) do
+     driven_by :rack_test
+   end

  config.fixture_path = "#{::Rails.root}/spec/fixtures"
  config.use_transactional_fixtures = true
  config.infer_spec_type_from_file_location!
  config.filter_rails_from_backtrace!
end

Capybara.register_driverでWebドライバーの設定を新しく作ります。
ちょっと書き方が独特な気がすると思いますが、こういうものなのだと思ってください。笑
途中に--headlessという設定がありますが、headlessは目に見える形でブラウザを立ち上げることなくブラウザテストを行うことができるものです。(ヘッドレス Chrome ことはじめ  |  Web  |  Google Developers
これによってテストの実行速度が速くなったりするので是非使いましょう。

RSpec.configureで2つのパターンのWebドライバー設定をしています。
config.before(:each, type: :system)はシステムテストが実行される都度設定されるものことを意味しています。(システムテストはRSpecの用語でE2Eテストと同義と思っていただいていいかと)
driven_byでドライバーを指定するのですが、デフォルトでは先ほど上で定義したselenium_chrome_headlessが、js: falseの場合にはrack_testが設定されることがわかりますね。
rack_testは高速にテストができるのですがjavascriptの機能などを見ることはできないドライバーです。
javascriptの確認をする必要がなく高速にテストを終えたいケースや、javascriptを動かしたくないテストケースに使えます。

また、このままではrails gコマンドを実行したときに意図しないテストファイルが自動生成されてしまいます。
面倒なのでテスト関連のファイルが生成できないようにします。

config/application.rb
  ...
  module App
    class Application < Rails::Application
      config.load_defaults 6.0

      # Don't generate system test files.
      config.generators.system_tests = nil

      # Timezone
      config.time_zone = 'Tokyo'
      config.active_record.default_timezone = :local

      # Language
      config.i18n.default_locale = :ja
+
+     # Don't create test files atomatically.
+     config.generators do |g|
+       g.test_framework :rspec,
+         fixtures: false,
+         view_specs: false,
+         helper_specs: false,
+         routing_specs: false,
+         controller_specs: false,
+         request_specs: false
+     end
    end
  end

ここまででRSpecでE2Eテストを実行する初期設定ができました!

テストが実行できるか試してみる

まずは試しにここまでの設定でちゃんとテスト自動化ができるのか試してみましょう。

まず、RSpecのシステムテストを実行するファイルを格納するspec/systemディレクトリを作成し、その中にsample_spec.rbの名前のサンプルファイルを用意してみましょう。

# mkdir spec/system
# touch spec/system/sample_spec.rb

RSpecは_spec.rbをテストコードと判断して実行するようになっているのでお忘れなきよう。

ではsample_spec.rbに「トップページにアクセスできること」を確認するテストコードを記述していきます。

spec/system/sample_spec.rb
feature "サンプルテスト", type: :system do
  scenario "トップページにアクセスできること" do
    visit root_path
    expect(current_path).to eq root_path
  end
end

テストコード自体はscenarioブロックに囲まれた部分です。featureは複数のscenarioを束ねるものでtype: :systemオプションをつけることでシステムテストとして実行することを宣言しています。

「トップページにアクセスできること」シナリオのテストコードの中身もちょっと紹介します。
visit root_pathroot_path、つまり/にアクセスしています。
expect([検査対象]).to [期待結果][検査対象][期待結果]であるかどうかを検査します。
今回の検査対象はcurrent_pathです。これは今表示されているページのパスのことです。期待結果がroot_pathなので、現在表示されているページが/であればOK、そうでなければNGになります。

実はここで使っているvisitcurrent_pathはCapybaraのおかげでわかりやすい言葉で使えるようになっています。Capybaraの使い方は「使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita」などがわかりやすいと思います。あとは全力で公式ドキュメントよむ。

このように操作検査が一つのシナリオの中に記述され、検査が通るかどうかでOK/NGが判断されます。
もちろん、操作も検査も1つのシナリオの中で複数記述することができます。

では、このテストを実行してみましょう。

# rspec spec/system/sample_spec.rb
Capybara starting Puma...
* Version 4.3.1 , codename: Mysterious Traveller
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:40933
.

Finished in 16.97 seconds (files took 16.66 seconds to load)
1 example, 0 failures

RSpecのテストを実行するときはrspecコマンドを使います。今回の例のようにファイル名を指定することで、そのテストファイルのみが実行されます。
ディレクトリを指定した場合は、そのディレクトリの全てのテストコードが、rspecだけの場合はspecディレクトリの全てのテストファイルが実行されるます。

最後に1 example, 0 failuresと表示されています。exampleはシナリオ数、failuresはそのうちNGだった数を表すので、今回は1つのテストシナリオが実行され全てOKであったことがわかります。

試しに、エラーになったときにどうなるか試してみましょう。

spec/system/sample_spec.rb
  feature "サンプルテスト", type: :system do
    scenario "トップページにアクセスできること" do
      visit root_path
-     expect(current_path).to eq root_path
+     expect(current_path).to eq sign_up_path
    end
  end
Capybara starting Puma...
* Version 4.3.1 , codename: Mysterious Traveller
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:37237

F

Failures:

  1) サンプルテスト トップページにアクセスできること
     Failure/Error: expect(current_path).to eq sign_up_path

       expected: "/sign_up"
            got: "/"

       (compared using ==)



     # ./spec/system/sample_spec.rb:4:in `block (2 levels) in <main>'

Finished in 4.23 seconds (files took 6.85 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/system/sample_spec.rb:2 # サンプルテスト トップページにアクセスできること

1 example, 1 failureなのでNGになっていることがわかりますね。
途中の内容をみてみると、expected: "/sign_up"に対してgot: "/"であるためにNGになっていることが分かります。想定通りテストがNGになりました。

RSpecのシステムテストではテストがNGになった場合、自動的にスクリーンショットを保存して置いてくれます。そのファイルはtmp/screenshots/に保存されます。
ただ、ファイルをみると真っ白。本当はroot_pathにアクセスしているのでトップページが表示されていてほしいですよね。
実はこれRSpecのバグっぽいんですよね...(Rails アプリケーションの不安定なテストを撲滅したい 〜system spec のデバッグ方法とテストを不安定にさせる要因〜 - あらびき日記

ということでスクリーンショットがちゃんと表示されるようにヘルパーを作ってみましょう!

スクリーンショットを正しく表示させる

まずはテストを実行するときにヘルパーファイルを読み取るようにします。
これはrails_helper.rbでコメントアウトを外すだけでOKです。

spec/rails_helper.rb
  ...
- # Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
+ Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
  ...

これで、spec/support/**/*.rbのファイルがテスト実行時に読み込まれるようになります。
ヘルパーファイルを格納しておくディレクトリを作って、スクリーンショットを表示させるためのヘルパーとしてvisible_screenshot_helper.rbを作りましょう!

# mkdir -p spec/support/helpers
# touch spec/support/helpers/visible_screenshot_helper.rb
visible_screenshot_helper.rb
module VisibleScreenshotHelper extend ActiveSupport::Concern

  included do |example_group|
    example_group.after do
      take_failed_screenshot
    end
  end

  def take_failed_screenshot
    return if @is_failed_screenshot_taken
    super
    @is_failed_screenshot_taken = true
  end

end

RSpec.configure do |config|
  config.include VisibleScreenshotHelper, type: :system
end

Rails アプリケーションの不安定なテストを撲滅したい 〜system spec のデバッグ方法とテストを不安定にさせる要因〜 - あらびき日記」の記事を参考にしました。
これでもう一度テストを実行してみましょう。スクリーンショットがちゃんと表示されるようになるはずです。

テスト開始前にスクリーンショットを削除する

こうやってテストしているとスクリーンショットが溜まっていっちゃいますよね...
ということでテスト実行の直前にスクリーンショットを一度全て削除するようにrails_helper.rbに設定を追加します。

spec/rails_helper.rb
  ...
  RSpec.configure do |config|
    # Driver setting for system tests.
    config.before(:each, type: :system) do
      driven_by :selenium_chrome_headless
    end

    config.before(:each, type: :system, js: false) do
      driven_by :rack_test
    end
+
+   # Delete screenshots before starting new tests
+   config.before(:all) do
+     FileUtils.rm_rf(Rails.root.join('tmp', 'screenshots'), secure: true)
+   end

  config.fixture_path = "#{::Rails.root}/spec/fixtures"
  config.use_transactional_fixtures = true
  config.infer_spec_type_from_file_location!
  config.filter_rails_from_backtrace!
end

これを追加するだけです。config.before(:all)はテスト実行前に1度だけ実行されることを意味しています。
FileUtils.rm_rfはRubyプログラムとしてrm -rfをやるためのモジュールです。
【Rails】RSpecのSystem Test実行前に前回テスト時のScreenshotを削除しておく - Qiita

これでスクリーンショットが溢れかえる心配がなくなりました♪

(オプション)iTerm2でスクリーンショットを表示する

ちょっと裏技みたいな感じですが、iTerm2の場合、webコンテナにとある環境変数を与えるとテストNGのタイミングでiTerm2上にスクリーンショットを表示してくれるようになります。

docker-compose.yml
  version: '3'

  services:
    db:
      image: postgres:12.1-alpine
      environment:
        - TZ=Asia/Tokyo
      volumes:
        - ./tmp/db:/var/lib/postgresql/data

    web:
      build: .
      volumes:
        - .:/app
      ports:
        - 3000:3000
      depends_on:
        - db
+     environment:
+       - RAILS_SYSTEM_TESTING_SCREENSHOT=inline

たったこれだけ。この環境変数を適用するために一度コンテナを再起動してRSpecを実行してみましょう。

# exit
$ docker-compose down
$ docker-compose up -d
$ docker-compose exec web ash
# rspec spec/system/sample_spec.rb

image.png
ちゃんとスクリーンショットが表示されていますね。
RSpecのスクリーンショットはどのファイルなのか探すのが面倒だったりするので、テストシナリオと紐づいてiTerm2で表示してくれるのは非常に助かります。

本日はここまでにしましょう!

後片付け

では後片付けしていきますー。
今回は特にDBにデータも保存していないのでDBを初期化する必要もありませんね。
そういえば、RSpecはテスト実行時に保存されたデータはテストシナリオごとに削除されるようになっているので、テストコード内でデータを保存したとしてもDBの初期化は必要ないんです。(しかもRSpecでテストをする場合、勝手にRAILS_ENVtestで実行されるので、developmentのDBを汚染することもないんです。)

と、いうことで今回はコンテナだけ落として終了です!

# exit 
$ docker-compose down

あ、あと、sample_spec.rbは今回のサンプル用だったので消しておきましょー。
あ、ついでにスクリーンショットも。

$ rm spec/system/sample_spec.rb
$ rm tmp/screenshots/*

まとめ

今回はTDD/BDDを説明させていただきました。
RedGreenリファクタリング の流れと、
GivenWhenThen で受入条件を考えていく方法を紹介しましたね。

さらにテスト自動化を実現するために、
RSpecを導入しました。(SeleniumCapybaraも)
そして初めてのテストコードを書いて実行することができましたね!

次回は、実際にここまでつくってきたアプリのテストコードをコーディングします。
その中でテストコードの書き方を覚えていきましょう!

では、次回も乞うご期待!ここまでお読みいただきありがとうございました!

本日のソースコード

Reference

Other Hands-on Links

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む