1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Automatically maintain test database schema

* Move check from generated helper to test_help.rb, so that all
  applications can benefit
* Rather than just raising when the test schema has pending migrations,
  try to load in the schema and only raise if there are pending
  migrations afterwards
* Opt out of the check by setting
  config.active_record.maintain_test_schema = false
* Deprecate db:test:* tasks. The test helper is now fully responsible
  for maintaining the test schema, so we don't need rake tasks for this.
  This is also a speed improvement since we're no longer reloading the
  test database on every call to "rake test".
This commit is contained in:
Jon Leighton 2013-12-29 11:05:04 +00:00
parent a1d0c0fa3d
commit ff7ab3bc78
17 changed files with 173 additions and 58 deletions

View file

@ -1,3 +1,12 @@
* Since the `test_help.rb` in Railties now automatically maintains
your test schema, the `rake db:test:*` tasks are deprecated. This
doesn't stop you manually running other tasks on your test database
if needed:
rake db:schema:load RAILS_ENV=test
*Jon Leighton*
* Fix presence validator for association when the associated record responds to `to_a`. * Fix presence validator for association when the associated record responds to `to_a`.
*gmarik* *gmarik*
@ -54,6 +63,8 @@
* Deprecated use of string argument as a configuration lookup in * Deprecated use of string argument as a configuration lookup in
`ActiveRecord::Base.establish_connection`. Instead, a symbol must be given. `ActiveRecord::Base.establish_connection`. Instead, a symbol must be given.
* Deprecated use of string argument as a configuration lookup in `ActiveRecord::Base.establish_connection`. Instead, a symbol must be given.
*José Valim* *José Valim*
* Fixed `update_column`, `update_columns`, and `update_all` to correctly serialize * Fixed `update_column`, `update_columns`, and `update_all` to correctly serialize

View file

@ -69,6 +69,9 @@ module ActiveRecord
mattr_accessor :timestamped_migrations, instance_writer: false mattr_accessor :timestamped_migrations, instance_writer: false
self.timestamped_migrations = true self.timestamped_migrations = true
# :nodoc:
mattr_accessor :maintain_test_schema, instance_accessor: false
def self.disable_implicit_join_references=(value) def self.disable_implicit_join_references=(value)
ActiveSupport::Deprecation.warn("Implicit join references were removed with Rails 4.1." \ ActiveSupport::Deprecation.warn("Implicit join references were removed with Rails 4.1." \
"Make sure to remove this configuration because it does nothing.") "Make sure to remove this configuration because it does nothing.")

View file

@ -389,6 +389,19 @@ module ActiveRecord
raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration? raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?
end end
def load_schema_if_pending!
if ActiveRecord::Migrator.needs_migration?
ActiveRecord::Tasks::DatabaseTasks.load_schema
check_pending!
end
end
def maintain_test_schema! # :nodoc:
if ActiveRecord::Base.maintain_test_schema
suppress_messages { load_schema_if_pending! }
end
end
def method_missing(name, *args, &block) # :nodoc: def method_missing(name, *args, &block) # :nodoc:
(delegate || superclass.delegate).send(name, *args, &block) (delegate || superclass.delegate).send(name, *args, &block)
end end

View file

@ -31,22 +31,15 @@ module ActiveRecord
config.active_record.use_schema_cache_dump = true config.active_record.use_schema_cache_dump = true
config.active_record.maintain_test_schema = true
config.eager_load_namespaces << ActiveRecord config.eager_load_namespaces << ActiveRecord
rake_tasks do rake_tasks do
require "active_record/base" require "active_record/base"
ActiveRecord::Tasks::DatabaseTasks.seed_loader = Rails.application
ActiveRecord::Tasks::DatabaseTasks.env = Rails.env
namespace :db do namespace :db do
task :load_config do task :load_config do
ActiveRecord::Tasks::DatabaseTasks.db_dir = Rails.application.config.paths["db"].first
ActiveRecord::Tasks::DatabaseTasks.migrations_paths = Rails.application.paths['db/migrate'].to_a
ActiveRecord::Tasks::DatabaseTasks.fixtures_path = File.join Rails.root, 'test', 'fixtures'
ActiveRecord::Tasks::DatabaseTasks.root = Rails.root
configuration = if ENV["DATABASE_URL"] configuration = if ENV["DATABASE_URL"]
{ Rails.env => ENV["DATABASE_URL"] } { Rails.env => ENV["DATABASE_URL"] }
else else

View file

@ -234,9 +234,7 @@ db_namespace = namespace :db do
desc 'Load a schema.rb file into the database' desc 'Load a schema.rb file into the database'
task :load => [:environment, :load_config] do task :load => [:environment, :load_config] do
file = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb') ActiveRecord::Tasks::DatabaseTasks.load_schema(:ruby, ENV['SCHEMA'])
ActiveRecord::Tasks::DatabaseTasks.check_schema_file(file)
load(file)
end end
task :load_if_ruby => ['db:create', :environment] do task :load_if_ruby => ['db:create', :environment] do
@ -281,10 +279,7 @@ db_namespace = namespace :db do
# desc "Recreate the databases from the structure.sql file" # desc "Recreate the databases from the structure.sql file"
task :load => [:environment, :load_config] do task :load => [:environment, :load_config] do
filename = ENV['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") ActiveRecord::Tasks::DatabaseTasks.load_schema(:sql, ENV['DB_STRUCTURE'])
ActiveRecord::Tasks::DatabaseTasks.check_schema_file(filename)
current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename)
end end
task :load_if_sql => ['db:create', :environment] do task :load_if_sql => ['db:create', :environment] do
@ -294,8 +289,15 @@ db_namespace = namespace :db do
namespace :test do namespace :test do
task :deprecated do
Rake.application.top_level_tasks.grep(/^db:test:/).each do |task|
$stderr.puts "WARNING: #{task} is deprecated. The Rails test helper now maintains " \
"your test schema automatically, see the release notes for details."
end
end
# desc "Recreate the test database from the current schema" # desc "Recreate the test database from the current schema"
task :load => 'db:test:purge' do task :load => %w(db:test:deprecated db:test:purge) do
case ActiveRecord::Base.schema_format case ActiveRecord::Base.schema_format
when :ruby when :ruby
db_namespace["test:load_schema"].invoke db_namespace["test:load_schema"].invoke
@ -305,7 +307,7 @@ db_namespace = namespace :db do
end end
# desc "Recreate the test database from an existent schema.rb file" # desc "Recreate the test database from an existent schema.rb file"
task :load_schema => 'db:test:purge' do task :load_schema => %w(db:test:deprecated db:test:purge) do
begin begin
should_reconnect = ActiveRecord::Base.connection_pool.active_connection? should_reconnect = ActiveRecord::Base.connection_pool.active_connection?
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
@ -319,7 +321,7 @@ db_namespace = namespace :db do
end end
# desc "Recreate the test database from an existent structure.sql file" # desc "Recreate the test database from an existent structure.sql file"
task :load_structure => 'db:test:purge' do task :load_structure => %w(db:test:deprecated db:test:purge) do
begin begin
ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test']) ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test'])
db_namespace["structure:load"].invoke db_namespace["structure:load"].invoke
@ -329,7 +331,7 @@ db_namespace = namespace :db do
end end
# desc "Recreate the test database from a fresh schema" # desc "Recreate the test database from a fresh schema"
task :clone => :environment do task :clone => %w(db:test:deprecated environment) do
case ActiveRecord::Base.schema_format case ActiveRecord::Base.schema_format
when :ruby when :ruby
db_namespace["test:clone_schema"].invoke db_namespace["test:clone_schema"].invoke
@ -339,18 +341,18 @@ db_namespace = namespace :db do
end end
# desc "Recreate the test database from a fresh schema.rb file" # desc "Recreate the test database from a fresh schema.rb file"
task :clone_schema => ["db:schema:dump", "db:test:load_schema"] task :clone_schema => %w(db:test:deprecated db:schema:dump db:test:load_schema)
# desc "Recreate the test database from a fresh structure.sql file" # desc "Recreate the test database from a fresh structure.sql file"
task :clone_structure => [ "db:structure:dump", "db:test:load_structure" ] task :clone_structure => %w(db:structure:dump db:test:load_structure)
# desc "Empty the test database" # desc "Empty the test database"
task :purge => [:environment, :load_config] do task :purge => %w(db:test:deprecated environment load_config) do
ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test'] ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test']
end end
# desc 'Check for pending migrations and load the test schema' # desc 'Check for pending migrations and load the test schema'
task :prepare => [:environment, :load_config] do task :prepare => %w(db:test:deprecated environment load_config) do
unless ActiveRecord::Base.configurations.blank? unless ActiveRecord::Base.configurations.blank?
db_namespace['test:load'].invoke db_namespace['test:load'].invoke
end end
@ -385,5 +387,3 @@ namespace :railties do
end end
end end
end end
task 'test:prepare' => ['db:test:prepare', 'db:test:load', 'db:abort_if_pending_migrations']

View file

@ -36,9 +36,8 @@ module ActiveRecord
module DatabaseTasks module DatabaseTasks
extend self extend self
attr_writer :current_config attr_writer :current_config, :db_dir, :migrations_paths, :fixtures_path, :root, :env, :seed_loader
attr_accessor :database_configuration, :migrations_paths, :seed_loader, :db_dir, attr_accessor :database_configuration
:fixtures_path, :env, :root
LOCAL_HOSTS = ['127.0.0.1', 'localhost'] LOCAL_HOSTS = ['127.0.0.1', 'localhost']
@ -51,6 +50,30 @@ module ActiveRecord
register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks) register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks)
register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks) register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks)
def db_dir
@db_dir ||= Rails.application.config.paths["db"].first
end
def migrations_paths
@migrations_paths ||= Rails.application.paths['db/migrate'].to_a
end
def fixtures_path
@fixtures_path ||= File.join(root, 'test', 'fixtures')
end
def root
@root ||= Rails.root
end
def env
@env ||= Rails.env
end
def seed_loader
@seed_loader ||= Rails.application
end
def current_config(options = {}) def current_config(options = {})
options.reverse_merge! :env => env options.reverse_merge! :env => env
if options.has_key?(:config) if options.has_key?(:config)
@ -133,6 +156,21 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename) class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename)
end end
def load_schema(format = ActiveRecord::Base.schema_format, file = nil)
case format
when :ruby
file ||= File.join(db_dir, "schema.rb")
check_schema_file(file)
load(file)
when :sql
file ||= File.join(db_dir, "structure.sql")
check_schema_file(file)
structure_load(current_config, file)
else
raise ArgumentError, "unknown format #{format.inspect}"
end
end
def check_schema_file(filename) def check_schema_file(filename)
unless File.exist?(filename) unless File.exist?(filename)
message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.} message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.}

View file

@ -281,6 +281,13 @@ for detailed changes.
* Add `Application#message_verifier` method to return a message * Add `Application#message_verifier` method to return a message
verifier. ([Pull Request](https://github.com/rails/rails/pull/12995)) verifier. ([Pull Request](https://github.com/rails/rails/pull/12995))
* The `test_help.rb` file which is required by the default generated test
helper will automatically keep your test database up-to-date with
`db/schema.rb` (or `db/structure.sql`). It raises an error if
reloading the schema does not resolve all pending migrations. Opt out
with `config.active_record.maintain_test_schema = false`. ([Pull
Request](https://github.com/rails/rails/pull/13528))
Action Pack Action Pack
----------- -----------
@ -421,6 +428,10 @@ for detailed changes.
* Deprecated `ConnectionAdapters::SchemaStatements#distinct`, * Deprecated `ConnectionAdapters::SchemaStatements#distinct`,
as it is no longer used by internals. ([Pull Request](https://github.com/rails/rails/pull/10556)) as it is no longer used by internals. ([Pull Request](https://github.com/rails/rails/pull/10556))
* Deprecated `rake db:test:*` tasks as the test database is now
automatically maintained. See railties release notes. ([Pull
Request](https://github.com/rails/rails/pull/13528))
### Notable changes ### Notable changes
* Added `ActiveRecord::Base.to_param` for convenient "pretty" URLs derived from * Added `ActiveRecord::Base.to_param` for convenient "pretty" URLs derived from

View file

@ -290,6 +290,8 @@ All these configuration options are delegated to the `I18n` library.
* `config.active_record.attribute_types_cached_by_default` sets the attribute types that `ActiveRecord::AttributeMethods` will cache by default on reads. The default is `[:datetime, :timestamp, :time, :date]`. * `config.active_record.attribute_types_cached_by_default` sets the attribute types that `ActiveRecord::AttributeMethods` will cache by default on reads. The default is `[:datetime, :timestamp, :time, :date]`.
* `config.active_record.maintain_test_schema` is a boolean value which controls whether Active Record should try to keep your test database schema up-to-date with `db/schema.rb` (or `db/structure.sql`) when you run your tests. The default is true.
The MySQL adapter adds one additional configuration option: The MySQL adapter adds one additional configuration option:
* `ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns in a MySQL database to be booleans and is true by default. * `ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns in a MySQL database to be booleans and is true by default.

View file

@ -224,7 +224,6 @@ and migrating the database. First, run:
```bash ```bash
$ cd test/dummy $ cd test/dummy
$ rake db:migrate $ rake db:migrate
$ rake db:test:prepare
``` ```
While you are here, change the Hickwall and Wickwall models so that they know that they are supposed to act While you are here, change the Hickwall and Wickwall models so that they know that they are supposed to act

View file

@ -211,31 +211,9 @@ This line of code is called an _assertion_. An assertion is a line of code that
Every test contains one or more assertions. Only when all the assertions are successful will the test pass. Every test contains one or more assertions. Only when all the assertions are successful will the test pass.
### Preparing your Application for Testing ### Maintaining the test database schema
Before you can run your tests, you need to ensure that the test database structure is current. For this you can use the following rake commands: In order to run your tests, your test database will need to have the current structure. The test helper checks whether your test database has any pending migrations. If so, it will try to load your `db/schema.rb` or `db/structure.sql` into the test database. If migrations are still pending, an error will be raised.
```bash
$ rake db:migrate
...
$ rake db:test:load
```
The `rake db:migrate` above runs any pending migrations on the _development_ environment and updates `db/schema.rb`. The `rake db:test:load` recreates the test database from the current `db/schema.rb`. On subsequent attempts, it is a good idea to first run `db:test:prepare`, as it first checks for pending migrations and warns you appropriately.
NOTE: `db:test:prepare` will fail with an error if `db/schema.rb` doesn't exist.
#### Rake Tasks for Preparing your Application for Testing
| Tasks | Description |
| ------------------------------ | ------------------------------------------------------------------------- |
| `rake db:test:clone` | Recreate the test database from the current environment's database schema |
| `rake db:test:clone_structure` | Recreate the test database from the development structure |
| `rake db:test:load` | Recreate the test database from the current `schema.rb` |
| `rake db:test:prepare` | Check for pending migrations and load the test schema |
| `rake db:test:purge` | Empty the test database. |
TIP: You can see all these rake tasks and their descriptions by running `rake --tasks --describe`
### Running Tests ### Running Tests

View file

@ -91,6 +91,13 @@ secrets, you need to:
5. Restart your server. 5. Restart your server.
### Changes to test helper
If your test helper contains a call to
`ActiveRecord::Migration.check_pending!` this can be removed. The check
is now done automatically when you `require 'test_help'`, although
leaving this line in your helper is not harmful in any way.
### Changes in JSON handling ### Changes in JSON handling
There are a few major changes related to JSON handling in Rails 4.1. There are a few major changes related to JSON handling in Rails 4.1.

View file

@ -1,3 +1,9 @@
* `test_help.rb` now automatically checks/maintains your test datbase
schema. (Use `config.active_record.maintain_test_schema = false` to
disable.)
*Jon Leighton*
* Configure `secrets.yml` and `database.yml` to read configuration * Configure `secrets.yml` and `database.yml` to read configuration
from the system environment by default for production. from the system environment by default for production.

View file

@ -4,8 +4,6 @@ require 'rails/test_help'
class ActiveSupport::TestCase class ActiveSupport::TestCase
<% unless options[:skip_active_record] -%> <% unless options[:skip_active_record] -%>
ActiveRecord::Migration.check_pending!
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
# #
# Note: You'll currently still have to declare fixtures explicitly in integration tests # Note: You'll currently still have to declare fixtures explicitly in integration tests

View file

@ -15,6 +15,8 @@ if ENV["BACKTRACE"].nil?
end end
if defined?(ActiveRecord::Base) if defined?(ActiveRecord::Base)
ActiveRecord::Migration.maintain_test_schema!
class ActiveSupport::TestCase class ActiveSupport::TestCase
include ActiveRecord::TestFixtures include ActiveRecord::TestFixtures
self.fixture_path = "#{Rails.root}/test/fixtures/" self.fixture_path = "#{Rails.root}/test/fixtures/"

View file

@ -174,6 +174,15 @@ module ApplicationTests
require "#{app_path}/config/environment" require "#{app_path}/config/environment"
db_test_load_structure db_test_load_structure
end end
test 'db:test deprecation' do
require "#{app_path}/config/environment"
Dir.chdir(app_path) do
output = `bundle exec rake db:migrate db:test:prepare 2>&1`
assert_equal "WARNING: db:test:prepare is deprecated. The Rails test helper now maintains " \
"your test schema automatically, see the release notes for details.\n", output
end
end
end end
end end
end end

View file

@ -187,7 +187,7 @@ module ApplicationTests
def test_scaffold_tests_pass_by_default def test_scaffold_tests_pass_by_default
output = Dir.chdir(app_path) do output = Dir.chdir(app_path) do
`rails generate scaffold user username:string password:string; `rails generate scaffold user username:string password:string;
bundle exec rake db:migrate db:test:clone test` bundle exec rake db:migrate test`
end end
assert_match(/7 runs, 13 assertions, 0 failures, 0 errors/, output) assert_match(/7 runs, 13 assertions, 0 failures, 0 errors/, output)
@ -197,7 +197,7 @@ module ApplicationTests
def test_scaffold_with_references_columns_tests_pass_by_default def test_scaffold_with_references_columns_tests_pass_by_default
output = Dir.chdir(app_path) do output = Dir.chdir(app_path) do
`rails generate scaffold LineItems product:references cart:belongs_to; `rails generate scaffold LineItems product:references cart:belongs_to;
bundle exec rake db:migrate db:test:clone test` bundle exec rake db:migrate test`
end end
assert_match(/7 runs, 13 assertions, 0 failures, 0 errors/, output) assert_match(/7 runs, 13 assertions, 0 failures, 0 errors/, output)

View file

@ -67,10 +67,55 @@ module ApplicationTests
assert_match %r{/app/test/unit/failing_test\.rb}, output assert_match %r{/app/test/unit/failing_test\.rb}, output
end end
test "migrations" do
output = script('generate model user name:string')
version = output.match(/(\d+)_create_users\.rb/)[1]
app_file 'test/models/user_test.rb', <<-RUBY
require 'test_helper'
class UserTest < ActiveSupport::TestCase
test "user" do
User.create! name: "Jon"
end
end
RUBY
app_file 'db/schema.rb', ''
assert_unsuccessful_run "models/user_test.rb", "Migrations are pending"
app_file 'db/schema.rb', <<-RUBY
ActiveRecord::Schema.define(version: #{version}) do
create_table :users do |t|
t.string :name
end
end
RUBY
app_file 'config/initializers/disable_maintain_test_schema.rb', <<-RUBY
Rails.application.config.active_record.maintain_test_schema = false
RUBY
assert_unsuccessful_run "models/user_test.rb", "Could not find table 'users'"
File.delete "#{app_path}/config/initializers/disable_maintain_test_schema.rb"
result = assert_successful_test_run('models/user_test.rb')
assert !result.include?("create_table(:users)")
end
private private
def assert_unsuccessful_run(name, message)
result = run_test_file(name)
assert_not_equal 0, $?.to_i
assert result.include?(message)
result
end
def assert_successful_test_run(name) def assert_successful_test_run(name)
result = run_test_file(name) result = run_test_file(name)
assert_equal 0, $?.to_i, result assert_equal 0, $?.to_i, result
result
end end
def run_test_file(name, options = {}) def run_test_file(name, options = {})
@ -83,7 +128,7 @@ module ApplicationTests
env["RUBYLIB"] = $:.join(':') env["RUBYLIB"] = $:.join(':')
Dir.chdir(app_path) do Dir.chdir(app_path) do
`#{env_string(env)} #{Gem.ruby} #{args.join(' ')}` `#{env_string(env)} #{Gem.ruby} #{args.join(' ')} 2>&1`
end end
end end