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

신뢰할 수 있는 Rails 마이그레이션을 검사하기 위해서는 데이터베이스 스키마에 대해 테스트해야 합니다.

마이그레이션 테스트를 작성해야 하는 경우

  • Post 마이그레이션(/db/post_migrate) 및 백그라운드 마이그레이션(lib/gitlab/background_migration)은 반드시 마이그레이션 테스트를 수행해야 합니다.
  • 만약 마이그레이션이 데이터 마이그레이션인 경우에는 반드시 마이그레이션 테스트를 수행해야 합니다.
  • 다른 마이그레이션은 필요한 경우에만 마이그레이션 테스트를 수행할 수 있습니다.

우리는 스키마 변경만 수행하는 포스트 마이그레이션에 대해서는 테스트를 강제하지 않습니다.

작동 방식

(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 또는 포스트 마이그레이션 db/post_migrate)를 테스트하려면 Rails에서 자동으로 로드되지 않으므로 require_migration! 헬퍼 메서드를 사용하여 마이그레이션 파일을 로드해야 합니다.

예시:

require 'spec_helper'

require_migration!

RSpec.describe ...

테스트 헬퍼

require_migration!

마이그레이션 파일은 Rails에 의해 자동으로 로드되지 않기 때문에 마이그레이션 파일을 수동으로 로드해야 합니다. 이를 위해 특정 스펙 파일의 스키마 버전을 포함하는 마이그레이션 파일을 자동으로 로드할 수 있는 require_migration! 헬퍼 메서드를 사용할 수 있습니다.

예를 들어, 파일 이름에 스키마 버전이 포함된 스펙 파일에서 마이그레이션 파일을 로드할 수 있습니다(예: 2021101412150000_populate_foo_column_spec.rb).

# frozen_string_literal: true

require 'spec_helper'
require_migration!

RSpec.describe PopulateFooColumn do
  ...
end

일부 경우에는 여러 마이그레이션 파일을 로드하기 위해 스펙에서 여러 번 require_migration!을 사용해야 할 수도 있습니다. 이 경우에는 스펙 파일과 다른 마이그레이션 파일 사이에 패턴이 없습니다. 다음과 같이 마이그레이션 파일 이름을 명시할 수 있습니다.

# frozen_string_literal: true

require 'spec_helper'
require_migration!
require_migration!('populate_bar_column')

RSpec.describe PopulateFooColumn do
  ...
end

table

임시 ActiveRecord::Base 파생 모델을 생성하기 위해 table 헬퍼를 사용합니다. FactoryBot 마이그레이션 스펙에 데이터를 생성하는 데에는 사용해서는 안 됩니다. 왜냐하면 이는 마이그레이션이 실행된 후에 변경될 수 있는 애플리케이션 코드에 의존하기 때문에 테스트가 실패할 수 있기 때문입니다. 예를 들어, projects 테이블에 레코드를 생성하려면 다음과 같이 사용할 수 있습니다.

project = table(:projects).create!(name: 'gitlab1', path: 'gitlab1')

migrate!

migrate! 헬퍼를 사용하여 테스트 중인 마이그레이션을 실행합니다. 이는 마이그레이션을 실행하고 schema_migrations 테이블에서 스키마 버전을 올리는 역할을 합니다. 이 헬퍼는 after 훅에서 남은 마이그레이션을 실행하기 때문에 어디서 시작해야 하는지 알아야 하기 때문에 필요합니다. 예시:

it 'migrates successfully' do
  # ... 마이그레이션 이전 기대사항

  migrate!

  # ... 마이그레이션 이후 기대사항
end

reversible_migration

change 또는 updown 후크 중 하나 또는 둘 다를 가진 마이그레이션을 테스트하기 위해 reversible_migration 헬퍼를 사용합니다. 이 헬퍼는 마이그레이션이 되돌아가고 어플리케이션 및 데이터의 상태가 최초에 실행됐을 때와 동일하게 되돌아갔는지를 테스트합니다.

  1. up 마이그레이션 전에 before 기대사항을 실행합니다.
  2. up 마이그레이션을 실행합니다.
  3. after 기대사항을 실행합니다.
  4. down 마이그레이션을 실행합니다.
  5. before 기대사항을 두 번째로 실행합니다.

예시:

reversible_migration do |migration|
  migration.before -> {
    # ... 마이그레이션 이전 기대사항
  }

  migration.after -> {
    # ... 마이그레이션 이후 기대사항
  }
end

포스트 배포 마이그레이션을 위한 커스텀 매처

우리는 spec/support/matchers/background_migrations_matchers.rb에 몇 가지 커스텀 매처를 가지고 있습니다. 이를 사용하여 포스트 배포 마이그레이션으로부터 올바르게 예약되었고 올바른 수의 인자를 받았는지를 검증할 수 있습니다.

모든 이 매처들은 내부 매처 be_background_migration_with_arguments를 사용하며, 이는 마이그레이션 클래스의 #perform 메서드가 제공된 인자를 수신할 때 크래쉬하지 않는지를 검증합니다.

be_scheduled_migration

예상된 클래스와 인자로 Sidekiq 작업이 큐에 들어간지 여부를 검증합니다.

이 매처는 헬퍼를 통해 직접적으로 작업을 예약하는 경우에 유용합니다.

# 마이그레이션
BackgroundMigrationWorker.perform_async('MigrationClass', args)

# 스펙
expect('MigrationClass').to be_scheduled_migration(*args)

be_scheduled_migration_with_multiple_args

예상된 클래스와 인자로 Sidekiq 작업이 큐에 들어간지 여부를 검증합니다.

이는 순서를 무시하고 배열 인자를 비교할 때 사용합니다.

# 마이그레이션
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 작업이 대기열에 들어갔는지 확인합니다.

이것은 또한 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 레코드가 생성되었는지 확인합니다.

*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
  ...

배경 마이그레이션 예약 테스트 예시

이를 테스트하기 위해서는 보통 다음을 해야 합니다:

  • 몇 가지 레코드 생성
  • 마이그레이션 실행
  • 예상된 작업이 올바른 레코드 세트, 올바른 일괄 크기, 간격 등으로 예약되었는지 확인

배경 마이그레이션의 동작은 배경 마이그레이션 클래스에 대한 별도의 테스트에서 검증되어야 합니다.

이 스펙은 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
  ...

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

이러한 테스트는 데이터베이스 트랜잭션 내에서 실행되지 않습니다. 삭제 데이터베이스 정리 전략을 사용하기 때문에 트랜잭션에 의존하지 마세요.

deletion_except_tables에서 시드 데이터를 변경하는 마이그레이션을 테스트할 때, 테스트가 트랜잭션 내에서 실행되고 데이터가 원래 값으로 롤백되도록 :migration_with_transaction 메타데이터를 추가할 수 있습니다.