deliveries, and when it is reset
config/environments/test.rb sets config.action_mailer.delivery_method = :test, so emails are appended to ActionMailer::Base.deliveries. The Rails guide notes that the array is reset automatically only in ActionMailer::TestCase and ActionDispatch::IntegrationTest. System tests inherit from neither, so clear it yourself, or a test reads the email from the test before it.
require "application_system_test_case"
class SignupsTest < ApplicationSystemTestCase
setup { ActionMailer::Base.deliveries.clear }
test "sign-up sends a confirmation link" do
visit new_user_registration_path
fill_in "Email", with: "[email protected]"
fill_in "Password", with: "a-long-test-password"
click_on "Sign up"
assert_text "confirmation link"
# Devise sends with deliver_now by default; see deliver_later below.
mail = ActionMailer::Base.deliveries.last
assert_equal ["[email protected]"], mail.to
link = mail.body.encoded[%r{https?://[^"\s]+/confirmation\?[^"\s]+}]
visit link
assert_text "confirmed"
end
enddeliver_later and the test adapter
deliver_later goes through Active Job. The guide is direct about the test queue adapter: it "will not perform jobs until perform_enqueued_jobs is called". So an email sent with deliver_later is only enqueued, deliveries stays empty, and a test that looks for it fails. In mailer and integration tests, wrap the action in perform_enqueued_jobs do … end; assert_emails counts both deliver_now and deliver_later, while assert_enqueued_emails checks only that the job was queued. Since Rails 7.2, all tests respect a queue_adapter set for the test environment, so :inline there runs mail jobs at once in system tests too.
Two staging settings a mailer test never sees
- Interceptors. The Action Mailer guide's own example registers a
SandboxEmailInterceptoron staging that rewritesmessage.toto one address. With it on, every email from staging goes to that address, and a test inbox never receives anything. - default_url_options. Emails need
config.action_mailer.default_url_optionswith ahostto build*_urllinks. Copied from production, it makes staging send production links. Only a real email from staging shows the host.
Against staging with a real inbox
Point Capybara at the deployed staging app and read the email over HTTP. The helper below uses only the standard library.
require "capybara/rspec"
require "selenium-webdriver"
require "net/http"
require "json"
require "securerandom"
Capybara.run_server = false
Capybara.app_host = ENV.fetch("STAGING_URL")
Capybara.default_driver = :selenium_chrome_headless
def last_email(address)
uri = URI("https://api.zyntra.app/emails/last")
uri.query = URI.encode_www_form(email: address)
req = Net::HTTP::Get.new(uri, "X-API-Key" => ENV.fetch("ZYNTRA_API_KEY"))
# The server waits up to 30 s for the first email.
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 45) { |http| http.request(req) }
raise "No email for #{address}: #{res.code}" unless res.is_a?(Net::HTTPSuccess)
JSON.parse(res.body)
end
RSpec.describe "Sign-up on staging", type: :feature do
it "sends a confirmation link to the staging host" do
address = "#{ENV.fetch("ZYNTRA_TEAM_ID")}.rails-#{SecureRandom.hex(4)}@zyntramail.com"
visit "/users/sign_up"
fill_in "Email", with: address
fill_in "Password", with: "a-long-test-password"
click_on "Sign up"
links = last_email(address)["links"].map { |l| l["url"] }.grep(%r{/confirmation\?})
expect(links.size).to eq(1)
expect(links.first).to start_with(ENV.fetch("STAGING_URL"))
visit links.first
expect(page).to have_text("confirmed")
end
endOn staging, deliver_later with a queue backend such as Sidekiq or Solid Queue needs a running worker. If the worker is down, the email is never sent, and this test is the one that notices.