【问题标题】:How do I test Rails migrations?如何测试 Rails 迁移?
【发布时间】:2011-05-21 01:24:58
【问题描述】:

我想在运行我编写的迁移后测试某些条件是否成立。目前最好的方法是什么?

为了具体化:我做了一个迁移,将一列添加到模型中,并给它一个默认值。但是我忘记更新该模型的所有预先存在的实例以使新列具有该默认值。我现有的测试都无法捕捉到这一点,因为它们都从一个新数据库开始并添加新数据,这些数据将具有默认值。但如果我推动生产,我知道事情会失败,我希望我的测试能告诉我这一点。

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

【问题讨论】:

标签: ruby-on-rails database testing


【解决方案1】:

Peter Marklund 在这里有一个测试迁移的示例要点: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 之前,请设置一些迁移前的数据,并断言它的状态是您之后所期望的。

我希望这会有所帮助。

更新:自从发布此内容后,我在博客上发布了example migration test

更新:这是一个测试迁移的想法,即使它们已经在开发中运行。

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

# 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

【讨论】:

  • 这听起来像是答案的一部分,但通常情况下,设置会从最新的开发数据库中克隆数据库结构,这里我们需要从较早的状态开始——理想情况下,新的迁移从未运行过(与运行和恢复相反)。让测试数据库进入适当状态的便捷方法是什么?
  • @SteveJorgensen 这是个好问题。当我编写迁移测试时,我通常对它们进行 TDD,然后在我实际运行迁移后删除它们。他们不会继续作为回归测试继续进行,因为正如您所说,一旦迁移,测试就会反映新的迁移状态,迁移测试会失败。很抱歉,我从来没有处理过这样设置数据库的问题。不过我有一个想法,一旦我有机会修改它,我会更新我的答案。
  • 一些 Google 搜索让我看到了一篇有趣的相关文章:spin.atomicobject.com/2007/02/27/migration-testing-in-rails
  • 注意 - 如果您使用带有def change... 的可逆迁移,那么您必须将migration.downmigration.up 替换为migration.migrate(:down)migration.migrate(:up)。这也应该适用于上下。
【解决方案2】:

我只是创建了一个类的实例,然后在上面调用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 已经为你测试过了。最后,使用默认值的原因是您不必更新以前的实例即可使用该默认值,对吧?

    【讨论】:

      【解决方案4】:

      注意:此答案可能实际上并非针对上述问题。我写这篇文章是为了了解如何在 Rails 中编写迁移测试的观众。

      我就是这样做的


      步骤 1

      您需要配置 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 模式下运行迁移测试,因为您需要进行实际的数据库命中。

      注意:如果您使用truncation 作为DatabaseCleaner 的策略,则可能不需要执行上述操作。


      第二步

      现在,您可以使用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
      

      【讨论】:

        【解决方案5】:

        我不了解 Rails,但我认为独立于工具的方法是相同的 我使用以下方法:

        • 确保已部署的数据库脚本版本在版本控制中得到适当的标记/标记
        • 基于此,您至少需要三个脚本:一个从头开始创建旧版本的脚本 (1)、一个从头开始创建新版本的脚本 (2) 和一个从旧版本创建新版本的脚本(3)。
        • 创建两个数据库实例/架构。在一个运行脚本 2 中,在另一个运行脚本 1 后跟脚本 3
        • 比较两个数据库中的结果,对数据字典使用 sql 查询。

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

        【讨论】:

          【解决方案6】:

          这可能不是最 Railsy 的答案;约束你的数据库。

          如果您声明了您的列 not null(在 Rails 迁移中为 null: false),数据库不会让您忘记提供默认值。

          关系数据库非常擅长执行约束。如果您养成添加它们的习惯,则可以保证数据的质量。

          想象一下,如果您在生产中已经存在某些数据之后添加存在验证,那么验证将失败。首先,在用户尝试编辑数据之前,验证不会运行,并且当它这样做时,用户可能不清楚导致错误的原因,因为他们此时可能不关心该特定值。其次,您的 UI 可能期望该值存在(在您的所有验证“保证”它之后),您最终会在凌晨 2 点获得一个关于意外 nil 的页面。如果您在添加验证时将该列限制为not null,则数据库将回检所有现有数据并强制您在迁移完成之前对其进行修复。

          虽然我在此示例中使用 not null,但对于唯一性验证以及您可以通过约束表达的任何其他内容也是如此。

          【讨论】:

            【解决方案7】:

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

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

            【讨论】:

              【解决方案8】:
              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
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2012-02-05
                • 1970-01-01
                • 2015-12-25
                • 2014-06-13
                • 1970-01-01
                • 2021-02-02
                • 1970-01-01
                相关资源
                最近更新 更多