You just developed a really cool recipe for some food, now you want to share it with your close friend. One way to do that is by copying the recipe URL and sending it to your friend via email. But wouldn't it be even better if you could click a share button, input the email address of your friend, and have the recipe sent to them right from the app? That's what we're going to build in this chapter.

If you worked through the guide in order, you already saw how to share a recipe with a friend in [Chapter 4 - How to approach testing](/guide/how-to-approach-testing/#practice-group-share-recipe-email-mailer). You filled a form, clicked send, and [letter_opener](https://github.com/ryanb/letter_opener?utm_source=minitestrails.com) opened the email preview in the browser. That was manual testing; this chapter converts those manual tests into automated tests so you can ensure the email goes out of the app and is delivered to the correct recipient.

You will not set up real email delivery here, which means no SMTP through Gmail, [Bento](https://bentonow.com?utm_source=minitestrails.com), Sendgrid, or similar services. Production mail needs API keys, DNS, and other things that will be too much to cover in this guide.

<%= render Guide::ChapterProgress.new(
  feature: "Share a recipe by email: `RecipeMailer#share`, integration tests, system smoke tests, a mailer preview, a share form powered by a native share dialog, and focused mailer tests.
  Setup `letter_opener`: Configure `letter_opener` gem in development so emails are opened in the browser instead of sending to a real mailbox.
  Password reset email: A mailer preview and focused mailer test for `PasswordsMailer#reset` (a leftover from [Chapter 12](/guide/testing-authentication/))",
  why: "The Cookbook app is growing features that leave your Rails app and travel to the outside world. Mailer is a great place to start: you share a recipe with your friend via email and test these features in isolation without relying on a real email service.",
  test: "Mailer: TDD for `RecipeMailer#share`, plus leftover tests for `PasswordsMailer#reset`.
  Integration: guest and signed-in alice both POST share from the show page; one email each and a redirect back to the show page.
  System smoke: open the dialog in the browser, send to a friend, land back on show page with the flash message.
  Mailer Preview: open `/rails/mailers` and check the design of recipe share and password reset in the browser without relying on any other gems.
  Development: Go through the \"share a recipe\" and \"password reset\" flow in the browser without relying on real email delivery, powered by letter_opener.",
  later: "Background delivery comes in [Chapter 15](/guide/testing-background-jobs/) while API testing comes in [Chapter 16](/guide/testing-api/)."
) %>

## What you will do in this chapter

1. Write guest and signed-in mailer tests, plus an integration test for the share recipe by email flow (red), then implement `RecipeMailer#share`.
2. Add the mailer HTML template plus a POST-only share route, controller action, and a modal to share the recipe with a native `<dialog>` (green).
3. Add a smoke system test for the share recipe by email flow.
4. Add a mailer test for `PasswordsMailer#reset` from [Chapter 12](/guide/testing-authentication/).
5. Add mailer previews for the share recipe by email and password reset, then preview them both in the browser at `/rails/mailers`.
6. Configure letter_opener, then click through share recipe and password reset flow in the real browser. Both emails open in a new browser tab instead of being sent to a real mailbox.
7. Run the full test suite and commit after everything is green.

## Scenarios to automate

| Scenario | Expected |
| --- | --- |
| Share recipe email | Guest share: one email to `friend@example.com`, heading uses "Someone has shared", title and includes the URL to recipe show page. <br /> Signed-in share: heading includes the email address of the user who shared the recipe |
| Share recipe over HTTP | Guest and signed-in alice can both POST `recipe[recipient_email]`; one email each; redirect back to recipe show page |
| Share recipe in the browser | Guest opens pancakes show, clicks **Share by email**, fills a friend address, submits, lands on the recipe show page with the success flash message |
| Password reset email | `PasswordsMailer#reset` for alice has correct to, from, subject, and a `/passwords/` link |

## What is a mailer?

A mailer is a Ruby class under `app/mailers/` that builds an email. Each **action** (method) sets instance variables that are used in a view under `app/views/<mailer_name>/`. Emails are sent when you call `deliver_now` (send immediately) or `deliver_later` (enqueue a job, [Chapter 15](/guide/testing-background-jobs/)) in any other parts of your app, though normally inside a controller or a background job.

## Share a recipe by email

You liked the recipe you just cooked and now want to share it with your friend. Here is how this feature can be implemented in the app:

1. Button to share the recipe by email in the recipe show page.
2. Form to share the recipe by email in the show page; form wrapped inside a native `<dialog>` element as a modal. Form will have one input field for the email address of the friend who will receive the recipe.
3. Controller action to share the recipe by email inside the existing controller at `app/controllers/recipes_controller.rb`.
4. Mailer to actually send the email when the recipe is shared (new mailer, `share` action and the corresponding view).

Sharing the recipe will be available to everyone in the app (both guests and signed-in users) which means no ownership check (no authorization) in the `app/policies/recipe_policy.rb` file.

You will start by writing the mailer and integration tests (red), then grow the mailer view, the POST share action, and a share dialog partial until all tests are green. Finally to wrap up the testing, you add the system smoke test to ensure the feature works in the browser.

### What counts as working?

| Flow | You might say |
| --- | --- |
| Share (mailer only) | When you share pancakes with `friend@example.com` as a guest, the heading says someone shared it. When alice shares, the heading includes `alice@example.com`. |
| Share (full feature) | Guest and signed-in alice both open pancakes show, POST a friend email, get one email and a redirect. Heading copy stays in the mailer tests. |
| Share (browser smoke) | Guest opens pancakes, clicks **Share by email**, submits a friend address, sees the flash on show. |

### Add mailer scenarios to the test file

Create a new test file  with `nano test/mailers/recipe_mailer_test.rb` and add following scenarios:

```ruby
# test/mailers/recipe_mailer_test.rb
require "test_helper"

class RecipeMailerTest < ActionMailer::TestCase
  include Rails.application.routes.url_helpers

  # Actor: mailer invoked from test (shared by signed-in user)
  # Starting point: recipes(:pancakes); users(:alice) email as sender
  # Action: build and deliver share with sender_email
  # Expected outcome: heading and body include alice's email address
  # test "shares a recipe" do
  # end

  # Actor: mailer invoked from test (shared by guest, no sender email)
  # Starting point: recipes(:pancakes) from fixtures
  # Action: build and deliver share recipe email to friend@example.com
  # Expected outcome: one email; correct to/from; title and path; "Someone has shared" heading
  # test "guest shares a recipe" do
  # end
end
```

Make sure to add `include Rails.application.routes.url_helpers` to the test file, that's required for `recipe_path(recipe)` in the mailer tests you will use later.

### Red: add the mailer tests

Replace the bodies of both share tests with the following:

```ruby
# test/mailers/recipe_mailer_test.rb
  test "shares a recipe" do
    recipe = recipes(:pancakes)
    sender = users(:alice).email_address
    email = RecipeMailer.share(recipe, "friend@example.com", sender)

    assert_emails 1 do
      email.deliver_now
    end

    assert_equal ["friend@example.com"], email.to
    assert_equal ["from@example.com"], email.from
    assert_includes email.subject, sender
    assert_includes email.body.encoded, sender
    assert_includes email.body.encoded, recipe.title
    assert_match recipe_path(recipe), email.body.encoded
  end

  test "guest shares a recipe" do
    recipe = recipes(:pancakes)
    email = RecipeMailer.share(recipe, "friend@example.com")

    assert_emails 1 do
      email.deliver_now
    end

    assert_equal ["friend@example.com"], email.to
    assert_equal ["from@example.com"], email.from
    assert_includes email.subject, "Someone has shared"
    assert_includes email.body.encoded, "Someone has shared"
    assert_includes email.body.encoded, recipe.title
    assert_match recipe_path(recipe), email.body.encoded
  end
```

<%= render Shared::Tip.new(
  title: "New assertion",
  markdown: <<~MD
    **`assert_emails 1 do ... end`** asks "did exactly one email send while executing the code inside this block?"
      Syntax: wrap the delivery call (`email.deliver_now` or `deliver_later` with the test adapter). The block must trigger delivery. If the count is wrong, the test fails with how many emails actually sent.
  MD
) %>

This is what's happening in the code above:

- Both examples use `RecipeMailer.share` to build the email.
- The first example passes Alice's email as the third argument (edge case: a signed-in share). The heading includes the sender.
- The second example covers the guest edge case. It passes two arguments to the mailer, so `sender_email` stays `nil` and the heading uses "Someone has shared..." because we don't know who shared the recipe.
- `email.deliver_now` inside `assert_emails` is the moment email is sent in the test environment.
- `assert_equal` on `email.to` and `email.from` checks headers of the email being sent.
- `assert_includes` on subject and `email.body.encoded` checks the heading copy for email subject and the body.
- `assert_match recipe_path(recipe), email.body.encoded` checks the recipe show path in the email. That needs `include Rails.application.routes.url_helpers` on the test class because `ActionMailer::TestCase` does not include them by default.

Run the mailer file:

```bash
bin/rails test test/mailers/recipe_mailer_test.rb
```

The test should fail with an error like this:

```bash
E

Error:
RecipeMailerTest#test_shares_a_recipe:
NameError: uninitialized constant RecipeMailerTest::RecipeMailer
    test/mailers/recipe_mailer_test.rb:8:in 'block in <class:RecipeMailerTest>'

E

Error:
RecipeMailerTest#test_guest_shares_a_recipe:
NameError: uninitialized constant RecipeMailerTest::RecipeMailer
    test/mailers/recipe_mailer_test.rb:25:in 'block in <class:RecipeMailerTest>'
```

The test fails because the `RecipeMailer` has not been created/defined yet in the project. That's a red for the `RecipeMailer`.

### Red: add the integration tests

Next up is adding the integration tests for the share recipe by email. These tests will ensure the email is sent and the user is redirected to the recipe show page; testing the full feature.

Open `test/integration/recipes_integration_test.rb` and append the following scenario comments to the end of the file:

```ruby
# test/integration/recipes_integration_test.rb
class RecipesIntegrationTest < ActionDispatch::IntegrationTest
  # ... existing tests ...

  # Actor: signed-in alice
  # Starting point: recipes(:pancakes) on show
  # Action: GET show, modal with share form present, POST friend email while signed in
  # Expected outcome: one email; redirect to show
  # test "shares a recipe" do
  # end

  # Actor: guest (not signed in)
  # Starting point: recipes(:pancakes) on show
  # Action: POST friend email
  # Expected outcome: one email; redirect to show
  # test "guest shares a recipe" do
  # end
end
```

Replace the bodies of both tests with the following:

```ruby
# test/integration/recipes_integration_test.rb
  test "shares a recipe" do
    sign_in_as users(:alice)
    recipe = recipes(:pancakes)

    get recipe_url(recipe)
    assert_response :success
    assert_select "dialog"
    assert_select "input[name='recipe[recipient_email]']"

    assert_emails 1 do
      post share_recipe_url(recipe), params: { recipe: { recipient_email: "friend@example.com" } }
    end

    assert_redirected_to recipe_url(recipe)
    follow_redirect!
    assert_response :success
    assert_match recipe.title, response.body
  end

  test "guest shares a recipe" do
    recipe = recipes(:pancakes)

    assert_emails 1 do
      post share_recipe_url(recipe), params: { recipe: { recipient_email: "friend@example.com" } }
    end

    assert_redirected_to recipe_url(recipe)
    follow_redirect!
    assert_response :success
    assert_match recipe.title, response.body
  end
```

This is what's happening in the code above:

- `test "shares a recipe"` uses `sign_in_as users(:alice)` from [Chapter 12](/guide/testing-authentication/#sign-in-helper-for-tests) to sign Alice into the app. The guest example has no `sign_in_as`.
- The first example `test "shares a recipe"` also checks the response status and the presence of the share form in the dialog. One of such checks is enough per feature so guest example doesn't repeat those checks.
- `assert_emails` proves recipe shared email is sent. It does not re-check heading copies or email content because you have already covered those in mailer tests.
- `assert_redirected_to` and `assert_match recipe.title, response.body` ensure the user lands back on the recipe show page and sees the recipe title.

Run both tests for the share recipe feature:

```bash
bin/rails test test/integration/recipes_integration_test.rb -i "/shares_a_recipe/"
```

You should see a failure and an error like this:

```bash
F

Failure:
RecipesIntegrationTest#test_shares_a_recipe [test/integration/recipes_integration_test.rb:282]:
Expected at least 1 element matching "dialog", found 0.
Expected 0 to be >= 1.

E

Error:
RecipesIntegrationTest#test_guest_shares_a_recipe:
NoMethodError: undefined method 'share_recipe_url' for an instance of RecipesIntegrationTest
    test/integration/recipes_integration_test.rb:304:in 'block (2 levels) in <class:RecipesIntegrationTest>'
    test/integration/recipes_integration_test.rb:303:in 'block in <class:RecipesIntegrationTest>'
```

Both of these errors are related to the fact that the share recipe feature is not implemented yet. One fails due to the missing dialog markup and the other due to the missing route for the share action.

That is **red** for the integration layer. You will fix it in the next steps.

### Green: Wire the mailer for sharing the recipe

You have two things to fix: the mailer and the integration test. First, you will generate the mailer for sharing the recipe by email to fix the mailer test. Then wire the share recipe by email feature (controller + views) to fix the integration test.

#### Generate the mailer for sharing the recipe

Generate the mailer action and the corresponding view file for sharing the recipe with the following command:

```bash
bin/rails generate mailer RecipeMailer share --no-test-framework
```

The generator adds the mailer class with the `share` action at `app/mailers/recipe_mailer.rb`, the mailer view file at `app/views/recipe_mailer/share.html.erb`, and a mailer preview at `test/mailers/previews/recipe_mailer_preview.rb`. You already wrote the mailer tests by hand so passing `--no-test-framework` tells the generator to skip the mailer test file.

#### Implement `RecipeMailer#share`

Open `app/mailers/recipe_mailer.rb` and replace the content of the whole file with the following:

```ruby
# app/mailers/recipe_mailer.rb
class RecipeMailer < ApplicationMailer
  def share(recipe, recipient, sender_email = nil)
    @recipe = recipe
    @sender_email = sender_email

    @heading =
      (
        if @sender_email
          "#{@sender_email} has shared a recipe with you"
        else
          "Someone has shared a recipe with you"
        end
      )

    mail to: recipient, subject: @heading
  end
end
```

This is what's happening in the code above:

- `@recipe` sets the recipe instance variable so it can be accessed later in the mailer view to display the recipe title and the URL to the recipe show page.
- `sender_email` is the email address of the user who shared the recipe. If `sender_email` is not provided, the subject will be "Someone has shared a recipe with you".
- `mail to: recipient, subject: ...` sets headers for the email being sent.

`RecipeMailer` inherits from `ApplicationMailer` at `app/mailers/application_mailer.rb`, so it inherits `default from: "from@example.com"` unless you override `from:` inside the `RecipeMailer` class. The application mailer should look like the following with the default from address and using the `mailer` layout:

```ruby
# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: "from@example.com"
  layout "mailer"
end
```

"mailer" layout is a Rails default layout for email templates. You can find and modify it at `app/views/layouts/mailer.html.erb` (and `app/views/layouts/mailer.text.erb` for text emails).

<%= render Shared::Tip.new(
  title: "Tip",
  markdown: <<~MD
    You should change the default `from` address when you are working on a production app; pointing to a support or marketing email address of your company. It's fine to keep as is for our case since we are only using this app for learning purposes.
  MD
) %>

#### Update the mailer view

Open `app/views/recipe_mailer/share.html.erb` and replace the content with the following:

```erb
<%%# app/views/recipe_mailer/share.html.erb %>
<p><%%= @heading %></p>

<h1><%%= @recipe.title %></h1>

<p>
  You can view the recipe by clicking the link below:

  <%%= link_to "View recipe", recipe_url(@recipe) %>
</p>
```

You can leave the text version of the email (`app/views/recipe_mailer/share.text.erb`) as is or update it to match the HTML version. I normally don't use it so I end up deleting it in production applications.

This is what's happening in the code above:

- `@heading` is the heading of the email you previously set in the mailer action. Guest and signed-in mailer tests assert on that string in subject and body.
- `recipe_url(@recipe)` emits a full URL (e.g. `https://localhost:3000/recipes/1`) required for real emails. You don't use `recipe_path` here because it doesn't include the domain name, for example it only returns `/recipes/1`.

Run the mailer test again:

```bash
bin/rails test test/mailers/recipe_mailer_test.rb
```

You want 0 failures and 0 errors. That is **green** for the mailer layer.

<%= render Guide::SupportCta.new(
  variant: :mid_chapter,
  site_metadata: site.data.site_metadata,
  milestone_hook: "RecipeMailer#share is green in isolation: subject, heading, body, and recipient match what the mailer test asserts.",
  headline: "The email content is proven before the share button exists.",
  body_text: "You wrote the mailer test first, watched it fail, then filled in the mailer and HTML template until assertions passed. If that habit is sticking, consider supporting this guide by sponsoring it or buying me a drink."
) %>

### Green: Wire the share form and fix integration test

The mailer test proved the content and message of the email. You will now wire the share feature to fix the integration test. The share feature will be implemented as follows:

- Member route on recipes to handle the share action (`post :share`).
- Controller action accessible to everyone in the app that sends the email to the recipient.
- View partial that holds the share form inside a HTML native `<dialog>`.

#### Routes

Append the following lines to `config/routes.rb` just below `resources :steps, only: :destroy`:

```ruby
# config/routes.rb
resources :recipes do
  # ... existing code ...
  resources :steps, only: :destroy

  member { post :share }
end
```

This will add `recipes/:id/share` route to the app for sharing the recipe by email.

#### Controller: Add the share action

Update the `RecipesController` at `app/controllers/recipes_controller.rb` to include the share action:

```ruby
# app/controllers/recipes_controller.rb
class RecipesController < ApplicationController
  # ... existing code ...

  def share
    recipient = params[:recipe][:recipient_email]
    sender = current_user&.email_address

    RecipeMailer.share(@recipe, recipient, sender).deliver_now
    redirect_to @recipe, notice: "Recipe shared with #{recipient}."
  end

  private

  # ... existing code ...
end
```

This is what's happening in the code above:

- `params[:recipe][:recipient_email]` is the address of the recipient sent from the share form. We are not using strong params here because it's just a single field and the one we know.
- Email address of the sender can be nil for guests so we are using `&.` (safe navigation operator) to avoid NoMethodError. Signed-in users pass their address into the subject.
- `deliver_now` sends the email to the recipient before redirecting the user back to the recipe show page.

<%= render Shared::Tip.new(
  title: "Synchronous vs Async delivery",
  markdown: <<~MD
    `deliver_now` is a synchronous delivery meaning the controller will wait for the email to be sent before returning the response.
    
    [Chapter 15](/guide/testing-background-jobs/) switches to `deliver_later` so Active Job queues the mailer and controller returns the response immediately. That is async delivery because the controller doesn't wait for the email to be sent.
  MD
) %>

Anyone in the app should be able to share a recipe by email. To enable the public access you need to update the `allow_unauthenticated_access` to include `share`. And while you are at it, make sure to also add the `share` action to the `set_recipe` list so the recipe instance is loaded for the share action:

```ruby
# app/controllers/recipes_controller.rb
class RecipesController < ApplicationController
  allow_unauthenticated_access only: %i[index show share]
  before_action :set_recipe, only: %i[show edit update destroy share]

  # ... existing code ...
end
```

#### Share dialog partial

For the view part, there are two ways to approach the share feature:

1. Create a new page that holds the share form. And submitting the form will send the email to the recipient.
2. Add a share form inside a modal component using the HTML native `<dialog>` element and render it directly inside the recipe show page.

In this guide, we will go with the second approach because it's simpler in terms of implementation and better in terms of the UX perspective. Simple because you don't need to create a new page, add a new route or controller action for it. Better UX because it makes the share form available right where the user is without navigating to the extra page.

Create a new file with `nano app/views/recipes/_share_modal.html.erb` and add the following markup for holding the share form inside a HTML native `<dialog>`:

```erb
<%%# app/views/recipes/_share_modal.html.erb %>
<button type="button" command="show-modal" commandfor="share-recipe-<%%= recipe.id %>">
  Share by email
</button>

<dialog id="share-recipe-<%%= recipe.id %>">
  <h2>Share <%%= recipe.title %></h2>

  <%%= form_with url: share_recipe_path(recipe), scope: :recipe, method: :post do |form| %>
    <fieldset class="fieldset">
      <%%= form.label :recipient_email, "Friend's email", class: "label" %>
      <%%= form.email_field :recipient_email, class: "input input-bordered w-full", required: true %>
    </fieldset>

    <%%= form.submit "Send recipe", class: "btn btn-primary" %>
  <%% end %>

  <form method="dialog">
    <button value="cancel">Cancel</button>
  </form>
</dialog>
```

Then render it inside the recipe show page at `app/views/recipes/show.html.erb` by adding the following line just below `<%%= render @recipe, for_show: true %>`:

```erb
<%%# app/views/recipes/show.html.erb %>
<%%= render @recipe, for_show: true %>
<%%= render "recipes/share_modal", recipe: @recipe %>

<%%# ... existing code ... %>
```

This is what's happening in the markup above:

- `command="show-modal"` and `commandfor` tell the browser to open that `<dialog>` when the button is clicked. `commandfor` targets the dialog to open by its ID. Both of these attributes come from the native HTML dialog API so you don't need any Stimulus controller or `onclick` handler for the dynamic modal (open/close).
- `form_with ... scope: :recipe` posts the recipient email field inside the "recipe" attribute e.g. `recipe[recipient_email]`, which matches `params[:recipe][:recipient_email]` in the controller.
- A nested `<form method="dialog">` near the end of the file with the Cancel button closes the dialog. That also comes from the native HTML dialog.

### Green: Run both mailer and integration tests

Run the mailer and integration tests for the recipe share feature together. You want 0 failures and 0 errors:

```bash
bin/rails test test/mailers/recipe_mailer_test.rb test/integration/recipes_integration_test.rb
```

### System test: a small smoke test for the share feature

Integration test already proved the feature for sharing the recipe with a friend works end to end. One small smoke test further strengthens the test suite by ensuring the feature works in a real browser.

Open `test/system/recipes_test.rb` and append the following scenario to the end of the file:

```ruby
# test/system/recipes_test.rb
  # Actor: guest (browser)
  # Starting point: recipes(:pancakes) on show
  # Action: open share dialog, fill friend's email, submit
  # Expected outcome: redirect to recipe show page with a flash message about sharing and a recipe title
  # test "shares a recipe" do
  # end
```

Replace the body of `test "shares a recipe"` with the following:

```ruby
# test/system/recipes_test.rb
  test "shares a recipe" do
    recipe = recipes(:pancakes)

    visit recipe_url(recipe)
    click_on "Share by email"
    fill_in "Friend's email", with: "friend@example.com"
    click_on "Send recipe"

    assert_text "Recipe shared with friend@example.com"
    assert_text recipe.title
  end
```

This is what's happening in the code above:

- `click_on "Share by email"` fires the native `command="show-modal"` button so the dialog opens in the browser.
- `fill_in "Friend's email"` uses the label from the share dialog partial to fill the recipient email field.
- For the outcome, you assert the flash message about sharing the recipe and the title of the recipe is still on the show page.

Run the system test, you want 0 failures and 0 errors:

```bash
bin/rails test test/system/recipes_test.rb -i test_shares_a_recipe
```

## Password reset mailer test

[Chapter 12](/guide/testing-authentication/#reset-a-forgotten-password) already proved that a known email **enqueues** `PasswordsMailer#reset` with `assert_enqueued_email_with`. We postponed the mailer test for the password reset feature in that chapter so all mailer tests could be added at once.

Create a new file with `nano test/mailers/passwords_mailer_test.rb` and add the following:

```ruby
# test/mailers/passwords_mailer_test.rb
require "test_helper"

class PasswordsMailerTest < ActionMailer::TestCase
  include Rails.application.routes.url_helpers

  test "reset a password by email" do
    freeze_time do
      user = users(:alice)
      email = PasswordsMailer.reset(user)

      assert_emails 1 do
        email.deliver_now
      end

      assert_equal [user.email_address], email.to
      assert_equal ["from@example.com"], email.from
      assert_equal "Reset your password", email.subject
      assert_match edit_password_path(user.password_reset_token), email.body.encoded
    end
  end
end
```

Here is what's happening in the code above:

- [`freeze_time`](https://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html?utm_source=minitestrails.com#method-i-freeze_time) freezes the time to the current time so the test is deterministic and doesn't depend on the real time. This is needed because the password reset token is based on the current time due to which test will otherwise fail because `edit_password_path(user.password_reset_token)` won't match with the one in the email body.
- Other assertions are mostly similar to the ones in the previous mailer test and assert email delivery plus the content of the email.

Run the mailer tests, you want 0 failures and 0 errors:

```bash
bin/rails test test/mailers/passwords_mailer_test.rb
```

## Email delivery in the test environment

Rails uses the `:test` delivery method by default in the test environment for email delivery. If you open `config/environments/test.rb` you will see the following line:

```ruby
# config/environments/test.rb
config.action_mailer.delivery_method = :test
```

With the `:test` delivery method, emails do not leave your machine. Instead, they accumulate in `ActionMailer::Base.deliveries` when something calls `deliver_now` (or `deliver_later`) and `assert_emails` asserts on that count. This is why all mailer tests passed from the beginning without any additional configuration (after wiring the implementation).

## Preview emails in the browser

Your mailer tests prove recipient, subject, and body strings but they do not tell you if the design side of the email is good or not. For example, whether the heading wraps oddly, whether the link looks like a button or just a plain URL, or whether the HTML feels cramped once you stare at it. That is what mailer previews are for; you can view the design of the email in the browser without needing to send them to a real email address.

After you define mailer specific files at `test/mailers/previews/`, you can visit the special URL `/rails/mailers` in the browser and see the preview of the email. Rails can render any mailer action in the browser while you work in development without any other additional gem setup; this feature is provided by default and called "Mailer Previews".

This is different from `letter_opener`, which you wire later in this chapter. `letter_opener` opens the email after a real delivery from the running app (catches the email after it is sent from the Rails app but before the real delivery happens). But the mailer preview just builds the mail object directly from a small Ruby class under `test/mailers/previews/` and displays the built email in the browser. I like to use both of them in projects I work on.

### Preview the share recipe email

Create a new preview file with `nano test/mailers/previews/recipe_mailer_preview.rb` and add the following:

```ruby
# Preview all emails at http://localhost:3000/rails/mailers/recipe_mailer
class RecipeMailerPreview < ActionMailer::Preview
  # Preview this email at http://localhost:3000/rails/mailers/recipe_mailer/share
  def share
    recipe = Recipe.new(id: 1, title: "Fluffy pancakes")
    sender = "alice@example.com"

    RecipeMailer.share(recipe, "friend@example.com", sender)
  end

  # Preview this email at http://localhost:3000/rails/mailers/recipe_mailer/share_by_guest
  def share_by_guest
    recipe = Recipe.new(id: 1, title: "Fluffy pancakes")

    RecipeMailer.share(recipe, "friend@example.com")
  end
end
```

Each public method on the preview class becomes one preview. The method must return a mail object e.g. `RecipeMailer.share(...)`. You can add as many preview actions as you want based on the different edge cases you want to view in the browser. Here, you have added two: one for a signed-in user and another for a guest. Normally, you would only have one preview action per mailer.

The "share" preview action is only building a `Recipe` object with the required attributes. `Recipe.new(id: 1, title: "Fluffy pancakes")` is enough for this preview since the view only needs `title`, `recipe_url`. We are also passing an `id` because `recipe_url` needs it to build the show page URL `/recipes/:id`. The best practice when building a record required for the preview is to skip optional attributes you are not rendering. That's the same habit as a focused test that only declares what's needed for that test to pass.

For the record to send to the actual mailer, you could also find and load them from the development database but I generally avoid that because it can break the preview if the record is not found. For example, a fresh app, a wiped DB, or a deleted pancakes row would break the preview for the share recipe by email.

To view these previews in the browser, follow the steps below:

1. Start the app if it is not already running (`bin/dev`).
2. Open [http://localhost:3000/rails/mailers](http://localhost:3000/rails/mailers) in the browser.
3. Click **RecipeMailer** → **share**, or go straight to [http://localhost:3000/rails/mailers/recipe_mailer/share](http://localhost:3000/rails/mailers/recipe_mailer/share).
4. Click **RecipeMailer** → **share_by_guest**, or go straight to [http://localhost:3000/rails/mailers/recipe_mailer/share_by_guest](http://localhost:3000/rails/mailers/recipe_mailer/share_by_guest).

<%= render Shared::Tip.new(
  title: "Heads up!",
  markdown: <<~MD
    If you click on the links inside the email preview, you might get a "Not Found" error. That's because the preview is only using dummy data and not the actual record from the development database.
  MD
) %>

### Preview the password reset email

For the password reset, you don't need to create a new preview file as it has already been created by the authentication generator. You can just open the existing preview file at `test/mailers/previews/passwords_mailer_preview.rb` and replace the content with the following:

```ruby
# Preview all emails at http://localhost:3000/rails/mailers/passwords_mailer
class PasswordsMailerPreview < ActionMailer::Preview
  # Preview this email at http://localhost:3000/rails/mailers/passwords_mailer/reset
  def reset
    user =
      User.new(
        email_address: "alice@example.com",
        password: "password",
        password_confirmation: "password"
      )

    PasswordsMailer.reset(user)
  end
end
```

This is what's happening in the code above:

- First the user record is built with the required attributes similar to how you built the recipe record for the share preview.
- `PasswordsMailer.reset(user)` builds the mail object that will be used by the mailer preview to display the email in the browser.

To preview the password reset email, follow the steps below:

1. Start the app if it is not already running (`bin/dev`).
2. Open [http://localhost:3000/rails/mailers](http://localhost:3000/rails/mailers) in the browser.
3. Click **PasswordsMailer** → **reset**, or go straight to [http://localhost:3000/rails/mailers/passwords_mailer/reset](http://localhost:3000/rails/mailers/passwords_mailer/reset).

### When a preview earns its keep

Reach for a preview when you care about how the email looks:

- You are editing HTML or CSS in `app/views/`.
- You want to check spacing, heading hierarchy, or a long title.
- You want to see how the email looks for both HTML (.html.erb) and Text (.text.erb) versions.
- You want to show the design of the email to a teammate without wiring SMTP.

Just keep it in mind that the preview is a development convenience and not an automated way to ensure email is being sent correctly. The tests are still the source of truth for that.

## Open emails in the browser with letter_opener

Mailer previews render a template in a special page inside the Rails app without going through the actual form submission. This doesn't test or let you know if the email is actually delivered or not. That's where `letter_opener` comes in; it opens the email in a new browser tab after it is sent from the Rails app. This will ensure the email leaves your app, though it still doesn't tell you if the email was received by the recipient or not. You will need to use a real email provider and SMTP configurations to know that for sure.

We will not discuss or configure the real delivery of the email here (that will be a separate topic) but you can ensure the email leaves your app by configuring the `letter_opener` gem.

Add the gem to the development group in your `Gemfile` just below the `web-console` gem:

```ruby
# Gemfile

# ... existing gems ...

group :development do
  gem "web-console"
  gem "letter_opener"
end

# ... existing gems ...
```

Then install it with the following command:

```bash
bundle install
```

Finally, append the following configuration to the end of `config/environments/development.rb` so the `delivery_method` for the development environment points to `letter_opener` and emails are intercepted by the gem:

```ruby
# config/environments/development.rb
Rails.application.configure do
  # ... existing configurations ...

  # Open the email in the browser instead of sending it to a real mailbox.
  config.action_mailer.delivery_method = :letter_opener
end
```

That's it! Now if you send an email from the app, it will be opened in a browser tab instead of being delivered to a real mailbox.

<%= render Shared::Tip.new(title: "Heads up!", markdown: <<~MD
  Make sure to restart the Rails server (`bin/dev`) to ensure the changes in gem and development configurations are picked up.
  
  You might get an error or the email might not be opened in the browser if you don't restart the server.
  MD
) %>

### Share a recipe in the browser

Time to see the `letter_opener` in action!

<%= render Guide::LoadDevelopmentFixtures.new %>

To view the email for the share recipe, follow the steps below:

1. Run the Rails server if it is not already running (`bin/dev`).
2. Open the recipe list page at `http://localhost:3000/recipes` and click on the "Fluffy pancakes" recipe to open the show page (you don't need to sign in).
3. Click **Share by email** button just below the "Destroy this recipe" button, enter `friend@example.com` in the dialog, submit **Send recipe**.
4. Confirm you land back on the show page with a success message and a new browser tab with recipe shared email is opened by `letter_opener`.

### Reset a password in the browser

Steps are similar to the share recipe for the password reset as well:

1. Open the sign-in page (`/session/new`).
2. Click **Forgot password?**.
3. Enter `alice@example.com` and click **Email reset instructions**.
4. Confirm you land on the sign-in page with the success flash message.
5. `letter_opener` will open a new browser tab with the password reset email. In that tab, confirm the subject and body mention a password reset, then click the reset link in the email.
6. On the edit password page, enter a new password twice (for example `new-password`) and click **Save**.
7. Sign in as Alice with `alice@example.com` and that new password. Confirm you see **Sign out**.

If you want Alice's fixture password back for later manual checks, reload development fixtures back again (`bin/rails db:fixtures:load`) or set the password again through the same reset flow.

## Commit your work

Run the full suite before you commit:

```bash
bin/rails test:all
```

You want 0 failures and 0 errors across model, mailer, integration, and system tests. Then commit your changes:

```bash
git add .
git commit -m "Add recipe share mailer, previews, and password reset mailer coverage"
```

Small commits make it easier to roll back the mailer, the route, or the integration test independently if something regresses later.

## What is next

Share still calls `deliver_now`, so the browser waits until the mailer finishes before the redirect returns. This is fine for the learning purpose of this chapter but in production applications, this can cause a significant delay in the response time especially when delivery is slow or you add more outbound work.

[Chapter 15](/guide/testing-background-jobs/) switches share recipe email to `deliver_later` so Active Job queues the mailer and the request can return right away. Next chapter will also teach you how to write a custom job by exporting every recipe in the app to a file.

Continue to **[Testing background jobs](/guide/testing-background-jobs/)**.
