Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add Rails generator and rake task support for creating data migration test coverage #355

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ You can generate a data migration as you would a schema migration:
rake data:migrate:up # Runs the "up" for a given migration VERSION
rake data:rollback # Rolls the schema back to the previous version (specify steps w/ STEP=n)
rake data:schema:load # Load data_schema.rb file into the database without running the data migrations
rake data:tests:setup # Setup data migrations for identified test suite
rake data:version # Retrieves the current schema version number for data migrations
rake db:abort_if_pending_migrations:with_data # Raises an error if there are pending migrations or data migrations
rake db:forward:with_data # Pushes the schema to the next version (specify steps w/ STEP=n)
Expand Down Expand Up @@ -125,10 +126,24 @@ DataMigrate.configure do |config|
'password' => nil,
}
config.spec_name = 'primary'
end

# Enable data_migration generator to create test files
config.test_support_enabled = true
end
```

### Test Suite Support

When `config.test_support_enabled = true`, the `data_migration` generator will create test files for your data migrations. This is dependent on
the test suite you are using.

For example, if you run `rails g data_migration add_this_to_that`, the following files will be created:

- `/spec/db/data/add_this_to_that_spec.rb` (for RSpec)
- `/test/db/data/add_this_to_that_test.rb` (for Minitest)

You can also run the Rake task `rake data:tests:setup` to configure your test suite to load data migrations.

## Capistrano Support

The gem comes with a capistrano task that can be used instead of `capistrano/rails/migrations`.
Expand Down
2 changes: 2 additions & 0 deletions lib/data_migrate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
require File.join(File.dirname(__FILE__), "data_migrate", "status_service")
require File.join(File.dirname(__FILE__), "data_migrate", "migration_context")
require File.join(File.dirname(__FILE__), "data_migrate", "railtie")
require File.join(File.dirname(__FILE__), "data_migrate", "helpers/infer_test_suite_type")
require File.join(File.dirname(__FILE__), "data_migrate", "tasks/data_migrate_tasks")
require File.join(File.dirname(__FILE__), "data_migrate", "tasks/setup_tests")
require File.join(File.dirname(__FILE__), "data_migrate", "config")
require File.join(File.dirname(__FILE__), "data_migrate", "schema_migration")
require File.join(File.dirname(__FILE__), "data_migrate", "database_configurations_wrapper")
Expand Down
5 changes: 3 additions & 2 deletions lib/data_migrate/config.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module DataMigrate
include ActiveSupport::Configurable
class << self

class << self
def configure
yield config
end
Expand All @@ -12,7 +12,7 @@ def config
end

class Config
attr_accessor :data_migrations_path, :data_template_path, :db_configuration, :spec_name
attr_accessor :data_migrations_path, :data_template_path, :db_configuration, :spec_name, :test_support_enabled

DEFAULT_DATA_TEMPLATE_PATH = "data_migration.rb"

Expand All @@ -21,6 +21,7 @@ def initialize
@data_template_path = DEFAULT_DATA_TEMPLATE_PATH
@db_configuration = nil
@spec_name = nil
@test_support_enabled = false
end

def data_template_path=(value)
Expand Down
15 changes: 15 additions & 0 deletions lib/data_migrate/helpers/infer_test_suite_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module DataMigrate
module Helpers
class InferTestSuiteType
def call
if File.exist?(Rails.root.join('spec', 'spec_helper.rb'))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

: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
76 changes: 76 additions & 0 deletions lib/data_migrate/tasks/setup_tests.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# frozen_string_literal: true

module DataMigrate
module Tasks
class SetupTests
INJECTION_MATCHER = Regexp.new(/require_relative ["|']\.\.\/config\/environment["|']/)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 call
return if injection_exists?

if find_injection_location.nil?
puts 'data_migrate: config/environment.rb was not found in the test helper file.'
return
end

add_newline

lines_for_injection.reverse.each do |line|
file_contents.insert(find_injection_location, "#{line}\n")
end

add_newline

File.open(test_helper_file_path, 'w') do |file|
file.puts file_contents
end

puts 'data_migrate: Test setup complete.'
end

private

def test_helper_file_path
case DataMigrate::Helpers::InferTestSuiteType.new.call
when :rspec
Rails.root.join('spec', 'rails_helper.rb')
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

when :minitest
Rails.root.join('test', 'test_helper.rb')
end
end

def file_contents
@_file_contents ||= File.readlines(test_helper_file_path)
end

def find_injection_location
@_find_injection_location ||= begin
index = file_contents.index { |line| line.match?(INJECTION_MATCHER) }
index.present? ? index + 1 : nil
end
end

def add_newline
file_contents.insert(find_injection_location, "\n")
end

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 }"
]
end

def injection_exists?
file_contents.each_cons(lines_for_injection.length) do |content_window|
if content_window.map(&:strip) == lines_for_injection.map(&:strip)
puts 'data_migrate: Test setup already exists.'
return true
end
end

false
end
end
end
end
20 changes: 20 additions & 0 deletions lib/generators/data_migration/data_migration_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,30 @@ class DataMigrationGenerator < Rails::Generators::NamedBase
def create_data_migration
set_local_assigns!
migration_template template_path, data_migrations_file_path
create_data_migration_test
end

protected

def create_data_migration_test
return unless DataMigrate.config.test_support_enabled

case DataMigrate::Helpers::InferTestSuiteType.new.call
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
end

def data_migrations_test_file_path
File.join(Rails.root, 'test', DataMigrate.config.data_migrations_path, "#{file_name}_test.rb")
end

def data_migrations_spec_file_path
File.join(Rails.root, 'spec', DataMigrate.config.data_migrations_path, "#{file_name}_spec.rb")
end

def set_local_assigns!
if file_name =~ /^(add|remove)_.*_(?:to|from)_(.*)/
@migration_action = $1
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require 'rails_helper'

describe <%= migration_class_name %>, type: :data_migration do
let(:migration) { <%= migration_class_name %>.new }

pending "should test `migration.up`"

pending "should test `migration.down`"
end
15 changes: 15 additions & 0 deletions lib/generators/data_migration/templates/data_migration_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require 'test_helper'

class <%= migration_class_name %>Test < ActiveSupport::TestCase
def setup
@migration = <%= migration_class_name %>.new
end

def test_migration_up
skip("Pending test coverage for @migration.up")
end

def test_migration_down
skip("Pending test coverage for @migration.down")
end
end
44 changes: 44 additions & 0 deletions spec/data_migrate/helpers/infer_test_suite_type_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
require 'spec_helper'

describe DataMigrate::Helpers::InferTestSuiteType do
subject(:infer_test_suite) { described_class.new }

describe '#call' do
before do
allow(Rails).to receive(:root).and_return(Pathname.new('/fake/path'))
end

context 'when RSpec is detected' do
before do
allow(File).to receive(:exist?).with(Rails.root.join('spec', 'spec_helper.rb')).and_return(true)
allow(File).to receive(:exist?).with(Rails.root.join('test', 'test_helper.rb')).and_return(false)
end

it 'returns :rspec' do
expect(infer_test_suite.call).to eq(:rspec)
end
end

context 'when Minitest is detected' do
before do
allow(File).to receive(:exist?).with(Rails.root.join('spec', 'spec_helper.rb')).and_return(false)
allow(File).to receive(:exist?).with(Rails.root.join('test', 'test_helper.rb')).and_return(true)
end

it 'returns :minitest' do
expect(infer_test_suite.call).to eq(:minitest)
end
end

context 'when no test suite is detected' do
before do
allow(File).to receive(:exist?).with(Rails.root.join('spec', 'spec_helper.rb')).and_return(false)
allow(File).to receive(:exist?).with(Rails.root.join('test', 'test_helper.rb')).and_return(false)
end

it 'raises an error' do
expect { infer_test_suite.call }.to raise_error(StandardError, 'Unable to determine test suite')
end
end
end
end
93 changes: 93 additions & 0 deletions spec/data_migrate/tasks/setup_tests_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# frozen_string_literal: true

require "spec_helper"

describe DataMigrate::Tasks::SetupTests do
let(:file_contents_with_injection) do
<<~FILE_CONTENTS
# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'

# data_migrate: Include data migrations for writing test coverage
Dir[Rails.root.join(DataMigrate.config.data_migrations_path, '*.rb')].each { |f| require f }
FILE_CONTENTS
end
let(:file_contents_without_injection) do
<<~FILE_CONTENTS
# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
FILE_CONTENTS
end
let(:file_contents_without_injection_matcher) do
<<~FILE_CONTENTS
# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
FILE_CONTENTS
end
let(:rails_root) { Pathname.new('/fake/app') }
let(:test_suite_inferrer) { instance_double(DataMigrate::Helpers::InferTestSuiteType) }

before do
allow(Rails).to receive(:root).and_return(rails_root)
allow(DataMigrate::Helpers::InferTestSuiteType).to receive(:new).and_return(test_suite_inferrer)
end

describe "#call" do
context 'when the injected code already exists' do
it 'returns early' do
allow(test_suite_inferrer).to receive(:call).and_return(:rspec)
allow(File).to receive(:readlines).and_return(file_contents_with_injection.lines)

expect(File).not_to receive(:open)

expect {
DataMigrate::Tasks::SetupTests.new.call
}.to output(/data_migrate: Test setup already exists./).to_stdout
end

context 'when the INJECTION_MATCHER is not found' do
it 'returns early' do
allow(test_suite_inferrer).to receive(:call).and_return(:rspec)
allow(File).to receive(:readlines).and_return(file_contents_without_injection_matcher.lines)

expect(File).not_to receive(:open)

expect {
DataMigrate::Tasks::SetupTests.new.call
}.to output(/data_migrate: config\/environment.rb was not found in the test helper file./).to_stdout
end
end

context 'for RSpec' do
it 'calls File.open for writing to rails_helper.rb' do
allow(test_suite_inferrer).to receive(:call).and_return(:rspec)
allow(File).to receive(:readlines).and_return(file_contents_without_injection.lines)

expect(File).to receive(:open).with(rails_root.join('spec', 'rails_helper.rb'), 'w')

expect {
DataMigrate::Tasks::SetupTests.new.call
}.to output(/data_migrate: Test setup complete./).to_stdout
end
end

context 'for Minitest' do
it 'calls File.open for writing to test_helper.rb' do
allow(test_suite_inferrer).to receive(:call).and_return(:minitest)
allow(File).to receive(:readlines).and_return(file_contents_without_injection.lines)

expect(File).to receive(:open).with(rails_root.join('test', 'test_helper.rb'), 'w')

expect {
DataMigrate::Tasks::SetupTests.new.call
}.to output(/data_migrate: Test setup complete./).to_stdout
end
end
end
end
end
Loading