Are spies an appropriate approach to see if Resque methods are being fired?
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 sinceallow(redis_double).to receive(:keys).with('delayed:*').and_return([])
returns an empty array in my example code, meaning thatqueues.each
never iterates once, soResque.remove_queue("queue:#{queue_name}")
is never called and"Clearing default...\n"\
is not return for the expected outputAlso, 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
How to destroy jobs enqueued by resque workers?
If you pop open a rails console, you can run this code to clear out your queue(s):
queue_name = "my_queue"
Resque.redis.del "queue:#{queue_name}"
Resque multiple workers in development mode
You need to add a COUNT
environment variable and then change resque:work
to resque:workers
. For example, to start 3 workers:
bundle exec env rake resque:workers QUEUE='*' COUNT='3'
Rake does not swallow RSpec message output
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
Running a Rake task with parameters
As I mentioned in a comment, the task isn't being invoked from the test because of the way you're stubbing here:
expect(Rake::Task['myapp:seed:all']).to receive(:invoke)
Although this checks whether invoke
was called, it doesn't actually invoke invoke
(actually, it makes the method return nil). To change that, you can either:
- tack on an
and_return(<something>)
- tack on
and_call_original
.
Probably in this case you'd want to use and_call_original
since you want to investigate what actually happens in the task. In order to stub individual method calls in the task, the approach you have been using (expect_any_instance_of(Object).to receive(:system)
) will technically work, but could probably be refactored to be more decoupled from the code.
For example, you could separate each system
call into its own method (available to the rake task), and then call those from the test. Then in order to stub it you only need to pass the method name. If you want, you can then go and unit test each of those methods individually, putting the system
call expectation in there.
I don't recall where exactly but I've heard it advised to not do any acual programming in Rake tasks. Put your code somewhere in your regular codebase, and call those methods from the rake task. This can be seen as an example of a more general pattern which is to refactor large methods into smaller ones. Writing code this way (and also with a functional style, but I won't get into that) makes your life easier when testing.
onto your followup question:
as you can see in the test case's failure message, the only difference between the actual and expected is that one is a regex and the other is a string.
A simple fix for this is to change this line:
expect_any_instance_of(Object).to receive(:system).with(/RAILS_ENV=testing rake db:drop/).and_return(true)
so that the with()
argument is a string, not a regex
How to test code dependent on environment variables using JUnit?
The library System Lambda has a method withEnvironmentVariable
for setting environment variables.
import static com.github.stefanbirkner.systemlambda.SystemLambda.*;
public void EnvironmentVariablesTest {
@Test
public void setEnvironmentVariable() {
String value = withEnvironmentVariable("name", "value")
.execute(() -> System.getenv("name"));
assertEquals("value", value);
}
}
For Java 5 to 7 the library System Rules has a JUnit rule called EnvironmentVariables
.
import org.junit.contrib.java.lang.system.EnvironmentVariables;
public class EnvironmentVariablesTest {
@Rule
public final EnvironmentVariables environmentVariables
= new EnvironmentVariables();
@Test
public void setEnvironmentVariable() {
environmentVariables.set("name", "value");
assertEquals("value", System.getenv("name"));
}
}
Full disclosure: I'm the author of both libraries.
Related Topics
How to Marshal a Hash with Arrays
Vps Apache Config - Invalid Command 'Passengerdefaultruby' After Adding Latest Passenger Gem
Can Ruby Tell If It Is Called from an Interactive Shell or Cron
How to Display Error Messages in a Multi-Model Form with Transaction
Selenium Can't Find Fields with Type Number
Sinatra Doesn't Know This Ditty Even When Default Route Is Implemented with Modular Style
How to Make Private Activities
How to Extract Specific Elements from an Array
Delayedjob: "Job Failed to Load: Uninitialized Constant Syck::Syck"
With Nokogiri I am Getting Error "Initialize': Getaddrinfo: No Such Host Is Known. (Socketerror)"
Rails + Google Calendar API Events Not Created
Proc Throws Error When Used with Do End
Ruby Implementation Win32API Get Mouse Scroll/Wheel Input
Selenium Webdriver & Chrome Driver - Not Able to Run Chrome Driver
How to Target a Specific Commit Sha with Capistrano Deploy
{|_, E| E.Length>1} What Is the Use of Underscore ( _ ) in Ruby