Skip to content

Test Architecture

Paul Danelli edited this page Dec 8, 2021 · 4 revisions

This document is currently in draft, and will be expanded further.

ActiveJob

This application has a large number of asynchronous jobs which require a complex setup and add a large overhead to each test. When running feature or integration tests we should prefer expectations around tests being enqueued with the correct values rather than running the tests themselves. This makes the tests less fragile and more independent. Given that jobs also run asynchronously we should write our expectations to test that the application is in the correct state with the assumption that the job is still to run.

Example

When testing a user sign-up feature, when the sign up form has been filled in correctly, the expectations should be that the user is redirected to the correct page, the database is updated and an email job is enqueued. The next test should not invoke the email job, but instead create a signed_up user and then access their confirmation page. A third test should create a new confirmed user and then sign in. No jobs need to be run. The job class itself should be tested separately.

Code

To test a job has been enqueued wrap the actions you expect to enqueue it in a block:

expect do
  # Some actions
end.to have_enqueued_job(JobClass).with(some_model, attribute: "some value").exactly(:once)

# Or... 

expect { }.to have_enqueued_job(JobClass)

If this is not possible and some jobs will have to run, we should ensure that only jobs specifically relevant to that test are run. This can be achieved by naming the tests in a perform_enqueued_jobs block:

perform_enqueued_jobs(only: [JobClass, AnotherJob]) 
 # Call jobs here
end

If you are not sure precisely which job classes will need to be called, restrict this to the jobs you know will be called. If the test fails, check your test.log for enqueued jobs, and add each required one to the list. The rails_helper.rb makes sure that the :test queue adaptor is always used and the queue is cleared after each test.

config.before do
  ActiveJob::Base.queue_adapter = :test
end

config.after do
  clear_enqueued_jobs
end

If you want a test performed immediately please call ActiveJob::Base.queue_adapter = :inline in your test (or a before block).

FactoryBot

Keeping tests fast

In order to keep the test suite running efficiently we need to ensure that each test commits as little as possible to the database. In general therefore you should use FactoryBot's build or build_stubbed method instead of create whenever possible. This way the objects are loaded only into memory and not into the database. When you want the object to be as close to a created object as possible, use build_stubbed. This will assign an id etc. When you are testing model validation, triggering of jobs etc, you may need to call build instead. When you are not using factories, please use new without save when ever possible.

To confirm that your tests do not use the database unnecessarily run the following command:

docker-compose exec FDOC=1 bin/rspec spec/your_spec.rb

This will output an analysis, with details to help you refactor your test:

[TEST PROF INFO] FactoryDoctor report

Total (potentially) bad examples: 1
Total wasted time: 00:00.046