Testing with TestDispatch View Source

This library was built to test if forms and links have the expected results in controller tests when they would be submitted or clicked on.

It can be used with or without the implementation of TestSelector.

Testing a form

We will explain the workings with the following form.

<form action="admin/users/create" method="post">
  <input name="_csrf_token" type="hidden" value="AHIqJmEUAxIvGy4HJ3oUGCMjChsLYBZ-SGgy7W1HElh3PKLsffgXXQO6">

  <label>Name</label>
  <input id="user_name" label="Name" name="user[name]" type="text">

  <label>Email</label>
  <input id="user_email" label="Email" name="user[email]" type="email">

  <label>Description</label>
  <textarea id="user_description" label="Description" name="user[description]" type="textarea"></textarea>

  <select id="user_role" name="user[role]">
    <option value="">Select a role</option>
    <option value="admin">Admin</option>
    <option value="moderator">Moderator</option>
  </select>

  <button type="submit">Create new user</button>
</form>

This form is shown on /admin/users/new the entity for this form is user.

To post the form as is we can use the following code:

conn
|> get("/admin/users/new")
|> disptach_form_with(:user)
|> html_response(422) =~ "can't be blank"

A request is made to the router here with the method POST to the action /admin/users/create and with the params %{"user" => %{"name" => "", "email" => "", "description" => "", "roles" => ""}}

The submit_form/3 will return a conn with the response of the controller. In this case it has returned an error because all fields are left blank.

To update the fields while posting, a map can be given that matches the fields.

params = %{
  name: "Marcel",
  email: "marcel@example.com",
  description: "Securing this place",
  role: "moderator"
}

conn
|> get("/admin/users/new")
|> disptach_form_with(params, :user)
|> redirected_to(302) == "/admin/users/2"

Now that the params are given each key is matched to the keys in the form and updated with the value that is provided. If the keys do not match they won't be posted.

### TestSelector

Some forms can be created that only have a submit button and cannot be found with an entity. In this we can use TestSelector to find the form and submit it.

<form action="admin/posts/1" method="delete" test-selector="posts-123-delete-post">
  <input name="_csrf_token" type="hidden" value="AHIqJmEUAxIvGy4HJ3oUGCMjChsLYBZ-SGgy7W1HElh3PKLsffgXXQO6">
  <button type="submit">Remove</button>
</form>

This form can be submitted with

conn
|> get("/admin/posts")
|> submit_form("post-123-delete-post")
|> html_response(200) =~ "Post is deleted"

More documentation on how to use TestSelector can be found in TestSelectors wiki

## Testing Links

Links can also be submitted and currently we can only do this by using TestSelector. For this click_link/3 can be used.

For this example we can take a posts show page on /posts/1

<div>
  <h1>Post</h1>
  <a href="/posts/1" data-method="delete" test-selector="post-123-delete-post">
   Remove
  </a>
  <table>
    <tr>
      <td>This is perfect</td>
      <td>
        <a href="/posts/1/comments/1"
           data-method="post"
           test-value="1"
           test-selector="post-123-upvote-comment" >
          Upvote Comment
      </td>
    </tr>
    <tr>
      <td>A better comment</td>
      <td>
        <a href="/posts/1/comments/2"
           data-method="post"
           test-value="1"
           test-selector="post-123-upvote-comment" >
          Upvote Comment
      </td>
    </tr>
  </table>
</div>

We can now delete this post in the controller test by doing:

conn
|> get('/posts/1')
|> click_link("post-123-delete-post")
|> html_response(200) =~ "Post was deleted"

The click_link parses the page and tries to find post-123-delete-post it take the method by the data-method attribute. The url to click on is taken from the href. In this case a delete request is done to posts/1.

To take a specific element from a list test-values can be used as third argument of click_link/3.

conn
|> get('/posts/1')
|> click_link("post-123-upvote-comment", "1")
|> html_response(200) =~ "Upvoted comment 2"

As expected here a post request is done to /posts/1/comments/2 for the second comment.

If there is no data-method set click_link/3 will do a get request by default.

## Clicking on links in mails

During the tests emails might be sent that we want to integrate in our flow. For that there is receive_mail/2. It expects the conn as the first argument and the found email will be added to the conn as the resp_body. Using the conn combined with the click_link/4 function you can simulate "clicking" on the link in an email.

build_conn()
|> get("/posts/1")
|> click_link("post-123-send-as-mail")
|> receive_mail()
|> click_link("post-123-show")
|> html_response(200)

TestDispatch expects the email to be send with the message {:delivered_email, %{} = email} where the mail should contain at least the to:, from: and subject:, html_body: fields.

When the mail is not received it will raise an error. Specific emails can be targeted by adding the :subject, :to or :from to the second argument of receive mail in a map.

receive_mail(conn, %{submit: "This exact message", to: "this_address@exmaple.com"})