バッチ処理とRSpec実装を行いました。
RSpecのtypeに、taskは用意されていないようで、少し手間取ったのでメモしておきます。
あと、引数を渡した場合、true/falseでも文字列型になるので、工夫が必要だった。
タスク作成
$ rails g task user create lib/tasks/user.rake
入れ物のみ実装
先ずは、hello worldを出力するだけにして、Specを通してから先の実装する事にしました。
lib/tasks/user.rake に追加
namespace :user do + desc '削除予定日時を過ぎたユーザーのアカウントを削除' + task :destroy do + p 'hello world' + end end
動作確認
$ rails -T rails user:destroy # 削除予定日時を過ぎたユーザーのアカウントを削除
$ rails user:destroy hello world
テスト実装
spec/lib/tasks/user_spec.rb を作成
(rake_helperで読み込むように変更。こっちの方がハマる事が少ないらしい)
require 'rails_helper' require 'rake'require 'rake_helper' RSpec.describe User dobefore(:all) do @rake = Rake::Application.new Rake.application = @rake Rake.application.rake_require 'tasks/user' Rake::Task.define_task(:environment) end before(:each) do @rake[task].reenable end# 削除予定日時を過ぎたユーザーのアカウントを削除 describe 'user:destroy' dolet(:task) { 'user:destroy' }let!(:task) { Rake.application['user:destroy'] } it 'successful response' doexpect(@rake[task].invoke).to be_truthyexpect(task.invoke).to be_truthy end end end
spec/rake_helper.rb を作成
(上記変更に伴い追加)
require 'rails_helper' require 'rake' RSpec.configure do |config| # すべてのタスクを読み込む config.before(:suite) do Rails.application.load_tasks end # タスクを毎回実行するようにする config.before(:each) do Rake.application.tasks.each(&:reenable) end end
動作確認
$ rspec spec/lib/tasks/user_spec.rb "hello world" 1 example, 0 failures
ログ出力とドライラン追加
バッチ処理は、ログに状況書かないと動いているか解らない。
運用時の困るので出力できるようにしておきます。
また、ドライラン時は標準出力が出た方が便利なので、一緒に。
(ドライランは変更はしないけど、変更対象を確認する為のもの)
lib/tasks/user.rake を変更
(メソッドを共通化。下記、1行目を追加)
+ require './lib/tasks/application.rb' + namespace :user do desc '削除予定日時を過ぎたユーザーのアカウントを削除' - task :destroy do - p 'hello world' + task :destroy, [:dry_run] => :environment do |_, args| + args.with_defaults(dry_run: 'true') + dry_run = (args.dry_run != 'false') + + logger = Logger.new("log/user_destroy_#{Rails.env}.log") + logger.info('=== START ===') + logger_info_and_puts(dry_run, logger, "dry_run: #{dry_run}") + + # TODO: 処理 + + logger.info('=== END ===') end end+ +# log出力と、ドライランのみ標準出力 +def logger_info_and_puts(dry_run, logger, message) + logger.info(message) + p message if dry_run +end
lib/tasks/application.rb を作成
(上記変更に伴い追加)
# log出力と、ドライランのみ標準出力 def logger_info_and_puts(dry_run, logger, message) logger.info(message) p message if dry_run end
動作確認
$ rails -T rails user:destroy[dry_run] # 削除予定日時を過ぎたユーザーのアカウントを削除
$ rails user:destroy "dry_run: true" $ rails user:destroy[true] "dry_run: true" $ rails user:destroy[false]
※log/user_destroy_development.log にも出力される事を確認
$ rspec spec/lib/tasks/user_spec.rb 1 example, 0 failures
※log/user_destroy_test.log にも出力される事を確認
流れを追加
lib/tasks/user.rake を変更
logger = Logger.new("log/user_destroy_#{Rails.env}.log") logger.info('=== START ===') logger_info_and_puts(dry_run, logger, "dry_run: #{dry_run}") + users = User.where('destroy_schedule_at <= ?', Time.current).order(:destroy_schedule_at).order(:id) + logger.debug(users) + + count = users.count + logger_info_and_puts(dry_run, logger, "count: #{count}") + + users.each.with_index(1) do |user, index| + target = "[#{index}/#{count}] id: #{user.id}, destroy_requested_at: #{user.destroy_requested_at}, destroy_schedule_at: #{user.destroy_schedule_at}" + if dry_run + logger_info_and_puts(dry_run, logger, "#{target} ... Target of destroy") + next + end + + logger_info_and_puts(dry_run, logger, "#{target} ... Destroy") # TODO: 処理 + end + + logger.info('=== END ===')
テスト実装
shared_contextできれいに共通化できたと思ったが、テストレコードが増え続けてしまった。
DatabaseCleanerも試したが、増えなくなる(Truncateされる)けど、他のテストケースで作成したレコードの影響を受けてしまった。
RSpec(デフォルト設定)では、before(:each)でtransactionを張って、after(:each)でrollbackされるので、冗長な部分はあるが、下記のように変更しました。
DatabaseCleanerはテスト時間が長くなるので入れたくなかった。
FactoryBot使うとTruncateされないという記事を見かけたが、DB接続して、Truncateされている事も確認できた。
spec/lib/tasks/user_spec.rb に追加
let!(:task) { Rake.application['user:destroy'] } + let!(:dry_run) { 'false' } - it 'successful response' do - expect(task.invoke).to be_truthy - end+ shared_context '2件(削除予約1件、削除対象0件)' do + FactoryBot.create(:user) + FactoryBot.create(:user, destroy_schedule_at: Time.current + Settings['destroy_schedule_days'].days - 1.hour) + end + shared_context '3件(削除予約1件、削除対象1件)' do + include_context '2件(削除予約1件、削除対象0件)' + FactoryBot.create(:user, destroy_schedule_at: Time.current - Settings['destroy_schedule_days'].days - 1.second) + end + shared_context '4件(削除予約1件、削除対象2件)' do + include_context '3件(削除予約1件、削除対象1件)' + FactoryBot.create(:user, destroy_schedule_at: Time.current - Settings['destroy_schedule_days'].days - 1.hour) + end ++ context '2件(削除予約1件、削除対象0件)' do+ include_context '2件(削除予約1件、削除対象0件)'* before(:each) do * FactoryBot.create(:user) * FactoryBot.create(:user, destroy_schedule_at: Time.current + Settings['destroy_schedule_days'].days - 1.hour) * end + it 'ユーザーが削除されない' do + expect do + expect(task.invoke(dry_run)).to be_truthy + end.to change(User, :count).by(0) + end + end + context '3件(削除予約1件、削除対象1件)' do+ include_context '3件(削除予約1件、削除対象1件)'* before(:each) do * FactoryBot.create(:user) * FactoryBot.create(:user, destroy_schedule_at: Time.current + Settings['destroy_schedule_days'].days - 1.hour) * FactoryBot.create(:user, destroy_schedule_at: Time.current - Settings['destroy_schedule_days'].days - 1.second) * end + it 'ユーザーが1件削除される' do + expect do + expect(task.invoke(dry_run)).to be_truthy + end.to change(User, :count).by(-1) + end + end + context '4件(削除予約1件、削除対象2件)' do+ include_context '4件(削除予約1件、削除対象2件)'* before(:each) do * FactoryBot.create(:user) * FactoryBot.create(:user, destroy_schedule_at: Time.current + Settings['destroy_schedule_days'].days - 1.hour) * FactoryBot.create(:user, destroy_schedule_at: Time.current - Settings['destroy_schedule_days'].days - 1.second) * FactoryBot.create(:user, destroy_schedule_at: Time.current - Settings['destroy_schedule_days'].days - 1.hour) * end + it 'ユーザーが2件削除される' do + expect do + expect(task.invoke(dry_run)).to be_truthy + end.to change(User, :count).by(-2) + end + end
TODO実装(削除処理)
lib/tasks/user.rake を変更
- # TODO: 処理 + unless user.destroy + logger.error('User destroy') + next + end + + # TODO: メール送信
動作確認
$ rspec spec/lib/tasks/user_spec.rb 3 examples, 0 failures
$ rails c > FactoryBot.create(:user) > FactoryBot.create(:user, destroy_schedule_at: Time.current + Settings['destroy_schedule_days'].days - 1.hour) > FactoryBot.create(:user, destroy_schedule_at: Time.current - Settings['destroy_schedule_days'].days - 1.second) > FactoryBot.create(:user, destroy_schedule_at: Time.current - Settings['destroy_schedule_days'].days - 1.hour) > User.all.count => 6 > exit $ rails user:destroy "dry_run: true" "count: 2" "[1/2] id: 6, destroy_requested_at: , destroy_schedule_at: 2020-08-07 20:05:28 +0900 ... Target of destroy" "[2/2] id: 5, destroy_requested_at: , destroy_schedule_at: 2020-08-07 21:05:20 +0900 ... Target of destroy" $ rails user:destroy[false] $ rails c > User.all.count => 4 > exit
【参考】ここまでのコミット内容
https://dev.azure.com/nightonly/rails-app-origin/_git/rails-app-origin/commit/c067f59cee0cc1f7de1e3fa42493754838a54d90
TODO実装(メール送信)
書くには多いので、下記参照してください。
https://dev.azure.com/nightonly/rails-app-origin/_git/rails-app-origin/commit/e5b9682ef5d6220802ffb4d7bbeb71c388413183
動作確認
$ rails user:destroy[false] /usr/bin/open file:tmp/letter_opener/1597410039_015593_f36e8fc/rich.html
LetterOpenerWebでブラウザからも確認できますね。ActionMailer Previewも。
$ rails s -> http://localhost:3000/letter_opener -> http://localhost:3000/rails/mailers