0
0

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

Posted at

はじめに

本記事では、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アプリケーションの開発に役立ててください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0