Feature: Add Rails generator and rake task support for creating data migration test coverage#355
Feature: Add Rails generator and rake task support for creating data migration test coverage#355joshmfrankel wants to merge 8 commits intoilyakatz:mainfrom
Conversation
…ge on data migrations * New Rake task `rake data:tests:setup` * New Config setting `config.test_support = true/false` (false by default)
| module Helpers | ||
| class InferTestSuiteType | ||
| def call | ||
| if File.exist?(Rails.root.join('spec', 'spec_helper.rb')) |
There was a problem hiding this comment.
We could perform add to the conditionals here for Rails.configuration.generators.options[:rails][:test_framework] == :rspec and Rails.configuration.generators.options[:rails][:test_framework] == :test_unit respectively to look for both installation and configuration.
There was a problem hiding this comment.
I like the idea of getting the test framework from Rails's config. I've worked on projects where both rspec and minitest were used (migration happening but not over and very little bandwidth to finish it) and I think its easier for us to maintain compatibility with the configuration over the presence of files?
Perhaps adding a config line would work too, but I'd imagine projects running on sinatra for example to maybe not follow the conventions set elsewhere?
There was a problem hiding this comment.
I added a simple configuration value to early return when set OR fallback to the inference logic.
| module DataMigrate | ||
| module Tasks | ||
| class SetupTests | ||
| INJECTION_MATCHER = Regexp.new(/require_relative ["|']\.\.\/config\/environment["|']/) |
There was a problem hiding this comment.
Had mixed feeling on this. Basically, I want to find a target phrase in either the rails_helper.rb or test_helper.rb. It seemed best to inject the require loop https://github.com/ilyakatz/data-migrate/pull/355/files#diff-ffd91a3de47aa2b12e05f99c726f74f27c7181b3f2f987c24aa9af32f72ea2acR57 after the environment was loaded. Open to other suggestions.
The setup task is also optional. You could just individually require files per data migration test file.
| def test_helper_file_path | ||
| case DataMigrate::Helpers::InferTestSuiteType.new.call | ||
| when :rspec | ||
| Rails.root.join('spec', 'rails_helper.rb') |
There was a problem hiding this comment.
rails_helper.rb contains the injection target BUT DataMigrate::Helpers::InferTestSuiteType checks spec_helper.rb (see: https://github.com/ilyakatz/data-migrate/pull/355/files#diff-e3eb6de888eae80ac87faa33be0419ff8abdc2bd7d14e23c243304834d53604eR5). I'm thinking these maybe should match in their conditional checks. Thoughts?
There was a problem hiding this comment.
I think if you were to remove the lines added to inject the require statement, this all disappears. It wouldn't be as full-fledged as this solution but avoids a bunch of complexity which users can just replace if they wished to do so.
A question that just came to mind, why not just inject the require statement inside the spec file generated?
There was a problem hiding this comment.
A question that just came to mind, why not just inject the require statement inside the spec file generated?
That would definitely work for projects that have RSpec / Minitest configured with data_migrate at the start. My original intent here was to allow existing projects to run a Rake task to easily add the line to the spec file. Though to be honest I agree with your first statement regarding the complexity of all this to essentially inject two lines of code. I'm going to change this to simply be part of the readme and see if it cleans things up
| def lines_for_injection | ||
| [ | ||
| "# data_migrate: Include data migrations for writing test coverage", | ||
| "Dir[Rails.root.join(DataMigrate.config.data_migrations_path, '*.rb')].each { |f| require f }" |
There was a problem hiding this comment.
DataMigrate.config.data_migrations_path can be an array of locations in order to support engine-based data migrations...
We could reverse this logic and do something like this perhaps:
| "Dir[Rails.root.join(DataMigrate.config.data_migrations_path, '*.rb')].each { |f| require f }" | |
| "Dir[*Array.wrap(DataMigrate.config.data_migrations_path).map{|path| Rails.root.join(path, '*.rb')}].each { |f| require f }" |
There was a problem hiding this comment.
I honestly think it would be easier to let the user require whichever file they are about to test within their spec file though?
There was a problem hiding this comment.
I honestly think it would be easier to let the user require whichever file they are about to test within their spec file though?
Definitely true to explicitly include these. I'm thinking for ease-of-use, having an automated way to include any & all data files might make things easier than multiple require lines. Not all data migrations would be tested, though, so might be a good case for an explicit approach.
Good catch on engines also
| def test_helper_file_path | ||
| case DataMigrate::Helpers::InferTestSuiteType.new.call | ||
| when :rspec | ||
| Rails.root.join('spec', 'rails_helper.rb') |
There was a problem hiding this comment.
I think if you were to remove the lines added to inject the require statement, this all disappears. It wouldn't be as full-fledged as this solution but avoids a bunch of complexity which users can just replace if they wished to do so.
A question that just came to mind, why not just inject the require statement inside the spec file generated?
| end | ||
|
|
||
| def data_migrations_spec_file_path | ||
| File.join(Rails.root, 'spec', DataMigrate.config.data_migrations_path, "#{file_name}_spec.rb") |
There was a problem hiding this comment.
You may have issues here too with data_migrations_path not being necessarily a single path either here.
There was a problem hiding this comment.
You could use the same logic here:
https://github.com/vprigent/data-migrate/blob/517328051687873c9f8412efe2d6a8f2cf7e9157/lib/generators/data_migration/data_migration_generator.rb#L61-L64
Though ideally we'd abstract that away and make a definitive location as being the location for new migrations while still allowing engines/libraries to integrate with data_migration in a way that's painless for users.
| @@ -0,0 +1,10 @@ | |||
| require 'rails_helper' | |||
| require './<%= data_migrations_file_path_with_version %>' | |||
There was a problem hiding this comment.
@vprigent Excellent idea here! Much easier and right next to the file being tested
|
Just recycled all proposed changes / suggestions. Much simpler implementation while preserving the ability to generate test files alongside migrations 🚀 |
Morozzzko
left a comment
There was a problem hiding this comment.
Nice work! I feel like we might merge it and start getting feedback on it!
lib/data_migrate/config.rb
Outdated
| @data_template_path = DEFAULT_DATA_TEMPLATE_PATH | ||
| @db_configuration = nil | ||
| @spec_name = nil | ||
| @test_support_enabled = false |
There was a problem hiding this comment.
Nitpick:
I've read your PR a few times so far. Decided to do one last round from the "does it make me confused if I jump straight into the code without reading the context?". So please consider my comments as ideas / suggestions, and not something authoritative.
In this instance, I feel like the name is a bit confusing. Because it's not that we turn on test support, but we turn on test generation. So I'd prefer to have test_generator_enabled, generate_tests, generate_specs, generate_test_files, etc.
There was a problem hiding this comment.
I like this suggested change as it better represents what the configuration value achieves 💯
lib/data_migrate/config.rb
Outdated
| @db_configuration = nil | ||
| @spec_name = nil | ||
| @test_support_enabled = false | ||
| @test_framework = nil |
There was a problem hiding this comment.
Nitpick:
It feels like the default shouldn't be nil, but we should attempt to infer rspec/minitest early.
I'm not sure it makes as much sense from the "config lifecycle" perspective, though. If not, I wonder if we can make the limitation a bit more obvious?
There was a problem hiding this comment.
What if we were to push the InferTestSuiteType call up into the configuration initializer here? That way, inference check would only happen once (at configuration level), and we could move the early return out of the inference class entierly. Setting the test_framework would then override the inferred value for manual control over the generator.
Also, I'd like to update InferTestSuiteType => InferTestFramework to match the configuration value naming
# lib/data_migrate/config.rb
def initialize
@data_migrations_table_name = "data_migrations"
@data_migrations_path = "db/data/"
@data_template_path = DEFAULT_DATA_TEMPLATE_PATH
@db_configuration = nil
@spec_name = nil
@test_support_enabled = false
@test_framework = DataMigrate::Helpers::InferTestFramework.new.call
end
# lib/data_migrate/helpers/infer_test_framework.rb
module DataMigrate
module Helpers
class InferTestFramework
def call
if File.exist?(Rails.root.join('spec', 'spec_helper.rb'))
:rspec
elsif File.exist?(Rails.root.join('test', 'test_helper.rb'))
:minitest
else
raise StandardError.new('Unable to determine test suite')
end
end
end
end
end
# lib/generators/data_migration/data_migration_generator.rb
def create_data_migration_test
return unless DataMigrate.config.test_support_enabled
case DataMigrate.config.test_framework
when :rspec
template "data_migration_spec.rb", data_migrations_spec_file_path
when :minitest
template "data_migration_test.rb", data_migrations_test_file_path
end
endI believe this cleans up the logic and keeps a good separation of responsibility throughout the flow. Thoughts on the above?
| def create_data_migration_test | ||
| return unless DataMigrate.config.test_support_enabled | ||
|
|
||
| case DataMigrate::Helpers::InferTestSuiteType.new.call |
There was a problem hiding this comment.
nitpick:
... adding to my previous comment: would be nice if we could always rely on config here, instead of manually infering things each time
* Update name of configuration value test_support_enabled => test_generator_enabled * Update name of configuration value test_framework => test_generator_framework * Config now automatically sets the test_generator_framework by inference and allows for overriding * Removed file system calls to Rails in favor of File.expand_path * Utilize configuration value to determine which generated spec/test file to create
|
I've made several updates in the latest commit: b74b7f9 |
| end | ||
|
|
||
| after(:all) do | ||
| FileUtils.rm_rf(Dir.pwd + '/db/data') |
There was a problem hiding this comment.
Fun fact: Not setting DataMigrate.config.data_migrations_path and trying to use it to cleanup test files directories, results in the spec trying to cleanup your root filesystem /. 🔥 I had the fun time of finding that out the hard way. The above is much safer now, also likely why all the tests were failing

Why
Implements: #148
I recently needed well-factored test coverage for a complex data migration, so I thought I'd take an attempt at implementing test suite support for both RSpec and Minitest.
What
config.test_generator_enabledwhich conditionally creates associated tests files uponrails g data_migration add_this_to_that. (Default: false)config.test_generator_frameworkwhich can be set to:minitestor:rspecNotes
I read the comment here: #162 (comment) regarding using
Rails.configuration.generators.options[:rails]. That returns a very clear:Rails.configuration.generators.options[:rails][:test_framework] #=> :rspec. While testing Minitest and RSpec, I noticed that this comes from having the rspec-rails gem installed. It doesn't necessarily check for valid installation. I attempted a valid installation check by looking for the framework specific filestest/test_helper.rbandspec/rails_helper.rb.Resources
Also, kudos on a great gem! 💎