My spec provides coverage like I had hoped, however, the following 2 messages are displaying in the rspec output:
rake resque:scheduler
rake environment resque:work
How do I swallow these during spec runs so they do not screw up my nyancat formatter?
Spec
describe 'database rake task' do
include_context 'rake'
let(:task_paths) { ['tasks/heroku'] }
before do
invoke_task.reenable
end
# rubocop:disable all
describe 'myapp:heroku' do
context ':setup' do
context ':secrets' do
let(:task_name) { 'myapp:heroku:setup:secrets' }
context 'with env' do
it 'works' do
expect(File).to receive(:exist?).with('.env').and_return(true)
expect_any_instance_of(Object).to receive(:system).with('heroku config:push --remote production').and_return(true)
expect { invoke_task.invoke }.to output(
"\nUpdating Secrets for production\n"
).to_stdout
end
end
context 'without env' do
it 'works' do
expect(File).to receive(:exist?).with('.env').and_return(false)
expect { invoke_task.invoke }.to raise_error("\nYou are missing the .env file\n").and(output(
"\nUpdating Secrets for production\n"
).to_stdout)
end
end
end
end
end
describe 'schedule_and_work' do
let(:task_name) { 'schedule_and_work' }
context 'with process fork' do
it 'works' do
expect(Process).to receive(:fork).and_return(true)
expect_any_instance_of(Object).to receive(:system).with('rake environment resque:work', {}).and_return(true)
expect(invoke_task.invoke).to be
end
end
context 'without process fork' do
it 'works' do
expect(Process).to receive(:fork).and_return(false)
expect(Process).to receive(:wait).and_return(true)
expect_any_instance_of(Object).to receive(:system).with('rake resque:scheduler', {}).and_return(true)
expect(invoke_task.invoke).to be
end
end
end
# rubocop:enable all
end
Rake Task
namespace :myapp do
namespace :heroku do
namespace :setup do
desc 'modify secrets'
task :secrets do
puts "\nUpdating Secrets for production\n"
raise "\nYou are missing the .env file\n" unless File.exist?('.env')
system('heroku config:push --remote production')
end
end
end
end
# Run resque scheduler on 2 free dynos
# https://grosser.it/2012/04/14/resque-scheduler-on-heroku-without-extra-workers/
task :schedule_and_work do
if Process.fork
sh 'rake environment resque:work'
else
sh 'rake resque:scheduler'
Process.wait
end
end
You can use the following test helper method
require 'stringio'
def silent_warnings
old_stderr = $stderr
$stderr = StringIO.new
yield
ensure
$stderr = old_stderr
end
-- Temporarily disabling warnings in Ruby | Virtuous Code
And wrap the invoking of a Rake task with silent_warnings method; like so
silent_warnings do
expect { invoke_task.invoke }.to output(
"\nUpdating Secrets for production\n"
).to_stdout
end
However, use it sparingly, since it swallow all warnings (printed to $stdout) produced within the block code, making it harder to debug in the future.
Also, you can wrap silent_warnings around all tests within an RSpec describe block using then around hook; e.g.
around(:example) do |example|
silent_warnings { example.run }
end
Again, use it sparingly
Related
I am currently trying to test a rake task with RSpec.
My Rails version is 4.2.4 and rspec-rails version is 3.3.2.
I've have the following in rails_helper.rb:
ENV['RAILS_ENV'] ||= 'test'
require 'spec_helper'
require File.expand_path('../../config/environment', __FILE__)
require 'rspec/rails'
require 'capybara/rspec'
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
RSpec.configure do |config|
...
config.use_transactional_fixtures = false
config.infer_spec_type_from_file_location!
...
end
and then 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 }
# Make the Rake task available as `task` in your examples:
subject(:task) { tasks[task_name] }
end
end
RSpec.configure do |config|
# Tag Rake specs with `:task` metadata or put them in the spec/tasks dir
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
my spec/support/database_cleaner.rb
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each, js: true) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.append_after(:each) do
DatabaseCleaner.clean
end
end
and finally, the spec:
require 'rails_helper'
describe "rake some:my_task", type: :task do
# This test passes
it 'preloads the Rails environment' do
expect(task.prerequisites).to include 'environment'
end
# This test fails
it 'creates AnotherModel' do
my_hash = {foo => 'bar'}
allow(MyClient::Event).to receive(:list).and_return(my_hash)
expect { task.execute }.not_to raise_error
expect(AnotherModel.count).to eq(1)
end
end
The problem is that for some reason, executing this code results in the following error:
Failure/Error: AnotherModel.count
ActiveRecord::StatementInvalid:
PG::InFailedSqlTransaction: ERROR: current transaction is aborted, commands ignored until end of transaction block
The rake task looks like this:
namespace :some do
desc 'Parse stream'
task my_task: :environment do |_t|
cint_events['events'].each do |event|
begin
events = MyClient::Event.list(some_hash)
events.each do |event|
if some_condition
# The test should check whether this object gets created
AnotherModel.first_or_create_by(foo: some_hash['foo'])
end
end
rescue => e
# Log errors
end
end
end
end
I've tried running:
RAILS_ENV=test rake db:drop db:create db:migrate
and then running the spec again but I keep getting the aforementioned error. What might this be caused by?
Thanks in advance!
You configured your tests to run in a database transaction, which is a good thing.
But within your rake task you just eat up all errors that appear with:
rescue => e
# Log errors
end
However, certain errors may still cause you transaction to fail and rollback. So my guess is, that some severe error is happening the first time you do a call to the database (For example, the column foo is not known to the database). After that, it catches the error and you are adding a statement (AnotherModel.count) to the already aborted transaction, which fails.
So a good place to start is to check what the value of e.message is in your rescue block.
Also note:
It is never a good idea to rescue all errors blindly, and almost always leads to strange and unexpected behaviours.
This error seems to occur when in a test environment and your SQL query via ActiveRecord doesn't recognize a field in your query. In other words you have a scope or are trying to return some ActiveRecord relation with a bad database column name.
See this related post:
ActiveRecord::StatementInvalid: PG InFailedSqlTransaction
describe 'Feature' do
before do
setup
end
describe 'Success' do
before do
setup_for_success
end
specify 'It works' do
...
end
end
end
RSpec will always run the setup before setup_for_success. It there a way to run setup_for_success first?
You can do this by scoping a before(:all) to run before a before(:each) try this:
describe 'Feature' do
before(:each) do
puts "second"
end
describe 'Success' do
before(:all) do
puts "first"
end
specify 'It works' do
...
end
end
end
# =>
10:29:54 - INFO - Running: spec
Run options: include {:focus=>true}
first
second
.
Finished in 0.25793 seconds (files took 2.52 seconds to load)
1 example, 0 failures
EDIT:
In Rspec 2, the actions run in this order:
before suite
before all
before each
after each
after all
after suite
Here's a link to the docs showing the order that the methods are called in: https://www.relishapp.com/rspec/rspec-core/v/2-2/docs/hooks/before-and-after-hooks#before/after-blocks-are-run-in-order
Apparently in Rspec 3.5, the before block calls have a different naming that also works. They run in this order:
before :suite
before :context
before :example
after :example
after :context
after :suite
describe 'Feature' do
before(:example) do
puts "second"
end
describe 'Success' do
before(:context) do
puts "first"
end
specify 'It works' do
...
end
end
end
10:59:45 - INFO - Running: spec
Run options: include {:focus=>true}
first
second
.
Finished in 0.06367 seconds (files took 2.57 seconds to load)
1 example, 0 failures
Here's the newer docs:
http://www.relishapp.com/rspec/rspec-core/v/3-5/docs/hooks/before-and-after-hooks
before filters are appended in the order they're specified. Since RSpec 2.10.0, you can prepend them instead by making them prepend_before filters.
Likewise, after filters are prepended by default, but you can append_after them instead.
Your code would end up as follows (compacted for brevity):
describe 'Feature' do
before { setup }
describe 'Success' do
prepend_before { setup_for_success }
it 'works' { ... }
end
end
It seems a little weird you put in a nested context what you need in the outer one. I suspect that you don't need that setup in all of the nested contexts. If that's the case you need to filtering your hooks.
RSpec.describe 'Feature' do
before :each, success: true do
setup_for_success
end
before :each do
setup
end
describe 'Success', success: true do
specify 'It works' do
...
end
end
describe 'Fail' do
specify 'Won´t work' do
...
end
end
end
You can do this without nesting:
RSpec.describe 'Feature' do
before :each, success: true do
setup_for_success
end
before :each do
setup
end
specify 'It works', success: true do
...
end
specify 'Won´t work' do
...
end
end
Here is the link to the docs:
https://relishapp.com/rspec/rspec-core/docs/hooks/filters
i have task /lib/crawler.rake like that:
namespace :crawler do
area_names = Dir[Rails.root.join("lib", "crawler", "*.rb")].map do |file_name|
File.basename(file_name, ".rb")
end
area_names.each do |area_name|
task area_name.to_sym => :environment do
logger = Logger.new("log/crawl_#{area_name}.log")
# do something
parallel_results = crawler.crawl
mutex = Mutex.new
Parallel.each(parallel_results, in_threads: [parallel_results.count, CRAWL_CONFIG["building_thread_max"]].min) do |pages|
begin
# do something
rescue => e
# do something
raise e
end
end
Availability.update_by_grounds_and_time
end
end
end
Logic here, if everything's ok after parallel, we'll call update_by_grounds_and_time method to update Availability; if get error, we'll stop action and raise error.
So i want to write rspec to test for these cases, i want to mock/stub output of task here (pass or raise error) and check did we call update_by_grounds_and_time method?
Can we don't need invoke really task? can we use Rspec Mock?
Can you help me!
Thank
What I usually do in these cases is I extract the meat into a separate class/service-object/whatever, which is much easier to test. The rake task then becomes just an invoker of that object and, as such, doesn't need to be tested.
If it is defined in Rakefile, try this:
require 'rake'
RSpec.describe "Rake Tasks" do
before do
file, path = Rake.application.find_rakefile_location
Rake.load_rakefile("#{path}/#{file}")
end
it "should invoke some tasks" do
expect(Availability).to receive(:update_by_grounds_and_time)
Rake.application["crawler:#{area_name}"].invoke
end
end
If it is defined in foo.rake, then try this one:
require 'rake'
RSpec.describe "Rake Tasks" do
before do
Rake.application.rake_require('/path/to/lib/tasks/foo')
end
it "should invoke some tasks" do
expect(Availability).to receive(:update_by_grounds_and_time)
Rake.application["crawler:#{area_name}"].invoke
end
end
UPDATE (error case)
For example
# foo.rake
Parallel.each(parallel_results, in_threads: [parallel_results.count, CRAWL_CONFIG["building_thread_max"]].min) do |pages|
begin
foo = Foo.new
foo.bar
# do something else
rescue => e
# do something
raise e
end
end
# foo_spec.rb
require 'rake'
RSpec.describe "Rake Tasks" do
before do
Rake.application.rake_require('/path/to/lib/tasks/foo')
end
it "should not call Availability#update_by_grounds_and_time if error raised" do
allow_any_instance_of(Foo).to receive(:bar).and_raise(StandardError)
expect(Availability).to_not receive(:update_by_grounds_and_time)
expect { Rake.application["crawler:#{area_name}"].invoke }.to raise_error(StandardError)
end
end
So in my app I can disable the cache for all tests, which would be ideal, but apparently there are a number of legacy tests that rely on the cache being functional. Is there a way to enable the Rails cache for a single RSpec test?
Something like:
before(:each) do
#cache_setting = Rails.cache.null_cache
Rails.cache.null_cache = true
end
after(:each) do
Rails.cache.null_cache = #cache_setting
end
it 'does not hit the cache' do
...
end
in spec_helper.rb
RSpec.configure do |config|
config.before(:example, disable_cache: true) do
allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::NullStore.new)
end
config.after(:example, disable_cache: true) do
allow(Rails).to receive(:cache).and_call_original
end
end
in xxx_spec.rb
RSpec.describe "a group without matching metadata" do
it "does not run the hook" do
puts Rails.cache.class
end
it "runs the hook for a single example with matching metadata", disable_cache: true do
puts Rails.cache.class
end
end
https://www.relishapp.com/rspec/rspec-core/docs/hooks/filters
I'm having scope issues when using RSpec's `before(:all)' block.
Previously I was using before(:each), which worked fine:
module ExampleModule
describe ExampleClass
before(:each) do
#loader = Loader.new
end
...
context 'When something' do
before(:each) do
puts #loader.inspect # Loader exists
# Do something using #loader
end
...
end
end
end
But switching the nested before(:each) block tobefore(:all) means loader is nil:
module ExampleModule
describe ExampleClass
before(:each) do
#loader = Loader.new
end
...
context 'When something' do
before(:all) do
puts #loader.inspect # Loader is nil
# Do something using #loader
end
...
end
end
end
So why is #loader nil in the before(:all) block, but not in the before(:each) block?
All the :all blocks happen before any of the :each blocks:
describe "Foo" do
before :all do
puts "global before :all"
end
before :each do
puts "global before :each"
end
context "with Bar" do
before :all do
puts "context before :all"
end
before :each do
puts "context before :each"
end
it "happens" do
1.should be_true
end
it "or not" do
1.should_not be_false
end
end
end
Output:
rspec -f d -c before.rb
Foo
global before :all
with Bar
context before :all
global before :each
context before :each
happens
global before :each
context before :each
or not
As per the Rspec documentation on hooks, before :all hooks are run prior to before :each.