debian-mirror-gitlab/doc/development/testing_guide/testing_migrations_guide.md

434 lines
14 KiB
Markdown
Raw Normal View History

2019-12-21 20:55:43 +05:30
---
2021-01-29 00:20:46 +05:30
stage: none
group: unassigned
2021-02-22 17:27:13 +05:30
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
2019-12-21 20:55:43 +05:30
type: reference
---
# Testing Rails migrations at GitLab
In order to reliably check Rails migrations, we need to test them against
a database schema.
## When to write a migration test
- Post migrations (`/db/post_migrate`) and background migrations
(`lib/gitlab/background_migration`) **must** have migration tests performed.
- If your migration is a data migration then it **must** have a migration test.
- Other migrations may have a migration test if necessary.
## How does it work?
Adding a `:migration` tag to a test signature enables some custom RSpec
`before` and `after` hooks in our
2020-04-08 14:13:33 +05:30
[`spec/support/migration.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/f81fa6ab1dd788b70ef44b85aaba1f31ffafae7d/spec/support/migration.rb)
2019-12-21 20:55:43 +05:30
to run.
2021-02-22 17:27:13 +05:30
A `before` hook reverts all migrations to the point that a migration
2019-12-21 20:55:43 +05:30
under test is not yet migrated.
2021-02-22 17:27:13 +05:30
In other words, our custom RSpec hooks finds a previous migration, and
2019-12-21 20:55:43 +05:30
migrate the database **down** to the previous migration version.
With this approach you can test a migration against a database schema.
2021-03-11 19:13:27 +05:30
An `after` hook migrates the database **up** and restores the latest
2019-12-21 20:55:43 +05:30
schema version, so that the process does not affect subsequent specs and
ensures proper isolation.
## Testing an `ActiveRecord::Migration` class
2021-12-11 22:18:48 +05:30
To test an `ActiveRecord::Migration` class (for example, a
2019-12-21 20:55:43 +05:30
regular migration `db/migrate` or a post-migration `db/post_migrate`), you
2021-02-22 17:27:13 +05:30
must load the migration file by using the `require_migration!` helper
2020-10-24 23:57:45 +05:30
method because it is not autoloaded by Rails.
Example:
2019-12-21 20:55:43 +05:30
```ruby
2020-10-24 23:57:45 +05:30
require 'spec_helper'
require_migration!
RSpec.describe ...
2019-12-21 20:55:43 +05:30
```
### Test helpers
2020-10-24 23:57:45 +05:30
#### `require_migration!`
2021-02-22 17:27:13 +05:30
Since the migration files are not autoloaded by Rails, you must manually
2020-10-24 23:57:45 +05:30
load the migration file. To do so, you can use the `require_migration!` helper method
2021-02-22 17:27:13 +05:30
which can automatically load the correct migration file based on the spec filename.
2020-10-24 23:57:45 +05:30
2021-12-11 22:18:48 +05:30
In GitLab 14.4 and later, you can use `require_migration!` to load migration files from spec files
that contain the schema version in the filename (for example,
`2021101412150000_populate_foo_column_spec.rb`).
2020-10-24 23:57:45 +05:30
2021-12-11 22:18:48 +05:30
```ruby
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe PopulateFooColumn do
...
end
```
In some cases, you must require multiple migration files to use them in your specs. Here, there's no
pattern between your spec file and the other migration file. You can provide the migration filename
like so:
2020-10-24 23:57:45 +05:30
```ruby
2021-12-11 22:18:48 +05:30
# frozen_string_literal: true
require 'spec_helper'
require_migration!
require_migration!('populate_bar_column')
RSpec.describe PopulateFooColumn do
...
end
2020-10-24 23:57:45 +05:30
```
2019-12-21 20:55:43 +05:30
#### `table`
Use the `table` helper to create a temporary `ActiveRecord::Base`-derived model
2020-04-08 14:13:33 +05:30
for a table. [FactoryBot](best_practices.md#factories)
2021-02-22 17:27:13 +05:30
**should not** be used to create data for migration specs because it relies on
application code which can change after the migration has run, and cause the test
to fail. For example, to create a record in the `projects` table:
2019-12-21 20:55:43 +05:30
```ruby
project = table(:projects).create!(id: 1, name: 'gitlab1', path: 'gitlab1')
```
#### `migrate!`
2021-02-22 17:27:13 +05:30
Use the `migrate!` helper to run the migration that is under test. It
runs the migration and bumps the schema version in the `schema_migrations`
2019-12-21 20:55:43 +05:30
table. It is necessary because in the `after` hook we trigger the rest of
the migrations, and we need to know where to start. Example:
```ruby
it 'migrates successfully' do
# ... pre-migration expectations
migrate!
# ... post-migration expectations
end
```
#### `reversible_migration`
Use the `reversible_migration` helper to test migrations with either a
2021-02-22 17:27:13 +05:30
`change` or both `up` and `down` hooks. This tests that the state of
2019-12-21 20:55:43 +05:30
the application and its data after the migration becomes reversed is the
same as it was before the migration ran in the first place. The helper:
1. Runs the `before` expectations before the **up** migration.
1. Migrates **up**.
1. Runs the `after` expectations.
1. Migrates **down**.
1. Runs the `before` expectations a second time.
Example:
```ruby
reversible_migration do |migration|
migration.before -> {
# ... pre-migration expectations
}
migration.after -> {
# ... post-migration expectations
}
end
```
2021-10-27 15:23:28 +05:30
### Custom matchers for post-deployment migrations
We have some custom matchers in
[`spec/support/matchers/background_migrations_matchers.rb`](https://gitlab.com/gitlab-org/gitlab/blob/v14.1.0-ee/spec/support/matchers/background_migrations_matchers.rb)
to verify background migrations were correctly scheduled from a post-deployment migration, and
receive the correct number of arguments.
All of them use the internal matcher `be_background_migration_with_arguments`, which verifies that
the `#perform` method on your migration class doesn't crash when receiving the provided arguments.
#### `be_scheduled_migration`
Verifies that a Sidekiq job was queued with the expected class and arguments.
This matcher usually makes sense if you're queueing jobs manually, rather than going through our helpers.
```ruby
# Migration
BackgroundMigrationWorker.perform_async('MigrationClass', args)
# Spec
expect('MigrationClass').to be_scheduled_migration(*args)
```
#### `be_scheduled_migration_with_multiple_args`
Verifies that a Sidekiq job was queued with the expected class and arguments.
This works the same as `be_scheduled_migration`, except that the order is ignored when comparing
array arguments.
```ruby
# 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`
Verifies that a Sidekiq job was queued with the expected delay, class, and arguments.
This can also be used with `queue_background_migration_jobs_by_range_at_intervals` and related helpers.
```ruby
# Migration
BackgroundMigrationWorker.perform_in(delay, 'MigrationClass', args)
# Spec
expect('MigrationClass').to be_scheduled_delayed_migration(delay, *args)
```
#### `have_scheduled_batched_migration`
Verifies that a `BatchedMigration` record was created with the expected class and arguments.
The `*args` are additional arguments passed to the `MigrationClass`, while `**kwargs` are any other
attributes to be verified on the `BatchedMigration` record (Example: `interval: 2.minutes`).
```ruby
# 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
)
```
### Examples of migration tests
Migration tests depend on what the migration does exactly, the most common types are data migrations and scheduling background migrations.
#### Example of a data migration test
2019-12-21 20:55:43 +05:30
This spec tests the
2021-12-11 22:18:48 +05:30
[`db/post_migrate/20200723040950_migrate_incident_issues_to_incident_type.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/db/post_migrate/20200723040950_migrate_incident_issues_to_incident_type.rb)
2019-12-21 20:55:43 +05:30
migration. You can find the complete spec in
2021-12-11 22:18:48 +05:30
[`spec/migrations/migrate_incident_issues_to_incident_type_spec.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/migrations/migrate_incident_issues_to_incident_type_spec.rb).
2019-12-21 20:55:43 +05:30
```ruby
2021-12-11 22:18:48 +05:30
# frozen_string_literal: true
2020-10-24 23:57:45 +05:30
2021-12-11 22:18:48 +05:30
require 'spec_helper'
2020-10-24 23:57:45 +05:30
require_migration!
2019-12-21 20:55:43 +05:30
2021-12-11 22:18:48 +05:30
RSpec.describe MigrateIncidentIssuesToIncidentType do
let(:migration) { described_class.new }
2019-12-21 20:55:43 +05:30
let(:projects) { table(:projects) }
2021-12-11 22:18:48 +05:30
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 }
2019-12-21 20:55:43 +05:30
before do
2021-12-11 22:18:48 +05:30
label_links.create!(target_id: incident_issue.id, label_id: label.id, target_type: 'Issue')
2019-12-21 20:55:43 +05:30
end
2021-12-11 22:18:48 +05:30
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)
2019-12-21 20:55:43 +05:30
2021-12-11 22:18:48 +05:30
expect(other_issue.reload.issue_type).to eql(issue_type)
end
2019-12-21 20:55:43 +05:30
end
2021-12-11 22:18:48 +05:30
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)
2019-12-21 20:55:43 +05:30
end
2021-12-11 22:18:48 +05:30
end
2019-12-21 20:55:43 +05:30
end
```
2021-10-27 15:23:28 +05:30
#### Example of a background migration scheduling test
To test these you usually have to:
- Create some records.
- Run the migration.
- Verify that the expected jobs were scheduled, with the correct set
of records, the correct batch size, interval, etc.
The behavior of the background migration itself needs to be verified in a [separate
test for the background migration class](#example-background-migration-test).
This spec tests the
[`db/post_migrate/20210701111909_backfill_issues_upvotes_count.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/v14.1.0-ee/db/post_migrate/20210701111909_backfill_issues_upvotes_count.rb)
post-deployment migration. You can find the complete spec in
[`spec/migrations/backfill_issues_upvotes_count_spec.rb`](https://gitlab.com/gitlab-org/gitlab/blob/v14.1.0-ee/spec/spec/migrations/backfill_issues_upvotes_count_spec.rb).
```ruby
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
```
2019-12-21 20:55:43 +05:30
## Testing a non-`ActiveRecord::Migration` class
To test a non-`ActiveRecord::Migration` test (a background migration),
2021-02-22 17:27:13 +05:30
you must manually provide a required schema version. Please add a
2020-04-08 14:13:33 +05:30
`schema` tag to a context that you want to switch the database schema within.
If not set, `schema` defaults to `:latest`.
2019-12-21 20:55:43 +05:30
Example:
```ruby
2020-04-08 14:13:33 +05:30
describe SomeClass, schema: 20170608152748 do
2019-12-21 20:55:43 +05:30
# ...
end
```
### Example background migration test
This spec tests the
2021-12-11 22:18:48 +05:30
[`lib/gitlab/background_migration/backfill_draft_status_on_merge_requests.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests.rb)
2019-12-21 20:55:43 +05:30
background migration. You can find the complete spec on
2021-12-11 22:18:48 +05:30
[`spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_spec.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_spec.rb)
2019-12-21 20:55:43 +05:30
```ruby
2021-12-11 22:18:48 +05:30
# frozen_string_literal: true
2019-12-21 20:55:43 +05:30
require 'spec_helper'
2021-12-11 22:18:48 +05:30
RSpec.describe Gitlab::BackgroundMigration::BackfillDraftStatusOnMergeRequests do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:merge_requests) { table(:merge_requests) }
2019-12-21 20:55:43 +05:30
2021-12-11 22:18:48 +05:30
let(:group) { namespaces.create!(name: 'gitlab', path: 'gitlab') }
let(:project) { projects.create!(namespace_id: group.id) }
2019-12-21 20:55:43 +05:30
2021-12-11 22:18:48 +05:30
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))
2019-12-21 20:55:43 +05:30
end
2021-12-11 22:18:48 +05:30
context "for MRs with #draft? == true titles but draft attribute false" do
let(:mr_ids) { merge_requests.all.collect(&:id) }
2019-12-21 20:55:43 +05:30
before do
2021-12-11 22:18:48 +05:30
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
2019-12-21 20:55:43 +05:30
end
2021-12-11 22:18:48 +05:30
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
2019-12-21 20:55:43 +05:30
2021-12-11 22:18:48 +05:30
it "marks successful slices as completed" do
expect(subject).to receive(:mark_job_as_succeeded).with(mr_ids.first, mr_ids.last)
2019-12-21 20:55:43 +05:30
2021-12-11 22:18:48 +05:30
subject.perform(mr_ids.first, mr_ids.last)
2019-12-21 20:55:43 +05:30
end
end
end
```
These tests do not run within a database transaction, as we use a deletion database
cleanup strategy. Do not depend on a transaction being present.