バッチ処理と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
