How to test rake task when arguments are passed - ruby-on-rails

I am testing rake task when arguments are passed, I am trying to run a rake task like this
rake do_something_after_n_number_of_days 30
when I run
let(:task) { Rake::Task['do_something_after_n_number_of_days'] }
task.invoke(30)
I get ARGV[1] as nil but when I run the rake task in the shell it works fine.
I have looked in to these answers 1 2 but all of them describe this approach
rake do_something_after_n_number_of_days[30] and I can test that with test.invoke(30) but in some shells I have to escape the brackets like this
rake do_something_after_n_number_of_days\[30\]

It's common practice in commands to use environment variables for configuration. You'll see this used in many different gems. For your needs, you could do something like this instead:
task :do_something_after_n_number_of_days do
raise ArgumentError, 'Invalid DAYS environment setting' if ENV['DAYS'].nil?
puts ENV['DAYS']
end
Then you can set the ENV in your test, like this:
let(:task) { Rake::Task['do_something_after_n_number_of_days'] }
context "when DAYS is set" do
before { ENV['DAYS'] = '100' }
it "does something" do
expect { task.invoke }.to output("100").to_stdout
end
end
context "when DAYS is nil" do
before { ENV['DAYS'] = nil }
it "raises an ArgumentError" do
expect { task.invoke }.to raise_error(ArgumentError, /Invalid DAYS/)
end
end

Related

Make Rake task testable with FactoryGirl user

I have been trying to test a rake task with RSPEC. So far my understanding of the problem is that DB transactions are creating different contexts for each aspects of the test (one in the test context and one in the rake task execution context). Here are the two files I'm using in my test:
One to make rake tasks available in Rspec and be able to call task.execute:
...spec/support/tasks.rb
require "rake"
module TaskExampleGroup
extend ActiveSupport::Concern
included do
let(:task_name) { self.class.top_level_description.sub(/\Arake /, "") }
let(:tasks) { Rake::Task }
subject(:task) { tasks[task_name] }
end
end
RSpec.configure do |config|
config.define_derived_metadata(:file_path => %r{/spec/tasks/}) do |metadata|
metadata[:type] = :task
end
config.include TaskExampleGroup, type: :task
config.before(:suite) do
Rails.application.load_tasks
end
end
One to properly test my rake task:
...spec/tasks/reengage_spec.rb
require "rails_helper"
describe "rake reengage:unverified_users", type: :task do
let(:confirmed_user){create(:user, :confirmed)}
it "Preloads the Rails environment" do
expect(task.prerequisites).to include "environment"
end
it "Runs gracefully with no users" do
expect { task.execute }.not_to raise_error
end
it "Logs to stdout without errors" do
expect { task.execute }.to_not output.to_stderr
end
it "Reengage confirmed users" do
task.execute
expect(confirmed_user).to have_attributes(:reactivated_at => DateTime.now)
end
end
So far, when the rake task is executed through Rspec, it seems like the user I have created is not available in the context of rake, hence his . reactivated_at remains nil. I though I could use database cleaner to be able to configure DB transactions and preserve the context but I have no idea where to start.
I could end putting all this code in a lib and test it separately but I'm wondering if there is a way to do this. Is there a straightforward way to share the context of execution or to make my FactoryGirl user available when the task is initialized?

Are spies an appropriate approach to see if Resque methods are being fired?

While simple coverage is reporting this as 100% covered I am not satisfied. The spec marked as focus I would like to confirm that all of the Resque methods are being fired. Is a spy or a double the right approach for this?
Spec
describe 'resque tasks' do
include_context 'rake'
let(:task_paths) { ['tasks/resque'] }
before do
invoke_task.reenable
end
# rubocop:disable all
describe 'resque' do
context ':setup' do
let(:task_name) { 'resque:setup' }
it 'works' do
invoke_task.invoke
expect(Resque.logger.level).to eq(1)
end
end
context ':scheduler_setup' do
let(:task_name) { 'resque:scheduler_setup' }
it 'works' do
expect(invoke_task.invoke).to be
end
end
context ':clear', focus: true do
let(:task_name) { 'resque:clear' }
it 'works' do
expect(Resque).to receive(:remove_queue).with('queue:default').and_return(true)
expect { invoke_task.invoke }.to output(
"Clearing default...\n"\
"Clearing delayed...\n"\
"Clearing stats...\n"\
"Clearing zombie workers...\n"\
"Clearing failed jobs...\n"\
"Clearing resque workers...\n"
).to_stdout
end
end
end
describe 'jobs:work' do
let(:task_name) { 'jobs:work' }
it 'works' do
expect_any_instance_of(Object).to receive(:system).with("bundle exec env rake resque:workers QUEUE='*' COUNT='#{ENV['WEB_WORKERS']}'").and_return(true)
expect(invoke_task.invoke).to be
end
end
# rubocop:enable all
end
Resque Rake Task
require 'resque'
require 'resque/tasks'
require 'resque/scheduler/tasks'
# http://jademind.com/blog/posts/enable-immediate-log-messages-of-resque-workers/
namespace :resque do
desc 'Initialize Resque environment'
task setup: :environment do
ENV['QUEUE'] ||= '*'
Resque.logger.level = Logger::INFO
end
task scheduler_setup: :environment
# see http://stackoverflow.com/questions/5880962/how-to-destroy-jobs-enqueued-by-resque-workers - old version
# see https://github.com/defunkt/resque/issues/49
# see http://redis.io/commands - new commands
desc 'Clear pending tasks'
task clear: :environment do
queues = Resque.queues
queues.each do |queue_name|
puts "Clearing #{queue_name}..."
Resque.remove_queue("queue:#{queue_name}")
end
puts 'Clearing delayed...'
Resque.redis.keys('delayed:*').each do |key|
Resque.redis.del key.to_s
end
Resque.redis.del 'delayed_queue_schedule'
Resque.reset_delayed_queue
puts 'Clearing stats...'
Resque.redis.set 'stat:failed', 0
Resque.redis.set 'stat:processed', 0
puts 'Clearing zombie workers...'
Resque.workers.each(&:prune_dead_workers)
puts 'Clearing failed jobs...'
cleaner = Resque::Plugins::ResqueCleaner.new
cleaner.clear
puts 'Clearing resque workers...'
Resque.workers.each(&:unregister_worker)
end
end
desc 'Alias for resque:work'
# http://stackoverflow.com/questions/10424087/resque-multiple-workers-in-development-mode
task 'jobs:work' do
system("bundle exec env rake resque:workers QUEUE='*' COUNT='#{ENV['WEB_WORKERS']}'")
end
Shared Context
shared_context 'rake' do
let(:invoke_task) { Rake.application[task_name] }
let(:highline) { instance_double(HighLine) }
before do
task_paths.each do |task_path|
Rake.application.rake_require(task_path)
end
Rake::Task.define_task(:environment)
end
before do
allow(HighLine).to receive(:new).and_return(highline)
end
end
The spec marked as focus I would like to confirm that all of the Resque methods are being fired. Is a spy or a double the right approach for this?
Yes. A Spy in this test would only be testing that it received those methods calls, since it is acting as a double stand-in for those tests; meaning you are not testing the behaviour of task in this test, you are testing that the task has an object like Resque receiving those method calls.
Spies
Message expectations put an example's expectation at the start, before you've invoked the code-under-test. Many developers prefer using an act-arrange-assert (or given-when-then) pattern for structuring tests. Spies are an alternate type of test double that support this pattern by allowing you to expect that a message has been received after the fact, using have_received.
-- Spies - Basics - RSpec Mocks - RSpec - Relish
An example of what this might look like for your it 'works' test
it 'works' do
expect(Resque).to receive(:remove_queue).with('queue:default').and_return(true)
expect { invoke_task.invoke }.to output(
"Clearing default...\n"\
"Clearing delayed...\n"\
"Clearing stats...\n"\
"Clearing zombie workers...\n"\
"Clearing failed jobs...\n"\
"Clearing resque workers...\n"
).to_stdout
end
Is as follows
RSpec.describe "have_received" do
it 'works' do
Rake::Task.define_task(:environment)
invoke_task = Rake.application['resque:clear']
redis_double = double("redis")
allow(redis_double).to receive(:keys).with('delayed:*').and_return([])
allow(redis_double).to receive(:del).with('delayed_queue_schedule').and_return(true)
allow(redis_double).to receive(:set).with('stat:failed', 0).and_return(true)
allow(redis_double).to receive(:set).with('stat:processed', 0).and_return(true)
allow(Resque).to receive(:queues).and_return([])
allow(Resque).to receive(:redis).and_return(redis_double)
# allow(Resque).to receive(:remove_queue).with('queue:default') #.and_return(true)
allow(Resque).to receive(:reset_delayed_queue) #.and_return(true)
allow(Resque).to receive(:workers).and_return([])
cleaner_double = double("cleaner")
allow(Resque::Plugins::ResqueCleaner).to receive(:new).and_return(cleaner_double)
allow(cleaner_double).to receive(:clear).and_return(true)
expect { invoke_task.invoke }.to output(
# "Clearing default...\n"\
"Clearing delayed...\n"\
"Clearing stats...\n"\
"Clearing zombie workers...\n"\
"Clearing failed jobs...\n"\
"Clearing resque workers...\n"
).to_stdout
expect(redis_double).to have_received(:keys)
expect(redis_double).to have_received(:del)
expect(redis_double).to have_received(:set).with('stat:failed', 0)
expect(redis_double).to have_received(:set).with('stat:processed', 0)
expect(Resque).to have_received(:queues)
expect(Resque).to have_received(:redis).at_least(4).times
# expect(Resque).to have_received(:remove_queue).with('queue:default')
expect(Resque).to have_received(:reset_delayed_queue)
expect(Resque).to have_received(:workers).twice
expect(Resque::Plugins::ResqueCleaner).to have_received(:new)
expect(cleaner_double).to have_received(:clear)
end
end
Notes:
The allow(Resque).to receive(:remove_queue).with('queue:default') is commented out since allow(redis_double).to receive(:keys).with('delayed:*').and_return([]) returns an empty array in my example code, meaning that queues.each never iterates once, so Resque.remove_queue("queue:#{queue_name}") is never called and "Clearing default...\n"\ is not return for the expected output
Also, there is a lot happening in this one task, and might be worthwhile breaking it down into smaller tasks.
This effectively stubs each of the expected method calls on the Resque object and then accesses after task has been invoked that the doubles receive those expected method calls. It does not test the outcomes of those tasks, only that method calls occurred and confirms those
methods are being fired.
References:
Spies - Basics - RSpec Mocks - RSpec - Relish
Allowing messages - Basics - RSpec Mocks - RSpec - Relish
A Closer Look at Test Spies

A General Method to be invoked before every rake execution

In my rails project (Rails 3.1, Ruby 1.9.3) there are around 40 rake tasks defined. The requirement is that I should be able to create an entry (the rake details) in a database table right when we start each rake. The details I need are the rake name, arguments, start time and end time. For this purpose, I don't want rake files to be updated with the code. Is it possible to do this outside the scope of rake files.
Any help would be greatly appreciated!!
Try this
https://github.com/guillermo/rake-hooks
For example in your Rakefile
require 'rake/hooks'
task :say_hello do
puts "Good Morning !"
end
before :say_hello do
puts "Hi !"
end
#For multiple tasks
namespace :greetings do
task :hola do puts "Hola!" end ;
task :bonjour do puts "Bonjour!" end ;
task :gday do puts "G'day!" end ;
end
before "greetings:hola", "greetings:bonjour", "greetings:gday" do
puts "Hello!"
end
rake greetings:hola # => "Hello! Hola!"
This seems to be a bit awkward, But it may help others.
Rake.application.top_level_tasks
will return an array of information including Rake name and its arguments.
Reference attached below.
pry(main)> a = Rake.application.top_level_tasks
=> ["import_data[client1,", "data.txt]"]
When you create rake task, you can pass a parent task which will run before your task:
task my_task: :my_parent_task do
# ...
end
If your task depends from more than 1 task, you can pass an array of parent tasks
task my_task: [:my_prev_task, :my_another_prev_task] do
# ...
end

Testing Rake in Rails: Multiple Error Raises Silenced In Test

I have a rake task that guards against dangerous Rails rake rasks, based on the environment. It works fine. When I test each individual dangerous method in RSpec, the test passes. When I test multiple in a row, for multiple environments, the test fails after the first one. Even if I run the test multiple times for the same dangerous action, rake db:setup for example, it will only pass the first time. If I run the tests as individual it statements, one for each dangerous action, only the first two will pass (there are 4).
How can I get RSpec to behave correctly here, and pass all the tests when run in a suite?
The rake task
# guard_dangerous_tasks.rake
class InvalidTaskError < StandardError; end
task :guard_dangerous_tasks => :environment do
unless Rails.env == 'development'
raise InvalidTaskError
end
end
%w[ db:setup db:reset ].each do |task|
Rake::Task[task].enhance ['guard_dangerous_tasks']
end
The RSpec test
require 'spec_helper'
require 'rake'
load 'Rakefile'
describe 'dangerous_tasks' do
context 'given a production environment' do
it 'prevents dangerous tasks' do
allow(Rails).to receive(:env).and_return('production')
%w[ db:setup db:reset ].each do |task_name|
expect { Rake::Task[task_name].invoke }.to raise_error(InvalidTaskError)
end
end
end
context 'given a test environment' do
it 'prevents dangerous tasks' do
allow(Rails).to receive(:env).and_return('test')
%w[ db:setup db:reset ].each do |task_name|
expect { Rake::Task[task_name].invoke }.to raise_error(InvalidTaskError)
end
end
end
end
RSpec Output
# we know the guard task did its job,
# because the rake task didn't actually run.
Failure/Error: expect { Rake::Task[task_name].invoke }.to raise_error(InvalidTaskError)
expected InvalidTaskError but nothing was raised
I can think about two solution of your problem.
But first we need to find out where is the root of the problem.
Root of the problem
Let's start with a line from your code:
Rake::Task[task].enhance ['guard_dangerous_tasks']
Comparing it with source code of Rake::Task
# File rake/task.rb, line 96
def enhance(deps=nil, &block)
#prerequisites |= deps if deps
#actions << block if block_given?
self
end
you can see, that guard_dangerous_tasks should be added to #prerequisites array. It can be easily checked:
p Rake::Task['db:reset'].prerequisites # => ["environment", "load_config", "guard_dangerous_tasks"]
Continuing with you source code.
You use invoke to execute tasks. If we pay close attention to invoke's' documentation, it states:
Invoke the task if it is needed.
Once the task is executed, it could not be invoked again (unless we reenable it).
But why should this to be a problem? We are running different tasks, aren't we? But actually we don't!
We run guard_dangerous_tasks before all tasks in our tasks array! And it's being executed only once.
Solution #1 not the best one
As soon as we know where is our problem we can think about one (not the best solution).
Let's reenable guard_dangerous_tasks after each iteration:
dangerous_task = Rake::Task['guard_dangerous_tasks']
%w[ db:setup db:reset ].each do |task_name|
expect { Rake::Task[task_name].invoke }.to raise_error(InvalidTaskError)
dangerous_task.reenable
end
Solution #2 guard_dangerous_tasks is not a prerequisite
We get better solution of our problem if we realize, that guard_dangerous_tasks should not be a prerequisite! Prerequisite are supposed to "prepare" stage and be executed only once. But we should never blind our eyes to dangers!
This is why we should extend with guard_dangerous_tasks as an action, which will be executed each time the parent task is run.
According to the source code of Rake::Task (see above) we should pass our logic in a block if we want it to be added as an action.
%w[ db:setup db:reset ].each do |task|
Rake::Task[task].enhance do
Rake::Task['guard_dangerous_tasks'].execute
end
end
We can leave our test unchanged now and it passes:
%w[ db:setup db:reset ].each do |task_name|
expect { Rake::Task[task_name].invoke }.to raise_error(InvalidTaskError)
end
But leaving invoke is a ticket for new problems. It's better to be replaced with execute:
%w[ db:setup db:reset ].each do |task_name|
expect { Rake::Task[task_name].execute }.to raise_error(InvalidTaskError)
end
Be careful with invoke!
We said above, that using invoke is a ticket for new problems. What kind of problems?
Let's try to test our code for both test and production environments. If we wrap our tests inside this loop:
['production','test'].each do |env_name|
env = ActiveSupport::StringInquirer.new(env_name)
allow(Rails).to receive(:env).and_return(env)
%w[ db:setup db:reset ].each do |task_name|
expect { Rake::Task[task_name].invoke }.to raise_error(InvalidTaskError)
end
end
our test will fail with original reason. You can easily fix this by replacing the line
expect { Rake::Task[task_name].invoke }.to raise_error(InvalidTaskError)
with
expect { Rake::Task[task_name].execute }.to raise_error(InvalidTaskError)
So what was the reason? You probably already guess it.
In failing test we invoked the same two tasks twice. First time they were executed. The second time they should be reenabled before invokation to execute. When we use execute, action is reenable automatically.
Note You can find working example of this project here: https://github.com/dimakura/stackoverflow-projects/tree/master/31821220-testing-rake
Looks like two tasks cannot point to the same task for enhancement so maybe there is a conflict at runtime. So try the block method to handle the situation.
class InvalidTaskError < StandardError; end
%w[ db:setup db:reset ].each do |task|
Rake::Task[task].enhance do
unless Rails.env == 'development'
raise InvalidTaskError
end
end
end
and in the spec file, the following modification would create two examples to track the specs properly.
# require 'rails_helper'
require 'spec_helper'
require 'rake'
load 'Rakefile'
describe 'dangerous_tasks' do
context 'given a production environment' do
%w[ db:setup db:reset ].each do |task_name|
it "prevents dangerous tasks #{task_name}" do
allow(Rails).to receive(:env).and_return('production')
expect { Rake::Task[task_name].invoke }.to raise_error(InvalidTaskError)
end
end
end
end
Did you try passing in the specific error?:
expect { Rake::Task[task_name].invoke }.to raise_error(StandardError)

How to Test Factories First with Minitest but without the spec framework?

I found a blog post about Testing Factories First (by BigBinary - which happens to be a Minitest/spec version of Thoughtbot's RSpec original).
Could you please show me the equivalent without the spec framework - just with Minitest (Rails)?
The Thoughtbot approach (RSpec)
spec/factories_spec.rb
FactoryGirl.factories.map(&:name).each do |factory_name|
describe "The #{factory_name} factory" do
it 'is valid' do
build(factory_name).should be_valid
end
end
end
Rakefile
if defined?(RSpec)
desc 'Run factory specs.'
RSpec::Core::RakeTask.new(:factory_specs) do |t|
t.pattern = './spec/factories_spec.rb'
end
end
task spec: :factory_specs
The BigBinary approach (Minitest, spec)
spec/factories_spec.rb
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe FactoryGirl do
EXCEPTIONS = %w(base_address base_batch bad_shipping_address)
FactoryGirl.factories.each do |factory|
next if EXCEPTIONS.include?(factory.name.to_s)
describe "The #{factory.name} factory" do
it 'is valid' do
instance = build(factory.name)
instance.must_be :valid?
end
end
end
end
lib/tasks/factory.rake
desc 'Run factory specs.'
Rake::TestTask.new(:factory_specs) do |t|
t.pattern = './spec/factories_spec.rb'
end
task test: :factory_specs
What is the Minitest equivalent (without spec)?
The approach I am presenting below is slightly different than the two original solutions - in the sense that my approach creates only one test, within which I cycle through the factories and run an assertion against each. I was not able to create a solution that mimics the original solutions any closer - which is (I believe) a separate test method for each factory. If someone could show such an implementation, that would be cool.
test/aaa_factories_tests/factories_test.rb
require File.expand_path(File.dirname(__FILE__) + '/../test_helper.rb')
class FactoriesTest < Minitest::Unit::TestCase
puts "\n*** Factories Test ***\n\n"
EXCEPTIONS = %w(name_of_a_factory_to_skip another_one_to_skip)
def test_factories
FactoryGirl.factories.each do |factory|
next if EXCEPTIONS.include?(factory.name.to_s)
instance = FactoryGirl.build(factory.name)
assert instance.valid?, "invalid factory: #{factory.name}, error messages: #{instance.errors.messages.inspect}"
instance = factory = nil
end
end
end
Thanks to the way Minitest works out of the box -- add any directories under test/ and minitest-rails will automatically create the associated rake task for it. So let's say you add a test/api/ directory, rake minitest:api will automagically be available. -- I see the task when I run bundle exec rake -T with no other configurations:
rake minitest:aaa_factories_tests # Runs tests under test/aaa_factories_tests
And I am able to run this task successfully:
-bash> bundle exec rake minitest:aaa_factories_tests
*** Factories Test ***
Run options: --seed 19208
# Running tests:
.
Finished tests in 0.312244s, 3.2026 tests/s, 9.6079 assertions/s.
1 tests, 3 assertions, 0 failures, 0 errors, 0 skips
Despite the ugliness of prepending the directory with aaa, I am able to have the factories tested first with:
bundle exec rake minitest:all
The reason for the aaa prepend solution is MiniTest does a Dir glob and on Mac OS X (and other Unix variants) the results are sorted alphabetically (though the results differ across different platforms).
As well, I prepended the default_tasks array with aaa_factories_tests to have the factories tested first in the default Minitest task (i.e. when running bundle exec rake minitest).
lib/tasks/factories_first.rake
MiniTest::Rails::Testing.default_tasks.unshift('aaa_factories_tests') if Rails.env =~ /^(development|test)\z/
Note that the above condition avoids erroneously referencing Minitest in environments where it is unavailable (I have confined minitest-rails to :test and :development groups in Gemfile). Without this if-condition, pushing to Heroku (for example to staging or production) will result in uninitialized constant MiniTest.
Of course I am also able to run the factories test directly:
bundle exec ruby -I test test/aaa_factories_tests/factories_test.rb
Here is a solution for MiniTest without the spec framework:
test/factories_test.rb
require File.expand_path(File.dirname(__FILE__) + '/test_helper')
class FactoriesTest < ActiveSupport::TestCase
EXCEPTIONS = %w(griddler_email)
FactoryBot.factories.map(&:name).each do |factory_name|
next if factory_name.to_s.in?(EXCEPTIONS)
context "The #{factory_name} factory" do
should 'be valid' do
factory = build(factory_name)
assert_equal true, factory.valid?, factory.errors.full_messages
end
end
end
end
lib/tasks/factory.rake
namespace :test do
desc 'Test factories'
Rake::TestTask.new(:factories) do |t|
t.pattern = './test/factories_test.rb'
end
end
task minitest: 'test:factories'
The most important thing is to use taks minitest instead of task test if you want the factories tests to be run before other tests.

Resources