如何测试Rails迁移?

58

我想测试一下我写的迁移脚本后是否满足某些条件。目前最好的方法是什么?

具体来说,我写了一个迁移脚本,它向模型中添加了一个列,并给它赋了一个默认值。但是我忘记更新所有已有的该模型实例,以便其在新列上拥有默认值。我的所有现有测试都不会捕获到这个问题,因为它们都从一个全新的数据库开始并添加新数据,这些新数据将具有默认值。但是如果我推送到生产环境,我知道事情会出错,我希望我的测试能告诉我。

我找到了http://spin.atomicobject.com/2007/02/27/migration-testing-in-rails/,但没有尝试过。它非常古老。那还是最先进的方法吗?


这篇文章看起来很有用:http://blog.carbonfive.com/2011/01/27/start-testing-your-migrations-right-now/ - maahd
8个回答

21

Peter Marklund在这里提供了一个示例gist用于测试迁移:https://gist.github.com/700194(使用rspec)。

请注意,自从他的示例以来,迁移方式已更改为使用实例方法而不是类方法。

以下是摘要:

  1. 像往常一样创建迁移
  2. 创建文件以放置您的迁移测试。建议: test/unit/import_legacy_devices_migration_test.rbspec/migrations/import_legacy_devices_migration_spec.rb 注意:您可能需要显式加载迁移文件,因为Rails可能不会为您加载它。像这样的东西应该可以做到:require File.join(Rails.root, 'db', 'migrate', '20101110154036_import_legacy_devices')
  3. 迁移(如ruby中的所有内容)只是一个类。测试updown方法。如果逻辑复杂,请建议将逻辑位拆分为较小的方法,这样更易于测试。
  4. 在调用up之前,设置一些数据,就像在您的迁移之前一样,并断言其状态在后续的状态中所期望的一样。

希望这可以帮助您。

更新:发布后,我在我的博客上发布了一个示例迁移测试

更新:这里有一个测试迁移的想法,即使在开发后运行它们。

编辑:我已将我的概念验证更新为使用来自我的博客文章的人为例子的完整规范文件。

# spec/migrations/add_email_at_utc_hour_to_users_spec.rb
require 'spec_helper'

migration_file_name = Dir[Rails.root.join('db/migrate/*_add_email_at_utc_hour_to_users.rb')].first
require migration_file_name


describe AddEmailAtUtcHourToUsers do

  # This is clearly not very safe or pretty code, and there may be a
  # rails api that handles this. I am just going for a proof of concept here.
  def migration_has_been_run?(version)
    table_name = ActiveRecord::Migrator.schema_migrations_table_name
    query = "SELECT version FROM %s WHERE version = '%s'" % [table_name, version]
    ActiveRecord::Base.connection.execute(query).any?
  end

  let(:migration) { AddEmailAtUtcHourToUsers.new }


  before do
    # You could hard-code the migration number, or find it from the filename...
    if migration_has_been_run?('20120425063641')
      # If this migration has already been in our current database, run down first
      migration.down
    end
  end


  describe '#up' do
    before { migration.up; User.reset_column_information }

    it 'adds the email_at_utc_hour column' do
      User.columns_hash.should have_key('email_at_utc_hour')
    end
  end
end

2
这听起来像是一个答案的一部分,但通常情况下,设置会从最新的开发数据库克隆数据库结构,而在这里,我们需要从早期状态开始 - 理想情况下是从未运行过新迁移(而不是已经运行并还原)。有什么方便的方法将测试数据库置于适当的状态? - Steve Jorgensen
@SteveJorgensen 这是一个好问题。当我编写迁移测试时,通常会使用TDD进行编写,然后在实际运行迁移后将其删除。它们不能继续作为回归测试存在,因为正如您所说,一旦迁移完成,测试反映了新的迁移状态,迁移测试失败。非常抱歉,我从未处理过像那样设置数据库的情况。不过我有一个想法,一旦我有机会尝试一下,就会更新我的答案。 - Amiel Martin
一些谷歌搜索给我带来了一个有趣且相关的文章:http://spin.atomicobject.com/2007/02/27/migration-testing-in-rails/。 - Steve Jorgensen
4
注意 - 如果您正在使用可逆迁移(reversible migration)和 def change...,则必须将 migration.downmigration.up 替换为 migration.migrate(:down)migration.migrate(:up)。这对于向上和向下迁移均有效。 - Tim Diggins

8

我只需创建一个类的实例,然后在其上调用updown

例如:

require Rails.root.join(
  'db',
  'migrate',
  '20170516191414_create_identities_ad_accounts_from_ad_account_identity'
)

describe CreateIdentitiesAdAccountsFromAdAccountIdentity do
  subject(:migration) { described_class.new }

  it 'properly creates identities_ad_accounts from ad account identities' do
    create_list :ad_account, 3, identity_id: create(:identity).id

    expect { suppress_output { migration.up } }
      .to change { IdentitiesAdAccount.count }.from(0).to(3)
  end
end

3
我做了一个迁移,向模型中添加了一列,并给它设置了默认值。但是我忘记更新所有已存在的该模型实例,使其具有新列的默认值。
根据这个声明,您只是在尝试测试“旧”模型是否具有正确的默认值,对吗?
理论上,您正在测试Rails是否起作用。也就是说,“Rails是否会为新添加的列设置默认值”。
在您的数据库中,“旧”记录将包含添加列并设置默认值的内容。
因此,您不需要更新其他记录以反映默认设置。从理论上讲,没有什么需要测试的,因为Rails已经为您测试过了。最后,使用默认值的原因是您无需更新之前的实例以使用该默认值,对吧?

2

注意:这个回答可能并不直接回答上面的问题。我写这篇文章是为了帮助想要知道如何在Rails中编写迁移测试的读者。

这就是我的方法:


第一步

你需要配置RSpec来使用DatabaseCleaner

# spec/support/db_cleaner.rb
RSpec.configure do |config|
  config.around(:each) do |example|
    unless example.metadata[:manual_cleaning]
      DatabaseCleaner.strategy = :transaction
      DatabaseCleaner.cleaning { example.run }
    else
      example.run
    end
  end
end

这将以超快的速度在transaction模式下运行所有示例。另外,您需要在truncation模式下运行迁移测试,因为您需要实际访问数据库。

注意:如果您正在使用DatabaseCleanertruncation策略,则可能不需要执行上述操作。


步骤2

现在,您可以选择是否要使用manual_cleaning语句来选择特定示例或示例组是否使用transaction,如下所示。

# spec/migrations/add_shipping_time_settings_spec.rb
require 'spec_helper'
require_relative  '../../db/migrate/20200505100506_add_shipping_time_settings.rb'

describe AddShippingTimeSettings, manual_cleaning: true do
  before do
    DatabaseCleaner.strategy = :truncation
    DatabaseCleaner.clean # Cleaning DB manually before suite
  end

  describe '#up' do
    context 'default values in database' do
      before do
        AddShippingTimeSettings.new.up
      end

      it 'creates required settings with default values' do
        data = Setting.where(code: AddShippingTimeSettings::SHIPPING_TIMES)
        expect(data.count).to eq(AddShippingTimeSettings::SHIPPING_TIMES.count)
        expect(data.map(&:value).uniq).to eq(['7'])
      end
    end
  end

  describe '#down' do
    context 'Clean Up' do
      before do
        AddShippingTimeSettings.new.up
        AddShippingTimeSettings.new.down
      end

      it 'cleans up the mess' do
        data = Setting.where(code: AddShippingTimeSettings::SHIPPING_TIMES)
        expect(data.count).to eq(0)
      end
    end
  end
end

1
这也许不是最符合 Rails 的答案; 限制你的数据库。如果你声明了列为not null(在 Rails 迁移中为 null: false),那么数据库就不会让你忘记提供默认值。关系型数据库非常擅长强制执行约束条件。如果你养成添加它们的习惯,就可以保证数据的质量。想象一下,如果你在生产环境中已经存在某些数据,而现在你要添加一个存在性验证,但这个验证会失败。首先,只有当用户尝试编辑数据时,验证才会运行,当它运行时,用户可能不清楚导致错误的原因,因为此时他们可能不关心该特定值。其次,你的 UI 可能期望该值存在(毕竟你的验证“保证”它),然后你会在凌晨2点收到有关意外 nil 的页面。如果你在添加验证时将列约束为not null,数据库将回溯所有现有数据,并强制你在迁移完成之前修复它们。虽然在这个例子中我使用了not null,但对于唯一性验证和其他任何可以用约束表达的东西同样适用。

1

我不了解Rails,但我认为无论使用什么工具,方法都是相同的。

  • 确保在版本控制中适当地标记/标签化已部署的数据库脚本的版本
  • 基于此,您至少需要三个脚本:一个从头开始创建旧版本的脚本(1),一个从头开始创建新版本的脚本(2)和一个从旧版本创建新版本的脚本(3)。
  • 创建两个db实例/模式。在其中一个运行脚本2,在另一个运行脚本1,然后是脚本3
  • 使用针对数据字典的SQL查询比较两个数据库的结果。

为了测试实际数据的影响,请在执行脚本2和1到3之间将测试数据加载到数据库中。再次运行SQL查询,比较结果。


0

您可以考虑使用生产数据的副本(例如yaml_db)来针对特定设置运行测试套件的隔离部分。

这有点元,如果您知道新迁移可能存在的问题,最好是增强它们以满足您的特定需求,但这是可能的。


-1
describe 'some_migration' do
  it 'does certain things' do
    context = ActiveRecord::Base.connection.migration_context
    # The version right before some_migration
    version = 20201207234341
    # Rollback to right before some_migration
    context.down(version)

    set_up_some_data

    context.migrate
    # Or if you prefer:
    # context.forward(1)

    expect(certain_things).to be(true)
  end
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接