GitLab에서 Rails 마이그레이션 테스트하기
GitLab v19.1Rails 마이그레이션을 신뢰성 있게 검사하려면 데이터베이스 스키마에 대해 테스트해야 합니다. Post 마이그레이션(/db/post_migrate) 및 백그라운드 마이그레이션 (lib/gitlab/background_migration)은 반드시 마이그레이션 테스트를 수행해야 합니다.
Rails 마이그레이션을 신뢰성 있게 검사하려면 데이터베이스 스키마에 대해 테스트해야 합니다.
마이그레이션 테스트를 작성해야 하는 경우#
-
Post 마이그레이션(
/db/post_migrate) 및 백그라운드 마이그레이션 (lib/gitlab/background_migration)은 반드시 마이그레이션 테스트를 수행해야 합니다. -
마이그레이션이 데이터 마이그레이션인 경우 반드시 마이그레이션 테스트가 있어야 합니다.
-
그 외 마이그레이션은 필요에 따라 마이그레이션 테스트를 가질 수 있습니다.
스키마 변경만 수행하는 post 마이그레이션에 대해서는 테스트를 강제하지 않습니다.
작동 방식#
(ee/)spec/migrations/ 및 spec/lib/(ee/)background_migrations의 모든 스펙은 자동으로
:migration RSpec 태그가 지정됩니다. 이 태그는
spec/support/migration.rb에서
일부 커스텀 RSpec before 및 after 훅을 실행합니다.
:gitlab_main 이외의 데이터베이스 스키마(예: :gitlab_ci)에 대해 마이그레이션을 수행하는 경우,
migration: :gitlab_ci와 같은 RSpec 태그로 명시적으로 지정해야 합니다. 예시는
spec/migrations/change_public_projects_cost_factor_spec.rb를
참조하세요.
before 훅은 테스트 대상 마이그레이션이 아직 마이그레이션되지 않은 시점까지 모든 마이그레이션을 되돌립니다.
즉, 커스텀 RSpec 훅이 이전 마이그레이션을 찾아 데이터베이스를 이전 마이그레이션 버전으로 다운 마이그레이션합니다.
이 방식을 통해 데이터베이스 스키마에 대해 마이그레이션을 테스트할 수 있습니다.
after 훅은 데이터베이스를 업 마이그레이션하여 최신 스키마 버전을 복원합니다. 이렇게 하면 이 프로세스가 이후 스펙에 영향을 미치지 않으며 올바른 격리가 보장됩니다.
ActiveRecord::Migration 클래스 테스트#
ActiveRecord::Migration 클래스(예: 일반 마이그레이션 db/migrate 또는 post 마이그레이션 db/post_migrate)를 테스트하려면,
Rails에서 자동 로드되지 않으므로 require_migration! 헬퍼 메서드를 사용하여 마이그레이션 파일을 로드해야 합니다.
예시:
require 'spec_helper'
require_migration!
RSpec.describe ...
테스트 헬퍼#
require_migration!#
마이그레이션 파일은 Rails에서 자동 로드되지 않으므로 직접 로드해야 합니다. 이를 위해 require_migration! 헬퍼 메서드를 사용할 수 있으며,
스펙 파일명을 기반으로 올바른 마이그레이션 파일을 자동으로 로드합니다.
require_migration!을 사용하여 파일명에 스키마 버전이 포함된 스펙 파일(예:
2021101412150000_populate_foo_column_spec.rb)에서 마이그레이션 파일을 로드할 수 있습니다.
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe PopulateFooColumn do
...
end
경우에 따라 스펙에서 여러 마이그레이션 파일을 require해야 할 수도 있습니다. 이 경우 스펙 파일과 다른 마이그레이션 파일 사이에 패턴이 없을 수 있습니다. 다음과 같이 마이그레이션 파일명을 직접 지정할 수 있습니다:
# frozen_string_literal: true
require 'spec_helper'
require_migration!
require_migration!('populate_bar_column')
RSpec.describe PopulateFooColumn do
...
end
table#
table 헬퍼를 사용하여 테이블에 대한 임시 ActiveRecord::Base 파생 모델을 생성합니다.
FactoryBot은
마이그레이션 스펙에서 데이터 생성에 사용해서는 안 됩니다. FactoryBot은 마이그레이션 실행 후 변경될 수 있는 애플리케이션 코드에 의존하므로 테스트 실패를 야기할 수 있습니다.
예를 들어 projects 테이블에 레코드를 생성하려면:
project = table(:projects).create!(name: 'gitlab1', path: 'gitlab1')
migrate!#
migrate! 헬퍼를 사용하여 테스트 대상 마이그레이션을 실행합니다. 마이그레이션을 실행하고
schema_migrations 테이블의 스키마 버전을 업데이트합니다. after 훅에서 나머지 마이그레이션을 트리거하며 시작 위치를 알아야 하므로 이 과정이 필요합니다. 예시:
it 'migrates successfully' do
# ... pre-migration expectations
migrate!
# ... post-migration expectations
end
reversible_migration#
reversible_migration 헬퍼를 사용하여 change 또는 up과 down 훅을 모두 가진 마이그레이션을 테스트합니다. 마이그레이션이 롤백된 후 애플리케이션과 데이터의 상태가 마이그레이션이 처음 실행되기 전과 동일한지 테스트합니다. 이 헬퍼는:
-
up 마이그레이션 이전에
before기대값을 실행합니다. -
up 마이그레이션을 실행합니다.
-
after기대값을 실행합니다. -
down 마이그레이션을 실행합니다.
-
before기대값을 두 번째로 실행합니다.
예시:
reversible_migration do |migration|
migration.before -> {
# ... pre-migration expectations
}
migration.after -> {
# ... post-migration expectations
}
end
Post-deployment 마이그레이션을 위한 커스텀 매처#
spec/support/matchers/background_migrations_matchers.rb에
백그라운드 마이그레이션이 post-deployment 마이그레이션에서 올바르게 스케줄링되었는지,
그리고 올바른 수의 인수를 받았는지 검증하는 커스텀 매처가 있습니다.
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
백그라운드 마이그레이션 테스트 예시#
이 스펙은
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
이 테스트들은 삭제 데이터베이스 정리 전략을 사용하므로 데이터베이스 트랜잭션 내에서 실행되지 않습니다. 트랜잭션이 존재한다고 가정하지 마세요.
deletion_except_tables에서 시드 데이터를 변경하는 마이그레이션을 테스트할 때, :migration_with_transaction 메타데이터를 추가하면 테스트가 트랜잭션 내에서 실행되어 데이터가 원래 값으로 롤백됩니다.