- マイグレーションテストを書くタイミング
- どのように動作しますか?
-
ActiveRecord::Migration
クラスのテスト -
ActiveRecord::Migration
以外のクラスのテスト
GitLab での Rails マイグレーションのテスト
Railsマイグレーションを確実にチェックするには、データベーススキーマと照らし合わせてテストする必要があります。
マイグレーションテストを書くタイミング
- ポストマイグレーション(
/db/post_migrate
)およびバックグラウンドマイグレーション(lib/gitlab/background_migration
)では、マイグレーションテストを実施する必要があります。 - マイグレーションがデータマイグレーションである場合、マイグレーションテストが必要です。
- その他のマイグレーションでも、必要に応じてマイグレーションテストを行うことができます。
スキーマの変更のみを行うポストマイグレーションでは、テストを実施しません。
どのように動作しますか?
(ee/)spec/migrations/
とspec/lib/(ee/)background_migrations
のすべての仕様には、自動的に:migration
RSpec タグが付けられます。このタグにより、spec/support/migration.rb
のカスタム RSpecbefore
およびafter
フックが実行されます。:gitlab_main
以外のデータベーススキーマ (たとえば:gitlab_ci
) に対してマイグレーションを実行する場合は、次のような RSpec タグで明示的に指定する必要があります:migration: :gitlab_ci
.例についてはspec/migrations/change_public_projects_cost_factor_spec.rbを参照してください。
before
フックは、テスト中のマイグレーションがまだマイグレーションされていない時点まで、すべてのマイグレーションを戻します。
言い換えると、私たちのカスタムRSpecフックは以前のマイグレーションを検出し、データベースを以前のマイグレーションバージョンまでマイグレーションします。
この方法で、データベーススキーマに対するマイグレーションをテストできます。
after
フックがデータベースをマイグレーションし、最新のスキーマ・バージョンをリストアします。このプロセスは後続の仕様に影響を与えず、適切な分離を保証します。
ActiveRecord::Migration
クラスのテスト
ActiveRecord::Migration
クラス (たとえば通常のマイグレーションdb/migrate
やマイグレーション後のdb/post_migrate
) をテストするには、require_migration!
ヘルパーメソッドを使ってマイグレーションファイルを読み込む必要があります。
使用例:
require 'spec_helper'
require_migration!
RSpec.describe ...
テストヘルパー
require_migration!
マイグレーションファイルはRailsによって自動ロードされないので、手動でロードする必要があります。そのためには、require_migration!
ヘルパーメソッドを使うことができます。このヘルパーメソッドは spec ファイル名に基づいて正しいマイグレーションファイルを自動的にロードします。
GitLab 14.4以降では、require_migration!
を使って、ファイル名にスキーマバージョンを含むspecファイルからマイグレーションファイルをロードすることができます(たとえば、2021101412150000_populate_foo_column_spec.rb
)。
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe PopulateFooColumn do
...
end
場合によっては、複数のマイグレーションファイルを必要とすることもあります。この場合、仕様ファイルと他のマイグレーションファイルの間にパターンはありません。このようにマイグレーションファイル名を指定することができます:
# frozen_string_literal: true
require 'spec_helper'
require_migration!
require_migration!('populate_bar_column')
RSpec.describe PopulateFooColumn do
...
end
table
table
ヘルパーを使用して、テーブル用の一時的なActiveRecord::Base
派生モデルを作成します。FactoryBotはマイグレーションが実行された後に変更される可能性があるアプリケーションコードに依存するため、マイグレーション仕様のデータ作成に使用すべきではありません。たとえば、projects
テーブルにレコードを作成します:
project = table(:projects).create!(id: 1, name: 'gitlab1', path: 'gitlab1')
migrate!
migrate!
ヘルパーを使用してテスト中のマイグレーションを実行します。マイグレーションを実行し、schema_migrations
テーブルのスキーマバージョンをバンプします。これはafter
フックで残りのマイグレーションをトリガーするために必要です。例
it 'migrates successfully' do
# ... pre-migration expectations
migrate!
# ... post-migration expectations
end
reversible_migration
change
あるいはup
とdown
の両方のフックでマイグレーションをテストするためにreversible_migration
ヘルパーを使います。これはマイグレーションが逆転した後のアプリケーションとデータの状態がマイグレーションが実行される前と同じであることをテストします。ヘルパーは
-
マイグレーションの前に
before
。 - マイグレーションをアップします。
-
after
期待を実行します。 - マイグレーションダウン。
-
before
、2回目の期待を実行します。
使用例:
reversible_migration do |migration|
migration.before -> {
# ... pre-migration expectations
}
migration.after -> {
# ... post-migration expectations
}
end
デプロイ後のマイグレーション用カスタムマッチャー
デプロイ後のマイグレーションからバックグラウンドマイグレーションが正しくスケジュールされ、正しい数の引数を受け取ったことを確認するために、spec/support/matchers/background_migrations_matchers.rb
にいくつかのカスタムマッチャーがあります。
これらはすべて内部 matcherbe_background_migration_with_arguments
を使用し、マイグレーションクラスの#perform
メソッドが提供された引数を受け取ったときにクラッシュしないことを検証します。
be_scheduled_migration
Sidekiqジョブが期待されるクラスと引数でキューに入れられたことを検証します。
このマッチャーは、通常、ヘルパーを経由するのではなく、手動でジョブをキューイングしている場合に意味があります。
# Migration
BackgroundMigrationWorker.perform_async('MigrationClass', args)
# Spec
expect('MigrationClass').to be_scheduled_migration(*args)
be_scheduled_migration_with_multiple_args
Sidekiqジョブが期待されるクラスと引数でキューに入れられたことを検証します。
これは、配列の引数を比較するときに順序が無視される点を除き、be_scheduled_migration
と同じ動作をします。
# Migration
BackgroundMigrationWorker.perform_async('MigrationClass', ['foo', [3, 2, 1]])
# Spec
expect('MigrationClass').to be_scheduled_migration_with_multiple_args('foo', [1, 2, 3])
be_scheduled_delayed_migration
Sidekiqジョブが期待される遅延、クラス、引数でキューに入れられたことを検証します。
これは、queue_background_migration_jobs_by_range_at_intervals
および関連ヘルパーでも使用できます。
# Migration
BackgroundMigrationWorker.perform_in(delay, 'MigrationClass', args)
# Spec
expect('MigrationClass').to be_scheduled_delayed_migration(delay, *args)
have_scheduled_batched_migration
BatchedMigration
レコードが期待されたクラスと引数で作成されたことを確認します。
*args
はMigrationClass
に渡される追加引数で、**kwargs
はBatchedMigration
レコードで検証されるその他の属性です (例:interval: 2.minutes
)。
# Migration
queue_batched_background_migration(
'MigrationClass',
table_name,
column_name,
*args,
**kwargs
)
# Spec
expect('MigrationClass').to have_scheduled_batched_migration(
table_name: table_name,
column_name: column_name,
job_arguments: args,
**kwargs
)
be_finalize_background_migration_of
マイグレーションが期待されるバックグラウンドマイグレーションクラスでfinalize_background_migration
を呼び出すことを検証します。
# Migration
finalize_background_migration('MigrationClass')
# Spec
expect(described_class).to be_finalize_background_migration_of('MigrationClass')
マイグレーションテストの例
マイグレーションテストはマイグレーションが具体的に何をするかによって異なりますが、最も一般的なタイプはデータマイグレーションとスケジューリングバックグラウンドマイグレーションです。
データマイグレーションテストの例
この仕様はdb/post_migrate/20200723040950_migrate_incident_issues_to_incident_type.rb
のマイグレーションをテストします。完全な仕様はspec/migrations/migrate_incident_issues_to_incident_type_spec.rb
にあります。
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe MigrateIncidentIssuesToIncidentType do
let(:migration) { described_class.new }
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
let(:labels) { table(:labels) }
let(:issues) { table(:issues) }
let(:label_links) { table(:label_links) }
let(:label_props) { IncidentManagement::CreateIncidentLabelService::LABEL_PROPERTIES }
let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
let!(:project) { projects.create!(namespace_id: namespace.id) }
let(:label) { labels.create!(project_id: project.id, **label_props) }
let!(:incident_issue) { issues.create!(project_id: project.id) }
let!(:other_issue) { issues.create!(project_id: project.id) }
# Issue issue_type enum
let(:issue_type) { 0 }
let(:incident_type) { 1 }
before do
label_links.create!(target_id: incident_issue.id, label_id: label.id, target_type: 'Issue')
end
describe '#up' do
it 'updates the incident issue type' do
expect { migrate! }
.to change { incident_issue.reload.issue_type }
.from(issue_type)
.to(incident_type)
expect(other_issue.reload.issue_type).to eq(issue_type)
end
end
describe '#down' do
let!(:incident_issue) { issues.create!(project_id: project.id, issue_type: issue_type) }
it 'updates the incident issue type' do
migration.up
expect { migration.down }
.to change { incident_issue.reload.issue_type }
.from(incident_type)
.to(issue_type)
expect(other_issue.reload.issue_type).to eql(issue_type)
end
end
end
バックグラウンドマイグレーションスケジューリングテストの例
これらをテストするには、通常
- いくつかのレコードを作成します。
- マイグレーションを実行します。
- 正しいレコードセット、正しいバッチサイズ、間隔などで、期待されたジョブがスケジュールされたことを確認します。
バックグラウンドマイグレーション自体の動作は、バックグラウンドマイグレーションクラスの別のテストで検証する必要があります。
この仕様では、db/post_migrate/20210701111909_backfill_issues_upvotes_count.rb
デプロイ後のマイグレーションをテストします。完全な仕様はspec/migrations/backfill_issues_upvotes_count_spec.rb
にあります。
require 'spec_helper'
require_migration!
RSpec.describe BackfillIssuesUpvotesCount do
let(:migration) { described_class.new }
let(:issues) { table(:issues) }
let(:award_emoji) { table(:award_emoji) }
let!(:issue1) { issues.create! }
let!(:issue2) { issues.create! }
let!(:issue3) { issues.create! }
let!(:issue4) { issues.create! }
let!(:issue4_without_thumbsup) { issues.create! }
let!(:award_emoji1) { award_emoji.create!( name: 'thumbsup', awardable_type: 'Issue', awardable_id: issue1.id) }
let!(:award_emoji2) { award_emoji.create!( name: 'thumbsup', awardable_type: 'Issue', awardable_id: issue2.id) }
let!(:award_emoji3) { award_emoji.create!( name: 'thumbsup', awardable_type: 'Issue', awardable_id: issue3.id) }
let!(:award_emoji4) { award_emoji.create!( name: 'thumbsup', awardable_type: 'Issue', awardable_id: issue4.id) }
it 'correctly schedules background migrations', :aggregate_failures do
stub_const("#{described_class.name}::BATCH_SIZE", 2)
Sidekiq::Testing.fake! do
freeze_time do
migrate!
expect(described_class::MIGRATION).to be_scheduled_migration(issue1.id, issue2.id)
expect(described_class::MIGRATION).to be_scheduled_migration(issue3.id, issue4.id)
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
end
end
end
end
ActiveRecord::Migration
以外のクラスのテスト
ActiveRecord::Migration
以外のテスト (バックグラウンドマイグレーション) を行うには、必要なスキーマのバージョンを手動で提供する必要があります。データベーススキーマを内部で切り替えたいコンテキストにschema
タグを追加してください。
設定されていない場合、schema
のデフォルトは:latest
です。
使用例:
describe SomeClass, schema: 20170608152748 do
# ...
end
バックグラウンドマイグレーションテスト例
この仕様はlib/gitlab/background_migration/backfill_draft_status_on_merge_requests.rb
のバックグラウンドマイグレーションをテストします。完全な仕様はspec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_spec.rb
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillDraftStatusOnMergeRequests do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:merge_requests) { table(:merge_requests) }
let(:group) { namespaces.create!(name: 'gitlab', path: 'gitlab') }
let(:project) { projects.create!(namespace_id: group.id) }
let(:draft_prefixes) { ["[Draft]", "(Draft)", "Draft:", "Draft", "[WIP]", "WIP:", "WIP"] }
def create_merge_request(params)
common_params = {
target_project_id: project.id,
target_branch: 'feature1',
source_branch: 'master'
}
merge_requests.create!(common_params.merge(params))
end
context "for MRs with #draft? == true titles but draft attribute false" do
let(:mr_ids) { merge_requests.all.collect(&:id) }
before do
draft_prefixes.each do |prefix|
(1..4).each do |n|
create_merge_request(
title: "#{prefix} This is a title",
draft: false,
state_id: n
)
end
end
end
it "updates all open draft merge request's draft field to true" do
mr_count = merge_requests.all.count
expect { subject.perform(mr_ids.first, mr_ids.last) }
.to change { MergeRequest.where(draft: false).count }
.from(mr_count).to(mr_count - draft_prefixes.length)
end
it "marks successful slices as completed" do
expect(subject).to receive(:mark_job_as_succeeded).with(mr_ids.first, mr_ids.last)
subject.perform(mr_ids.first, mr_ids.last)
end
end
end
これらのテストは、削除データベースのクリーンアップ戦略を使用しているため、データベーストランザクション内では実行されません。トランザクションが存在することに依存しないでください。