Need controller side effects testing advise

Despite the tendency to reduce the amount of logic in controllers to a bare minimum, the logic to e.g. schedule background jobs is triggered by a chain that starts in the controller by saving or updating a model.

What is the best way to make sure a certain job was scheduled?

Looking at Rails Guides’ What to include in your Functional Tests](Testing Rails Applications — Ruby on Rails Guides), and Integration testing, it’s not obvious where such a test would fit better.

What practices do you follow?

I think functional tests should focus on requests (and their parameters) and responses, not so much on what ends up happening within the models/jobs.

If you want to add a test to make sure that a job is scheduled, I would add it to the model that is supposed to do that. Considering your description of your application, it seems that one of the model’s responsibilities is to schedule a job when some conditions are met.

It’s not that I’m struggling with a single application I’m working on right now. In general, the way to cover this with tests has no common practices.

So, the controller triggers update/create for the model. But what if that model has no way to be checked through any of the controllers? Something like UserActivity, or DownloadCount. I hope you got the idea to figure out an better example.

Neither of Integration/Functional tests (Request/Controller specs in RSpec terminology) is recommended to check the side effects on the database.

A citation from Functional tests:

You should test for things such as:

  • was the web request successful?
  • was the user redirected to the right page?
  • was the user successfully authenticated?
  • was the appropriate message displayed to the user in the view?
  • was the correct information displayed in the response?

Integration tests have helpers to work with the page elements.

There is no way to understand if the model was created/updated => no way to make sure the background job was scheduled, even if it’s covered in model’s callback test.

So, the controller triggers update/create for the model. But what if that model has no way to be checked through any of the controllers?

If you need to test the interaction of models, controllers, and background jobs, I believe you need an integration test.

As a rule of thumb, I try to not check model-level behavior in my controller specs. I try to keep it as high-level as possible.

Neither of Integration/Functional tests (Request/Controller specs in RSpec terminology) is recommended to check the side effects on the database.

This can be a slippery slope: You want to test that values are written/updated in the database, but you don’t want to test that ActiveRecord does what it’s supposed to do.

Despite the tendency to reduce the amount of logic in controllers to a bare minimum, the logic to e.g. schedule background jobs is triggered by a chain that starts in the controller by saving or updating a model.

If this is the case that I’m trying to test, I would add an expectation to my model spec: “After I create a notification record in the database, I expect a job to be enqueued for the NotificationJob.”

While best practices may say don’t test side effects in feature/integration specs, I do it all the time.

For example, if a user action should enqueue an email being sent via a background worker, I’d do something like

      expect do
        perform_enqueued_jobs do
          click_button 'Approve'
        end
      end.to change { job.reload.status }.from('review').to('complete')

      expect(page).to have_content('Service call marked as approved')

      # email is job complete email
      expect(ActionMailer::Base.deliveries.size).to eq(1)

      expect(current_email).to have_subject("Service Call Completed ##{job.id}")
      expect(current_email).to deliver_to(client.email)

I personally don’t see anything wrong with this. It tests the full stack and I’m able to assert that what I expected to happen, did occur. :man_shrugging:t2:

If you’re looking for more general advice about where and how to test, The Practical Test Pyramid is a good article. I also think every developer should check out Martin Fowler’s website and subscribe to his RSS feed. It’s a treasure trove of practical Software Engineering advice.

1 Like

Have you considered simply not doing that? When I am integration-testing logic that may contain background jobs, I tell the test example to execute all jobs inline. The specific job, to me, is an implementation detail that shouldn’t matter at an integration level.

# spec_helper.rb
config.around(:each) do |example|
  if (mode = example.metadata[:sidekiq])
    Sidekiq::Testing.send("#{mode}!") { example.run }
  else
    example.run
  end
end
# spec example
it "does something", sidekiq: :inline do
  expect(subject.("foo")).to do_something
end
1 Like