Testing Contexts
Requirement: This guide expects that you have gone through the introductory guides and got a Phoenix application up and running.
Requirement: This guide expects that you have gone through the Introduction to Testing guide.
Requirement: This guide expects that you have gone through the Contexts guide.
At the end of the Introduction to Testing guide, we generated an HTML resource for posts using the following command:
$ mix phx.gen.html Blog Post posts title body:text
This gave us a number of modules for free, including a Blog context and a Post schema, alongside their respective test files. As we have learned in the Context guide, the Blog context is simply a module with functions to a particular area of our business domain while Post schema maps to a particular table in our database.
In this guide, we are going to explore the tests generated for our contexts and schemas. Before we do anything else, let's run mix test
to make sure our test suite runs cleanly.
$ mix test
................
Finished in 0.6 seconds
19 tests, 0 failures
Randomized with seed 638414
Great. We've got nineteen tests and they are all passing!
Testing posts
If you open up test/hello/blog_test.exs
, you will see a file with the following:
defmodule Hello.BlogTest do
use Hello.DataCase
alias Hello.Blog
describe "posts" do
alias Hello.Blog.Post
@valid_attrs %{body: "some body", title: "some title"}
@update_attrs %{body: "some updated body", title: "some updated title"}
@invalid_attrs %{body: nil, title: nil}
def post_fixture(attrs \\ %{}) do
{:ok, post} =
attrs
|> Enum.into(@valid_attrs)
|> Blog.create_post()
post
end
test "list_posts/0 returns all posts" do
post = post_fixture()
assert Blog.list_posts() == [post]
end
...
As the top of the file we import Hello.DataCase
, which as we will see soon, it is similar to HelloWeb.ConnCase
. While HelloWeb.ConnCase
sets up helpers for working with connections, which is useful when testing controllers and views, Hello.DataCase
provides functionality for working with contexts and schemas.
Next we define an alias, so we can refer to Hello.Blog
simply as Blog
.
Then we start a describe "posts"
block. A describe
block is a feature in ExUnit that allows us to group similar tests. The reason why we have grouped all post related tests together is because contexts in Phoenix are capable of grouping multiple schemas together. For example, if we ran this command:
$ mix phx.gen.html Blog Comment comments post_id:references:posts body:text
We will get a bunch of new functions in the Hello.Blog
context plus a whole new describe "comments"
block in our test file.
The tests defined for our context are very straight-forward. They call the functions in our context and assert on their results. As you can see, some of those tests even create entries in the database:
test "create_post/1 with valid data creates a post" do
assert {:ok, %Post{} = post} = Blog.create_post(@valid_attrs)
assert post.body == "some body"
assert post.title == "some title"
end
At this point, you may wonder: how can Phoenix make sure the data created in one of the tests do not affect other tests? We are glad you asked. To answer this question, let's talk about the DataCase
.
The DataCase
If you open up test/support/data_case.ex
, you will find the following:
defmodule Hello.DataCase do
use ExUnit.CaseTemplate
using do
quote do
alias Hello.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import Hello.DataCase
end
end
setup tags do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Demo.Repo, shared: not tags[:async])
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
:ok
end
def errors_on(changeset) do
...
end
end
Hello.DataCase
is another ExUnit.CaseTemplate
. In the using
block, we can see all of the aliases and imports DataCase
brings into our tests. The setup
chunk for DataCase
is very similar to the one from ConnCase
. As we can see, most of the setup
block revolves around setting up a SQL Sandbox.
The SQL Sandbox is precisely what allows our tests to write to the database without affecting any of the other tests. In a nutshell, at the beginning of every test, we start a transaction in the database. When the test is over, we automatically rollback the transaction, effectively erasing all of the data created in the test.
Furthermore, the SQL Sandbox allows multiple tests to run concurrently, even if they talk to the database. This feature is provided for PostgreSQL databases and it can be used to further speed up your contexts and controllers tests by adding a async: true
flag when using them:
use Hello.DataCase, async: true
There are some considerations you need to have in mind when running asynchronous tests with the sandbox, so please refer to the Ecto.Adapters.SQL.Sandbox
for more information.
Finally at the end of the of the DataCase
module we can find a function named errors_on
with some examples of how to use it. This function is used for testing any validation we may want to add to our schemas. Let's give it a try by adding our own validations and then testing them.
Testing schemas
When we generate our HTML Post resource, Phoenix generated a Blog context and a Post schema. It generated a test file for the context but no test file for the schema. However, this doesn't mean we don't need to test the schema, it just means we did not have to test the schema so far.
You may be wondering then: when do we test the context directly and when do we test the schema directly? The answer to this question is the same answer to the question of when do we add code to a context and when do we add it to the schema?
The general guideline is to keep all side-effect free code in the schema. In other words, if you are simply working with data structures, schemas and changesets, put it in the schema. The context will typically have the code that creates and updates schemas and then write them to a database or an API.
We'll be adding additional validations to the schema module, so that's a great opportunity to write some schema specific tests. Open up lib/hello/blog/post.ex
and add the following validation to def changeset
:
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :body])
|> validate_required([:title, :body])
|> validate_length(:title, min: 2)
end
The new validation says the title needs to have at least 2 characters. Let's write a test for this. Create a new file at test/hello/blog/post_test.exs
with this:
defmodule Hello.Blog.PostTest do
use Hello.DataCase, async: true
alias Hello.Blog.Post
test "title must be at least two characters long" do
changeset = Post.changeset(%Post{}, %{title: "I"})
assert %{title: ["should be at least 2 character(s)"]} = errors_on(changeset)
end
end
And that's it. As our business domain grows, we have well defined places to test our contexts and schemas.