Are Spies an Appropriate Approach to See If Resque Methods Are Being Fired

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 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

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:

  1. tack on an and_return(<something>)
  2. 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



Leave a reply



Submit