- 마이그레이션 테스트를 작성하는 시기
- 작동 방식
-
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
의 사용자 지정 RSpec before
및 after
후크를 활성화합니다. :gitlab_main
과 같은 데이터베이스 스키마에 대해 마이그레이션을 수행하는 경우 명시적으로 지정해야 합니다.
예를 들어 :gitlab_ci
와 같이 RSpec 태그로: migration: :gitlab_ci
. 예제는
spec/migrations/change_public_projects_cost_factor_spec.rb
을 참조하세요.
before
후크는 테스트 중인 마이그레이션이 아직 마이그레이션되지 않은 상태로 이전 마이그레이션으로 되돌립니다.
다시 말해, 우리의 사용자 지정 RSpec 후크는 이전 마이그레이션을 찾고 데이터베이스를 이전 마이그레이션 버전으로 이동시킵니다.
after
후크는 데이터베이스를 up으로 마이그레이션하고 최신 스키마 버전을 복원하여 이 프로세스가 후속 스펙에 영향을 미치지 않도록하며 적절한 격리를 보장합니다.
ActiveRecord::Migration
클래스 테스트하기
ActiveRecord::Migration
클래스(예: 일반 마이그레이션 db/migrate
또는 포스트-마이그레이션 db/post_migrate
등)를 테스트하려면 require_migration!
도우미 메서드를 사용하여 마이그레이션 파일을 로드해야 하며, Rails에 의해 자동으로 로드되지 않기 때문입니다.
예:
require 'spec_helper'
require_migration!
RSpec.describe ...
테스트 도우미
require_migration!
마이그레이션 파일은 Rails에 의해 자동으로 로드되지 않으므로 마이그레이션 파일을 수동으로 로드해야 합니다. 이를 위해 스펙 파일 이름을 기반으로 올바른 마이그레이션 파일을 자동으로 로드할 수 있는 require_migration!
도우미 메서드를 사용할 수 있습니다.
GitLab 14.4 이상에서는 마이그레이션 파일을 로드하기 위해 스키마 버전이 파일 이름에 포함된 스펙 파일에서 require_migration!
을 사용할 수 있습니다(예: 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
# ... 마이그레이션 전 기대
migrate!
# ... 마이그레이션 후 기대
end
reversible_migration
reversible_migration
도우미를 사용하여 change
또는 up
및 down
후크 중 하나 또는 둘 다를 갖는 마이그레이션을 테스트할 수 있습니다. 이것은 마이그레이션이 전진되었을 때 응용 프로그램 및 그 데이터 상태가 처음에 마이그레이션이 실행되기 전과 동일해야 함을 테스트합니다. 이 도우미는:
- up 마이그레이션 이전의 기대를 실행합니다.
- up 마이그레이션을 실행합니다.
- after 기대를 실행합니다.
- down 마이그레이션을 실행합니다.
- 두 번째로 up 기대를 실행합니다.
예:
reversible_migration do |migration|
migration.before -> {
# ... 마이그레이션 전 기대
}
migration.after -> {
# ... 마이그레이션 후 기대
}
end
배포 후 마이그레이션을 위한 사용자 정의 Matcher
우리는 배포 후 마이그레이션에서 백그라운드 마이그레이션이 올바르게 예약되었고 올바른 인수의 수를 받았는지를 확인하는데 사용되는
spec/support/matchers/background_migrations_matchers.rb
가 있습니다.
모두 be_background_migration_with_arguments
이라는 내부 Matcher를 사용하는데, 이 Matcher는 주어진 인수를 받아들일 때 마이그레이션 클래스의 #perform
메소드가 충돌하지 않았는지 확인합니다.
be_scheduled_migration
예상된 클래스와 인수를 사용하여 Sidekiq 작업이 대기열에 들어갔는지 확인합니다.
이 Matcher는 보통 도우미를 통해가 아니라 수동으로 작업을 대기열에 넣는 경우에 의미가 있습니다.
# 마이그레이션
BackgroundMigrationWorker.perform_async('MigrationClass', args)
# 스펙
expect('MigrationClass').to be_scheduled_migration(*args)
be_scheduled_migration_with_multiple_args
예상된 클래스와 인수를 사용하여 Sidekiq 작업이 대기열에 들어갔는지 확인합니다.
이 Matcher는 be_scheduled_migration
과 동일하게 작동하지만, 배열 인수를 비교할 때 순서가 무시됩니다.
# 마이그레이션
BackgroundMigrationWorker.perform_async('MigrationClass', ['foo', [3, 2, 1]])
# 스펙
expect('MigrationClass').to be_scheduled_migration_with_multiple_args('foo', [1, 2, 3])
be_scheduled_delayed_migration
예상된 지연, 클래스 및 인수를 사용하여 Sidekiq 작업이 대기열에 들어갔는지 확인합니다.
이 Matcher는 queue_background_migration_jobs_by_range_at_intervals
및 관련 도우미와 함께 사용할 수도 있습니다.
# 마이그레이션
BackgroundMigrationWorker.perform_in(delay, 'MigrationClass', args)
# 스펙
expect('MigrationClass').to be_scheduled_delayed_migration(delay, *args)
have_scheduled_batched_migration
기대되는 클래스 및 인수로 BatchedMigration
레코드가 생성되었는지 확인합니다.
*args
는 MigrationClass
로 전달된 추가 인수이며, **kwargs
는 BatchedMigration
레코드에서 확인되어야 하는 다른 속성들입니다 (예: interval: 2.minutes
).
# 마이그레이션
queue_batched_background_migration(
'MigrationClass',
table_name,
column_name,
*args,
**kwargs
)
# 스펙
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
을 예상된 백그라운드 마이그레이션 클래스로 호출하는지 확인합니다.
# 마이그레이션
finalize_background_migration('MigrationClass')
# 스펙
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 '연습 사항을 수정합니다' 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 '연습 사항을 수정합니다' 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 '백그라운드 마이그레이션을 올바르게 스케줄링합니다.', :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 "opens 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
이러한 테스트는 데이터베이스 트랜잭션 내에서 실행되지 않습니다. 삭제 데이터베이스 정리 전략을 사용하므로 트랜잭션에 의존하지 마십시오.