Twi●●er風サービスの日次サマリー機能を例にRuby on Railsでバッチ処理テストを極める



## はじめに

本記事では、Ruby on Railsを使用したバッチ処理の実装とテスト方法について、実践的な例を交えて詳しく解説します。Twitter風のサービスで日次サマリーを生成するバッチ処理を題材に、効果的なテスト手法を紹介します。

## 1. プロジェクトのセットアップ

まず、新しいRailsプロジェクトを作成し、必要なgemをインストールします。

```bash
rails new twitter_summary_app --database=postgresql
cd twitter_summary_app
```

次に、`Gemfile`を以下のように編集します：

```ruby
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をインストールします：

```bash
bundle install
```

## 2. モデルとマイグレーションの作成

必要なモデルとマイグレーションを作成します。

```bash
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`を編集して、フォロワーとフォロー対象のリレーションを設定します：

```ruby
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
```

マイグレーションを実行します：

```bash
rails db:create
rails db:migrate
```

## 3. モデルの関連付け

`app/models/user.rb`に以下の関連付けを追加します：

```ruby
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`に以下の関連付けを追加します：

```ruby
class Post < ApplicationRecord
  belongs_to :user
  has_many :likes
end
```

## 4. バッチ処理の実装

`lib/tasks/daily_summary.rake`を作成し、以下のコードを追加します：

```ruby
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をインストールし、初期化します：

```bash
rails generate rspec:install
```

`spec/rails_helper.rb`に以下の設定を追加します：

```ruby
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`を作成し、以下のコードを追加します：

```ruby
FactoryBot.define do
  factory :user do
    name { Faker::Name.name }
  end
end
```

同様に、他のモデルのファクトリも作成します。

## 7. バッチ処理のテスト実装

`spec/lib/tasks/daily_summary_spec.rb`を作成し、以下のテストを記述します：

```ruby
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. テストの実行

以下のコマンドでテストを実行します：

```bash
bundle exec rspec spec/lib/tasks/daily_summary_spec.rb
```

## 9. CI設定

`.github/workflows/batch_test.yml`を作成し、以下の内容を追加します：

```yaml
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`を作成し、以下の内容を追加します：

```yaml
test:
  adapter: postgresql
  encoding: unicode
  host: localhost
  port: 5432
  username: postgres
  password: postgres
  database: twitter_summary_app_test
```

## まとめ

以上で、Ruby on Railsを使用したバッチ処理の実装とテスト方法について、実践的な例を交えて解説しました。この記事を参考に、読者の皆さんも自身のプロジェクトでバッチ処理のテストを実装してみてください。適切なテストを書くことで、バッチ処理の信頼性と保守性を大幅に向上させることができます。

バッチ処理のテストは、アプリケーションの重要な部分であり、特に大規模なデータ処理や定期的なタスクを扱う際に非常に重要です。本記事で紹介した手法を活用し、より堅牢なRailsアプリケーションの開発に役立ててください。