Phoenix has a nice set of helpers for testing HTML responses. The default Phoenix generators offer a great way to get a quick start on testing HTML, but this guide hopes to dive in a bit further.
In this post we’ll go over:
- Using the Phoenix helpers to test your controllers and their responses.
- Some common errors, what they mean, and how to fix them.
- How to use ExMachina to generate test data.
- How to use regular functions and the Elixir
|>
to customize test data generated by ExMachina.
Setup
Follow the Phoenix installation instructions and then start a new project with
mix phoenix.new my_blog
. Once all the dependencies are installed and you’ve
created your database with mix ecto.create
, we can get started.
Testing the list of blog posts
Let’s create a file at test/controllers/post_controller_test.exs
defmodule MyBlog.PostControllerTest do
use MyBlog.ConnCase
setup do
{:ok, conn: Phoenix.ConnTest.conn()}
end
test "lists all blog posts", %{conn: conn} do
posts = create_pair(:post)
conn = get conn, post_path(conn, :index)
for post <- posts do
assert html_response(conn, 200) =~ post.title
end
end
end
In the setup
block we are generating a conn to be used. Note that when Phoenix
1.1 is released this will be done automatically by MyBlog.ConnCase
.
The second part of the tuple returned in the setup
block is available as the
second argument to your test. We pattern match against it to make the conn
available to our test.
We are calling the ExMachina
function called create_pair/3
that will
insert 2 posts. We could have used build_pair/3
if we didn’t need to save to
the database, but in this case we need the posts saved.
Next we make a request to our controller and then check that the response has the titles of each of our blog posts.
The html_response
helper makes sure that the response was successful (status
code 200) and returns the HTML body. Using =~
checks that whatever is on the
right hand side (post.title
) is found somewhere in the string on the left hand
side (the HTML body).
Now let’s try to run the tests with mix test
. You should get an error like
this:
** (CompileError) test/controllers/post_controller_test.exs:5: function create_pair/1 undefined
Setting up ExMachina
To fix that, let’s add ExMachina and add our factory.
Add {:ex_machina, "~> 0.5.0"}
to your mix.exs
dependencies. It will look
something like this:
defp deps do
[{:ex_machina, "~> 0.5.0"},
{:phoenix, "~> 1.0.3"},
# Other dependencies...
]
end
Then add :ex_machina
to your list of applications. In a default Phoenix app
this will look something like this:
def application do
[mod: {MyBlog, []},
applications: [:ex_machina, :phoenix, :phoenix_html, :cowboy, :logger,
:phoenix_ecto, :postgrex]]
end
Now let’s create our factory for generating test data. We’ll do this in
lib/my_blog/factory.ex
. Be sure to make the extension .ex
and not .exs
or
it will not be compiled and you will get errors. See the ExMachina README for
the latest.
defmodule MyBlog.Factory do
use ExMachina.Ecto, repo: MyBlog.Repo
def factory(:post) do
%MyBlog.Post{
title: sequence(:title, &"My Post #{&1}"),
body: "This is my post about something",
author: "Me!"
}
end
end
This is setting up a factory called :post
that is built using the
MyBlog.Post
struct. The body and author are static, but the title uses the
sequence/2
to ensure that the title is always unique. Whenever a post is built
or created n
will be incremented by 1. The &("My Post #{&1}")
syntax is a
shorthand for fn(n) -> "My Post #{n}" end
. You can use whichever one you are
most comfortable with.
Let’s add our factory to MyBlog.ConnCase
so that our factory functions are
available in all of our controller tests.
In test/support/conn_case.ex
add import MyBlog.Factory
in the using
block.
using do
quote do
# ...other code automatically generated by Phoenix when you start a project
import MyBlog.Factory
end
end
Adding the Post model
If we run mix test
we will now get:
== Compilation error on file test/support/factory.ex ==
** (CompileError) test/support/factory.ex:5: MyBlog.Post.__struct__/0 is
undefined, cannot expand struct MyBlog.Post
This means that we have not set up a struct called MyBlog.Post
. Let’s create a
model and migration to add it. Run:
mix phoenix.gen.model Post posts title:string body:text author:string
mix ecto.migrate
Adding routes and controller action
Running mix test
will now show us:
** (CompileError) test/controllers/post_controller_test.exs:11: function posts_path/2 undefined
Let’s go ahead and add our route and controller action now.
In web/router.ex
add your route to the default browser scope
scope "/", MyBlog do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
resources "/posts", PostController, only: [:index]
end
Then create web/controllers/post_controller.ex
defmodule MyBlog.PostController do
use MyBlog.Web, :controller
def index(conn, _params) do
end
end
Get and display posts
Let’s finish off the controller and make sure we are rendering our posts.
defmodule MyBlog.PostController do
use MyBlog.Web, :controller
alias MyBlog.Post
def index(conn, _params) do
# MyBlog.Repo is aliased for you when you `use MyBlog.Web, :controller`
posts = Repo.all(Post)
render(conn, "index.html", posts: posts)
end
end
If we run mix test
we’ll see something like this:
undefined function: MyBlog.PostView.render/2 (module MyBlog.PostView is not available)
That means we need to add our view and template. Let’s add a view at
web/views/post_view.ex
defmodule MyBlog.PostView do
use MyBlog.Web, :view
end
And a template at web/templates/post/index.html.eex
<%= for post <- @posts do %>
<%= post.title %>
<% end %>
Now when you run mix test
everything should pass smoothly.
Using function composition for more robust tests
Let’s say that our post can be tagged. A post can be tagged with multiple tags. We’ll start by updating our factory. We’ll assume our models have already been created and jump to updating our tests.
def factory(:tag) do
%MyBlog.Tag{
name: sequence(:tag_name, fn(n) -> "Tag #{n}" end)
}
end
def factory(:post_tag) do
%MyBlog.PostTag{
post: build(:post)
tag: build(:tag)
}
end
We’re going to use another sequence so that when we build or create a :tag, the name is always unique. When building a :post_tag we will build a post and a tag by default. This can be overridden just like any other attribute. We’ll see how to do that in a second.
Let’s add a test:
test "list of post shows the post's tags", %{conn: conn} do
tag_name = "elixir"
create(:post)
tag = create(:tag, name: tag_name)
create(:post_tag, tag: tag, post: post)
conn = get conn, post_path(conn, :index)
assert html_response(conn, 200) =~ tag_name
end
This test will work, but the problem is that the test is brittle and not very expressive. If someone needs to create a tagged post in another test they will have to remember to do this. If you change how you create posts, you will then need to change this in every test that uses it.
Instead let’s add a function to our factory in lib/my_blog/factory.ex
to make
this more clear.
def tagged_with(post, tag_name) do
tag = create(:tag, name: tag_name)
create(:post_tag, post: post, tag: tag)
post
end
Now our test will look like this
test "list of post shows the post's tags", %{conn: conn} do
tag_name = "elixir"
post = create(:post) |> tagged_with(tag_name)
conn = get conn, post_path(conn, :index)
assert html_response(conn, 200) =~ tag_name
end
Now if you need to create a tagged post in another test, it is much easier to do
so. This is also nice because if we change how we tag posts we can just update
the tagged_with/2
function instead of changing every test.
Using regular functions with ExMachina can make your code easier to read, and easier to change in the future.
This is a small taste of what you can do with ExMachina and the Phoenix tests. Be sure to check out the Phoenix.ConnTest docs and the ExMachina docs for more examples.