成果物の作成2作品目⑨ 予定管理サービス インテグレーションテスト実装
今回は全体のインテグレーションテストを作り込んでいきます。
まずはヘッダーレイアウトのインテグレーションテストです。
omniauthを使ったログイン部分を実装していきます。
minitestでテストを書いているのですが、日本語で検索してもomniauthのテスト方法がRspecを用いた方法しか出てこず(ominiauth英語公式ページもRspecでの方法)、めっちゃ詰まりました…
英語圏のwebページにて少しだけ断片的な情報が見つかったので、試行錯誤しながら実装してみました。
loginメソッドと、後で使うのでlogoutメソッドを作りました。
test/test_helper.rb
setup do OmniAuth.config.test_mode = true end def login OmniAuth.config.add_mock(:twitter, { uid: '12345678910' }) get '/auth/twitter' request.env['omniauth.env'] = OmniAuth.config.mock_auth[:twitter] get '/auth/twitter/callback' end def logout delete '/logout' end
テストは通るようなので、これで大丈夫そうです。
条件分岐部分で少しつまりましたが、こんな感じで、
def setup @user = users(:user) end test "スケジュール新規作成機能に関するテスト)" do #createアクションのif文条件分岐3つ網羅 login(@user) #test_helperのloginメソッド get new_schedule_path assert_select 'h1', "スケジュール入力ページ" assert_select 'input[type="text"]' # 無効な送信 条件分岐1 assert_no_difference 'DaySchedule.count' do post schedule_index_path, params: { day_schedule: { day_schedule: "" } } end assert_equal '入力値が空か、255文字以上です。保存に失敗しました', flash[:danger] assert_template 'schedule/new' # 有効な送信 assert_difference 'DaySchedule.count', 1 do post schedule_index_path, params: { day_schedule: { day_schedule: "テスト用文章" } } end follow_redirect! assert_equal 'スケジュールの作成に成功しました!', flash[:success] assert_template 'time_schedules/new' # もう一度スケジュール新規作成 get new_schedule_path assert_no_difference 'DaySchedule.count' do post schedule_index_path, params: { day_schedule: { day_schedule: "テスト用文章2" } } end # 無効な送信 assert_equal 'すでにあなたの予定は存在します、ヘッダーリンクの「予定とユーザー情報」ページから、一度予定を削除してからリトライしてください', flash[:danger] assert_template 'schedule/new' # スケジュール削除 get user_path(@user) assert_difference 'DaySchedule.count', -1 do delete schedule_path(@user.day_schedule.first) end follow_redirect! assert_equal 'スケジュールを削除しました', flash[:success] assert_template 'home/index' end end
合わせてフィクスチャーも各モデル同士のリレーションシップを考慮して書きました。
/schedule/test/fixtures/users.yml
user: id: 1 provider: twitter uid: 123 user_name: user other_user: id: 2 provider: twitter uid: 456 user_name: other_user <% 7.times do |n| %> user_<%= n %>: id: <%= n + 3 %> provider: twitter uid: <%= n + 3 %> user_name: <%= "User #{n}" %> <% end %>
test/fixtures/day_schedules.yml
one_day: day_schedule: today's_schedule user: other_user
test/fixtures/time_schedules.yml
first_time: time_schedule: test start_time: 1900-00-01 01:00:00 end_time: 1900-00-01 02:00:00 day_schedule: one_day
全部ブログに書き出すと膨大になってしまうので、とりあえずこれぐらいにしておきます。
今回、テストのominiauthのログインが分からず、だいぶ時間を消費したり、
ビューファイルがテストしづらいhtmlのコーディングになっていたので書き直したり、
ユーザーを削除した際にログアウトできていないバグを見つけてコントローラを書き直したり、
効率的なテストの書き方をRails テスティングガイドを見ながらあれこれ考えて書いたり、
フィクスチャーファイルも何回か書き直したり、
大分時間がかかりましたが、様々な知見が得られたと思います。
バグフィックスも終わったので、後はコードを最終チェックしていよいよ完成にしたいと思います!
成果物の作成2作品目⑧ 予定管理サービス コントローラのbeforeアクションの追加、全体のテスト実装
今回は細かいバグを取り除くために、コントローラのbeforeアクションとテストを書いていこうと思います。
minitestを使っています。
beforアクションの追加
今のままだと未ログイン状態でも誰がログインしていようと、お構いなしにいろんなURLにアクセスできてしまうので、
各コントローラに before_action制限をかけます
app/controllers/users_controller.rb
before_action :logged_in_user, only: [:index,:show, :destroy] before_action :correct_user, only: :destroy
/schedule/app/controllers/schedule_controller.rb
before_action :logged_in_user, only: [:show,:new, :destroy,:create] before_action :correct_user, only: :destroy
/schedule/app/controllers/time_schedules_controller.rb
before_action :logged_in_user, only: [:new,:create] before_action :correct_user, only: [:new,:create]
テストの追加
テストを完成させていこうと思います。
Userモデル、DayScheduleモデル、TImeScheduleモデルの種類があるので、それぞれフィクスチャーを作成しました。
モデル
全部載せると長すぎるので、とりあえずUserモデルのみ掲載します。
/schedule/test/models/user_test.rb
require 'test_helper' class UserTest < ActiveSupport::TestCase def setup @user = users(:user) end test "サニティーチェック" do assert @user.valid? end test "user_nameのバリデーションが機能するか" do @user.user_name = " " assert_not @user.valid? end test "uidのバリデーションが機能するか" do @user.uid = "" assert_not @user.valid? end test "providerのバリデーションが機能するか" do @user.provider = "" assert_not @user.valid? end test "Userモデルが削除されると紐付いているDayScheduleモデルも同時に削除されるか" do @user.save @user.day_schedule.create!(day_schedule: "test") assert_difference 'DaySchedule.count', -1 do @user.destroy end end end
コントローラ
require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:user) end test "未ログイン状態でユーザ一覧ページにgetアクセスするとログインページにリダイレクトれるか" do get users_path assert_redirected_to root_url end test "未ログイン状態でユーザーページにgetアクセするときちんとログインページにリダイレクトされるか。" do get user_path(@user) assert_redirected_to root_url end test "未ログイン状態でユーザ情報を削除するときちんと失敗するか" do assert_no_difference 'User.count' do delete user_path(@user) end assert_redirected_to root_url end end
結果はグリーンです!
手動テストでも動作は大丈夫でした。
今回はインテグレーションテストまではできませんでしたが、次回完成まで持っていきたいと思います。
成果物の作成2作品目⑦ 予定管理サービス ユーザー一覧のページネーション機能追加、それに対するテスト。
今回は登録ユーザー数が増えても大丈夫なようにユーザー一覧のページネーション機能追加、
それから、好き勝手にアクセス出来ないように、コントローラのbeforeアクションの追加を行っていこうと思います。
ページネーション
まずgemの追加とbundeleインストール
gem 'will_paginate', '3.1.6' gem 'bootstrap-will_paginate', '1.0.0'
コントローラの対応アクションに以下を追加します。今回は5人の表示にします
def index @users = User.paginate(page: params[:page], per_page: 5) end
それからviewのページネイトさせたい範囲をユーザー一覧にwillpaginateで囲みます この際、ページを前後するリンクボタンに表示させる文字もオプションで付けれます。
<%= will_paginate, :previous_label => ' < 前へ', :next_label => '次へ >' %> %> <ul class="users"> <%= render @users %> </ul> <%= will_paginate, :previous_label => ' < 前へ', :next_label => '次へ >' %> %>
仮想ユーザーの追加
実際にページネーションがうまく機能しているか確認するために、仮想のユーザーデータを作ります。
gem 'faker', '1.7.3'
seedsに以下の記述をします
db/seeds.rb
User.create!( provider: "twitter", uid:1, user_name: "Example" ) 7.times do |n| name = Faker::Name.name User.create!( provider: "twitter", uid:n+1, user_name: name) end
今回は1ページに付き5人表示なので、とりあえず8人分揃えてみました。
表示は大丈夫だったので、次はテストを書きます。
michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> lana: name: Lana Kane email: hands@example.gov password_digest: <%= User.digest('password') %> malory: name: Malory Archer email: boss@example.gov password_digest: <%= User.digest('password') %> <% 5.times do |n| %> user_<%= n %>: name: <%= "User #{n}" %> email: <%= "user-#{n}@example.com" %> password_digest: <%= User.digest('password') %> <% end %>
続きまして、テストを書きます。
rails g integration_test users_index
フィクスチャー test/fixtures/users.yml
user: id: 1 provider: twitter uid: 1 user_name: user otheruser: id: 2 provider: twitter uid: 2 user_name: other_user <% 7.times do |n| %> user_<%= n %>: id: <%= n + 3 %> provider: twitter uid: <%= n + 3 %> user_name: <%= "User #{n}" %> <% end %>
test/integration/users_index_test.rb
require 'test_helper' class UsersIndexTest < ActionDispatch::IntegrationTest def setup @user = users(:user) end test "ページネーションがうまく機能しているか" do get users_path assert_template 'users/index' assert_select 'div.pagination' User.paginate(page: 1, per_page: 5).each do |user| assert_select 'a[href=?]', user_path(user), text: user.user_name end end end
無事にテストも通過し、実動作もちゃんとしているようなので完成です!
成果物の作成2作品目⑥ 予定管理サービス 全体のレイアウトを整理、そしてバグの改善
前々回のモデル作成は3回ほど作り直したり、グラフ部分の作成もも試行錯誤の連続で結構時間が掛かかったのですが、 今回は表示部分なので、割とサクサク進められました。
今回はフォーム画面とスケジュール画面が雑多、かつ不必要な情報も多かったので、整えました。
バグの改善render
昨日気づいたのですが、バグがありました。
render先を変えることで対応。
スケジュール表示画面のレイアウト
まず、スケジュール画面の表示が見辛い構成だったので、bootstrapの形式に従い、classにcontainer,row,col-md-〇〇の入れ子構造を持たせてレイアウトを整え、
またhtmlコード自体も散らかっていたので、成形しました。
<div class=”container”> <h2>スケジュール</h2> <div class="row"> <div class= "col-md-4"> <ul class="list-group"> <% @time_shcedule.each do |t| %> <li class="list-group-item list-group-item-info"> <%= t.time_schedule %> : <%= t.start_time.slice(11..15) %> : ~ <%= t.end_time.slice(11..15) %> </li> <% end %> </ul> </div> <div class= "col-md-8"> <% if @day_schedule.time_schedule.first %> <%= timeline @chart %> <% end%> </div> </div> </div>
また新規作成ページとスケジュールページで予定一覧の表示部分は同じコードを使いまわせそうなので、
パーシャル化してコードをすっきりさせておきました。この際、それぞれ違うインスタンス変数を使うので、変数はロケール化しておきました。
パーシャル呼び出し部分
<%= render partial: 'time_schedules/time_schedules_view', locals: { time_schedules: @time_schedules } %>
パーシャル time_schedules/time_schedules_view
<ul class="list-group"> <% time_schedules.each do |t| %> <li class="list-group-item list-group-item-info"> <%= t.time_schedule %>: <%= t.start_time.slice(11..15) %> ~ <%= t.end_time.slice(11..15) %> </li> <% end %> </ul>
全体のレイアウト
custom.scssファイルを作り、フォントや、色の指定を行い、背景画像の指定も行いました。
body{ font-family: 游ゴシック体, 'Yu Gothic', YuGothic, 'ヒラギノ角ゴシック Pro', 'Hiragino Kaku Gothic Pro', メイリオ, Meiryo, Osaka, 'MS Pゴシック', 'MS PGothic', sans-serif; background-image: image-url('bg.jpg'); }
背景画像は以下のウェブサービスを利用させていただきました。
おしゃれな写真が多く、好きなサイトです。
フリー素材ぱくたそ(www.pakutaso.com)
背景が変わって、ヘッダーが浮き気味だったので、なじませるために色を変えて、半透明にしてみました。
#menu { a { color:#000000; } background-color: rgba(245,245,245,0.3); & li a:hover { background: rgba(205,92,92,0.9); } }
時間データの表示の改善
また、グラフ描画に使う関係上、予定の開始時間と終了時間はモデル内に"1900-00-00 00:00:00"という形でstring型で入れてあります。(年と日の部分は使わないので1900-00-00で固定)
このまま表示すると1900-00-00 00:00:00の年と、日、秒のデータが邪魔なので、表示させる際に、12~16行目部分のみ切り出して表示させたいです。
調べてみると、指定の範囲に対応する部分文字列を削除するslice!メソッドがあるらしいので、
これを使ってモデルからデータ取り出し加工する自作メソッドを作ろうと四苦八苦したのですが、
途中でふと、そんな複雑な事をしなくても見える部分だけ変えればいい事に気づき、view表示部にslice!メソッドを使うことにしました。
また、調べるとslice!とは逆に特定に部分だけを抽出してくれるsliceメソッドが有ることに気づいたので、こちらで実装しました。
先程も出ましたこのコードですね
<%= t.start_time.slice(11..15) %> ~ <%= t.end_time.slice(11..15) %>
かなり完成に近づいてきましたね!
今回issueを忘れてしまったために、後からの作成の作成になってしまったのですが、
いつもどおりgit pushしてマージします 。
暫定的にherokuにもデプロイしてみて、試してみましましたが問題なさそうです。
次回はバグをなくすために、バリデーション部分やコントローラのbeforeアクションの細かい部分を作り込んでいき、
また、テスト部分のコーディングや手動テスト、試運転やらを行い、問題がなければ完成に持っていこうと思います
成果物の作成2作品目⑤ 予定管理サービス スケジュールのグラフ表示
グラフ表示部分の導入です。 ライブラリはchartkickを使おうと思います。
公式サイトを見ながら勧めていきます。 https://chartkick.com/
表示するグラフのタイプは選べるのですが、何がいいかは試行錯誤しながら決めていきます。
おそらくtimelineかpie chartがいいかな。
設定
まずこのgemをインストール
gem "chartkick"
bundle install
application.jsに以下の項目を加えます
//= require Chart.bundle //= require chartkick
timelineを使うためにはgooglechartsの機能も使わないといけないので、
headタグ内に
<%= javascript_include_tag "https://www.gstatic.com/charts/loader.js" %>
を埋め込みます。
<!DOCTYPE html> <html lang="ja"> <html> <head> <%= javascript_include_tag "https://www.gstatic.com/charts/loader.js" %> <title>full_title(schedule)</title> <%= csrf_meta_tags %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> </head>
実装
さて、時間グラフ描画用のtimelineの書式は
こういう[[名前][始まる時間][終わる時間].
[名前2][始まる時間][終わる時間] ..]という2次元配列の形式になっています。
<%= timeline [["Monday", "0000-00-00 15:00:00", "0000-00-00 15:15:00"], ["Tuesday", "0000-00-00 16:05:00", "0000-00-00 16:07:00"]] %>
時刻と日付データが以上の形式なので、この形式でそれぞれDB内に、time_scheduleカラム、start_timeカラム、end_timeカラムとしてデータを入れます。
そして、そのDBからデータを取り出し、chartkickの形式に沿った2次元配列をコントローラで作り、
viewでグラフ描画すれば完成!
と思いきや、グラフに渡す時刻データと実際に描画される描画の時刻が9時間分ずれるという謎の現象が発生。
いろいろ調べましたが、世界標準時UTCと日本標準時JSTの時差も9時間なので、おそらく時刻の設定の関係と思われます。
いろいろ試行錯誤しましたが、全然うまく描画されません。
グラフの表示機能はオミットしようかと思いましたが、せっかくだし、面白そうなので自作のメソッドを組んで対応してみることにしました。
時刻データと、描画されるデータに9時間分のズレがありますので、DBからデータを引っ張って来てあげて、
9時間ずらしたデータをchartkickに渡してあげれば、正常なグラフが描画されるはずです。
グラフ描画用にモデルの普通のデータから、2次元配列の生成をする
app/controllers/schedule_controller.rb
def show @day_schedule = DaySchedule.find(params[:id]) @time_shcedule = @day_schedule.time_schedule #グラフ描画用多次元配列の作成 @chart = [] #グラフ用の配列の宣言 @time_shcedule.each do |num| val1 = num.time_schedule val2 = num.start_time val2 = conversion(val2) #timechartのグラフの時刻表示が9時間ほどずれてしまうので、ずれを無くすために val3 = num.end_time #applicationhelperのconversionメソッドを自作し、文字列を操作、 val3 = conversion(val3) #多次元配列を作り、chartkickでグラフ描画。 @chart << [val1, val2, val3] end
変換メソッド
app/helpers/application_helper.rb
def conversion(string) #グラフの表示が9時間分ずれているので、変換メソッドを作成。 a = string[11..12] #変数[11..12]で、変数内の前から12~13番目の文字情報を取り出す return "1900-00-00 15:00:00" if a == "00" #文字12~13番目の文字がどれかなら、対応する文字列を返す return "1900-00-00 16:00:00" if a == "01" return "1900-00-00 17:00:00" if a == "02" return "1900-00-00 18:00:00" if a == "03" return "1900-00-00 19:00:00" if a == "04" return "1900-00-00 20:00:00" if a == "05" return "1900-00-00 21:00:00" if a == "06" return "1900-00-00 22:00:00" if a == "07" return "1900-00-00 23:00:00" if a == "08" return "1900-00-00 00:00:00" if a == "09" return "1900-00-00 01:00:00" if a == "10" return "1900-00-00 02:00:00" if a == "11" return "1900-00-00 03:00:00" if a == "12" return "1900-00-00 03:00:00" if a == "13" return "1900-00-00 05:00:00" if a == "14" return "1900-00-00 06:00:00" if a == "15" return "1900-00-00 07:00:00" if a == "16" return "1900-00-00 08:00:00" if a == "17" return "1900-00-00 09:00:00" if a == "18" return "1900-00-00 10:00:00" if a == "19" return "1900-00-00 11:00:00" if a == "20" return "1900-00-00 12:00:00" if a == "21" return "1900-00-00 13:00:00" if a == "22" return "1900-00-00 14:00:00" if a == "23" end
コントローラ
/schedule/app/views/schedule/show.html.erb
<%= timeline @chart %>
これで無事にきちんと描画されました!
嬉しいですね!
成果物の作成2作品目④ 予定管理サービス スケジュール関係のモデルの作成
スケジュール関係のモデルの実装です。
今回もissueを作ります。
DBモデル作成
モデルですが、これがかなり悩んで紆余曲折試行錯誤した結果以下で行くことにしました。
Userモデル(登録ユーザ情報)omniauth-twitterに合わせて中身を作ります。
主キー | 名前 | プロバイダー(ツイッター) | ツイッターUID | ツイッター写真 |
---|---|---|---|---|
integer型 | string型 | string型 | string型 | string型 |
id | user_name | :provider | uid | image_url |
DayScheduleモデル(その日の予定)
外部キー | 主キー | 一日の予定 |
---|---|---|
integer型 | integer型 | string型|integer |
User_id | day_schedule_id | day_schedule |
TimeScheduleモデル(時間辺りの予定)
外部キー | 主キー | 時間辺りの予定名 | 始まりの時間 | 終わりの時間 |
---|---|---|---|---|
integer型 | integer型 | string型 | time型or string型 | time型or string型 |
DaySchedule_id | time_schedule_id | time_schedule | start_time | end_time |
モデルの作成
3つのモデルを作ることにしましたが、各モデルの関係性を表すとこうなります。
User → 1対多 → DaySchedule
User ← 1対1 ← DaySchedule
DaySchedule → 1対多 → TimeSchedule
DaySchedule ← 1対1 ← TimeSchedule
Userモデルはもう作ってあるので、
まず上記の関係性に従ってScheduleモデルを作っていきます。
DayScheduleモデル
以下のコマンドを入力します
rails g modelDaySchedule day_schedule:integer schedule:string user:references
作成されたモデルにバリデーションを付けていきます。
schedule_per_day.rb
class SchedulePerDay < ApplicationRecord belongs_to :user has_many :time_schedule, dependent: :destroy validates :user_id, presence: true validates :schedule ,presence: true,length: { maximum: 255 } end
予定名day_scheduleは255文字までしか入れない仕様なので、length: { maximum: 255 }を追加します。
SchedulePerDayモデルテスト
そしてモデルに対するテストを書きます。
schedule_per_day_test.rb
require 'test_helper' class SchedulePerDayTest < ActiveSupport::TestCase def setup @user = users(:user) @schedule_per_day = @user.schedule_per_days.build(schedule: "Lorem ipsum",day_of_the_week: 1) end test "should be valid" do assert @schedule_per_day.valid? end test "user id should be present" do @schedule_per_day.user_id = nil assert_not @schedule_per_day.valid? end test "day_of_the_week should be present " do @schedule_per_day.day_of_the_week = nil assert_not @schedule_per_day.valid? end test "day_of_the_week should be in 0^6" do @schedule_per_day.day_of_the_week = 10 assert_not @schedule_per_day.valid? end test "schedule should be present" do @schedule_per_day.schedule = nil assert_not @schedule_per_day.valid? end test "shedule should be at most 255" do @schedule_per_day.schedule = "a" * 256 assert_not @schedule_per_day.valid? end
テストはgreenになりましたので、SchedulePerDayモデルは完成です!
SchedulePerTimeモデル
同様にSchedulePerTimeモデルを制作します
rails g model TimeSchedule TimeSchedule:string start_time:string end_time:string user:references
作成されたモデルにバリデーションを付けていきます。
schedule_per_time.rb
class SchedulePerTime < ApplicationRecord belongs_to :schedule_per_day validates :Schedule_per_day_id, presence: true validates :schedule_per_one, presence: true,length: { maximum: 255 } validates :start_time ,presence: true validates :end_time ,presence: true end
validates :start_time ,presence: trueと validates :end_time ,presence: trueの部分はまだ完全に決まってないので、後で追加するかもしれません。
SchedulePerTimeモデルテスト
そしてモデルに対するテストを書きます。
schedule_per_time_test.rb
class SchedulePerTimeTest < ActiveSupport::TestCase def setup @schedule_per_day = schedule_per_days(:one) @schedule_per_time = @schedule_per_day.schedule_per_time.build(schedule_per_one: "Lorem ipsum", start_time: "01:00", end_time: "01:00") end test "should be valid" do assert @schedule_per_time.valid? end test "schedule_per_day_id should be present" do @schedule_per_time.schedule_per_day_id= nil assert_not @schedule_per_time.valid? end test "schedule_per_one should be present" do @schedule_per_time.schedule_per_one = nil assert_not @schedule_per_time.valid? end test "schedule_per_one be at most 255" do @schedule_per_time.schedule_per_one = "a" * 256 assert_not @schedule_per_time.valid? end test "srtart_time should be present " do @schedule_per_time.start_time= nil assert_not @schedule_per_time.valid? end test "end_time should be present " do @schedule_per_time.end_time= nil assert_not @schedule_per_time.valid? end end
こちらもGreenで完成です!
成果物の作成2作品目③ 予定管理サービス ユーザーページとスケジュールページの作成
まず、ログインした後にリダイレクトされるスケジュールページを簡単に作り、ユーザーページの原型も作っていきます。
まず、githubにissueします。
#○と番号が出ますので、
ローカルリポジトリと紐付けるために開発環境でgit checkut -b #〇でその番号と同じブランチを作ります。
レイアウト関係のgemファイルを追加し
gemfile
gem 'will_paginate', '3.1.6' gem 'bootstrap-will_paginate', '1.0.0'
登録ユーザー一覧ページ、ユーザープロフィールページ、フッター、ヘッダー、認証系の自作コントローラメソッド、追加に伴うroutes.rbの変更などを行 いました。
また、custom.scssを作成し、レイアウトを追加して、テストも少々書いておきました。
そして最後に
git add. gitcommit -m "#イシュー番号" git push origin"#イシュー番号"
で無事にGithub上のコードページに出のトップにcommitが出てきたので、これをマージします。
そして、isuuesタブを開いて、close issuesを選択、issueがclose状態になるのを確認して終了です!
前回は紐付いたcommitによって自動的にcloseになると勘違いしてて気づかなかったんですが、手動でしてあげる必要があるみたいですね。