はじめに
本記事では、Ruby on Railsを使用したバッチ処理の実装とテスト方法について、実践的な例を交えて詳しく解説します。Twitter風のサービスで日次サマリーを生成するバッチ処理を題材に、効果的なテスト手法を紹介します。
1. プロジェクトのセットアップ
まず、新しいRailsプロジェクトを作成し、必要なgemをインストールします。
rails new twitter_summary_app --database=postgresql
cd twitter_summary_app
次に、Gemfile
を以下のように編集します:
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '3.1.2'
gem 'rails', '~> 7.0.0'
gem 'pg', '~> 1.1'
gem 'puma', '~> 5.0'
gem 'bootsnap', require: false
group :development, :test do
gem 'debug', platforms: %i[ mri mingw x64_mingw ]
gem 'rspec-rails'
gem 'factory_bot_rails'
gem 'faker'
end
group :development do
gem 'web-console'
end
gemをインストールします:
bundle install
2. モデルとマイグレーションの作成
必要なモデルとマイグレーションを作成します。
rails g model User name:string
rails g model Post content:text user:references
rails g model Like user:references post:references
rails g model Follow follower:references followed:references
rails g model DailySummary user:references date:date posts_count:integer likes_received:integer new_followers:integer
db/migrate/YYYYMMDDHHMMSS_create_follows.rb
を編集して、フォロワーとフォロー対象のリレーションを設定します:
class CreateFollows < ActiveRecord::Migration[7.0]
def change
create_table :follows do |t|
t.references :follower, foreign_key: { to_table: :users }
t.references :followed, foreign_key: { to_table: :users }
t.timestamps
end
add_index :follows, [:follower_id, :followed_id], unique: true
end
end
マイグレーションを実行します:
rails db:create
rails db:migrate
3. モデルの関連付け
app/models/user.rb
に以下の関連付けを追加します:
class User < ApplicationRecord
has_many :posts
has_many :likes
has_many :daily_summaries
has_many :follower_relationships, foreign_key: :followed_id, class_name: 'Follow'
has_many :followers, through: :follower_relationships, source: :follower
has_many :followed_relationships, foreign_key: :follower_id, class_name: 'Follow'
has_many :followeds, through: :followed_relationships, source: :followed
end
app/models/post.rb
に以下の関連付けを追加します:
class Post < ApplicationRecord
belongs_to :user
has_many :likes
end
4. バッチ処理の実装
lib/tasks/daily_summary.rake
を作成し、以下のコードを追加します:
namespace :daily_summary do
desc "Generate daily summary for all users"
task generate: :environment do
User.find_each do |user|
summary = DailySummary.new(user: user, date: Date.today)
summary.posts_count = user.posts.where(created_at: Time.zone.now.all_day).count
summary.likes_received = user.posts.sum { |post| post.likes.where(created_at: Time.zone.now.all_day).count }
summary.new_followers = user.followers.where(created_at: Time.zone.now.all_day).count
summary.save!
end
puts "Daily summaries generated successfully!"
end
end
5. テスト環境のセットアップ
RSpecをインストールし、初期化します:
rails generate rspec:install
spec/rails_helper.rb
に以下の設定を追加します:
require 'rake'
Rails.application.load_tasks
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
config.before(:each, type: :task) do
Rake::Task.clear
Rails.application.load_tasks
end
end
6. ファクトリの作成
spec/factories/users.rb
を作成し、以下のコードを追加します:
FactoryBot.define do
factory :user do
name { Faker::Name.name }
end
end
同様に、他のモデルのファクトリも作成します。
7. バッチ処理のテスト実装
spec/lib/tasks/daily_summary_spec.rb
を作成し、以下のテストを記述します:
require 'rails_helper'
RSpec.describe "daily_summary:generate", type: :task do
let(:user) { create(:user) }
let(:task) { Rake::Task['daily_summary:generate'] }
before do
create_list(:post, 3, user: user, created_at: Time.zone.now)
create_list(:like, 5, post: user.posts.first, created_at: Time.zone.now)
create_list(:follow, 2, followed: user, created_at: Time.zone.now)
end
it "generates daily summary for users" do
expect { task.invoke }.to change { DailySummary.count }.by(1)
summary = DailySummary.last
expect(summary.user).to eq(user)
expect(summary.posts_count).to eq(3)
expect(summary.likes_received).to eq(5)
expect(summary.new_followers).to eq(2)
end
it "handles time-dependent operations correctly" do
allow(Date).to receive(:today).and_return(Date.new(2024, 8, 4))
allow(Time.zone).to receive(:now).and_return(Time.zone.local(2024, 8, 4, 0, 0, 0))
task.invoke
summary = DailySummary.last
expect(summary.date).to eq(Date.new(2024, 8, 4))
end
context "when user has no activity" do
let(:inactive_user) { create(:user) }
it "creates summary with zero counts" do
task.invoke
summary = DailySummary.find_by(user: inactive_user)
expect(summary.posts_count).to eq(0)
expect(summary.likes_received).to eq(0)
expect(summary.new_followers).to eq(0)
end
end
context "when there are a large number of users" do
before { create_list(:user, 1000) }
it "completes without timeout" do
expect { Timeout.timeout(30) { task.invoke } }.not_to raise_error
end
end
end
8. テストの実行
以下のコマンドでテストを実行します:
bundle exec rspec spec/lib/tasks/daily_summary_spec.rb
9. CI設定
.github/workflows/batch_test.yml
を作成し、以下の内容を追加します:
name: Batch Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1.2
- name: Install dependencies
run: |
sudo apt-get install libpq-dev
bundle install
- name: Setup database
env:
RAILS_ENV: test
POSTGRES_PASSWORD: postgres
run: |
cp config/database.yml.ci config/database.yml
bundle exec rails db:create
bundle exec rails db:schema:load
- name: Run batch tests
run: bundle exec rspec spec/lib/tasks
また、config/database.yml.ci
を作成し、以下の内容を追加します:
test:
adapter: postgresql
encoding: unicode
host: localhost
port: 5432
username: postgres
password: postgres
database: twitter_summary_app_test
まとめ
以上で、Ruby on Railsを使用したバッチ処理の実装とテスト方法について、実践的な例を交えて解説しました。この記事を参考に、読者の皆さんも自身のプロジェクトでバッチ処理のテストを実装してみてください。適切なテストを書くことで、バッチ処理の信頼性と保守性を大幅に向上させることができます。
バッチ処理のテストは、アプリケーションの重要な部分であり、特に大規模なデータ処理や定期的なタスクを扱う際に非常に重要です。本記事で紹介した手法を活用し、より堅牢なRailsアプリケーションの開発に役立ててください。