InfoGrab DocsInfoGrab Docs

GitLab에서 Rails 마이그레이션 테스트하기

요약

Rails 마이그레이션을 신뢰성 있게 검사하려면 데이터베이스 스키마에 대해 테스트해야 합니다. 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 beforeafter 훅을 실행합니다. :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 또는 updown 훅을 모두 가진 마이그레이션을 테스트합니다. 마이그레이션이 롤백된 후 애플리케이션과 데이터의 상태가 마이그레이션이 처음 실행되기 전과 동일한지 테스트합니다. 이 헬퍼는:

  • 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 레코드가 예상된 클래스와 인수로 생성되었는지 확인합니다.

*argsMigrationClass에 전달되는 추가 인수이고, **kwargsBatchedMigration 레코드에서 확인할 다른 속성입니다(예: 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 메타데이터를 추가하면 테스트가 트랜잭션 내에서 실행되어 데이터가 원래 값으로 롤백됩니다.

GitLab에서 Rails 마이그레이션 테스트하기

GitLab v19.1
원문 보기
요약

Rails 마이그레이션을 신뢰성 있게 검사하려면 데이터베이스 스키마에 대해 테스트해야 합니다. 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 beforeafter 훅을 실행합니다. :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 또는 updown 훅을 모두 가진 마이그레이션을 테스트합니다. 마이그레이션이 롤백된 후 애플리케이션과 데이터의 상태가 마이그레이션이 처음 실행되기 전과 동일한지 테스트합니다. 이 헬퍼는:

  • 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 레코드가 예상된 클래스와 인수로 생성되었는지 확인합니다.

*argsMigrationClass에 전달되는 추가 인수이고, **kwargsBatchedMigration 레코드에서 확인할 다른 속성입니다(예: 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 메타데이터를 추가하면 테스트가 트랜잭션 내에서 실행되어 데이터가 원래 값으로 롤백됩니다.