Testing Contexts

It’s time to test the M of the MVC, models. Phoenix generates a module in test/support/data_case.ex to serve as a foundation for your tests that interact with the database. In our case, Accounts and Multimedia contexts both work with the database. The data_case handles setup and teardown of the database and integrates with Ecto.Sandbox to allow concurrent transactional tests. Crack it open and import the fixtures we just defined:

1: defmodule​ Rumbl.DataCase ​do
use​ ExUnit.CaseTemplate
using ​do
5: quote​ ​do
alias Rumbl.Repo
import​ Ecto
import​ Ecto.Changeset
10: import​ Ecto.Query
import​ Rumbl.DataCase
import​ Rumbl.TestHelpers
end
end
15: 
setup tags ​do
:ok​ = Ecto.Adapters.SQL.Sandbox.checkout(Rumbl.Repo)
unless​ tags[​:async​] ​do
20:  Ecto.Adapters.SQL.Sandbox.mode(Rumbl.Repo, {​:shared​, self()})
end
:ok
end
25: 
def​ errors_on(changeset) ​do
Ecto.Changeset.traverse_errors(changeset, ​fn​ {message, opts} ->
Regex.replace(​~​r​"​​%{(w+)}"​, message, ​fn​ _, key ->
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
30: end​)
end​)
end
end

Let’s take a look in more detail.

On line 12 we import our test helpers inside the using block. The using block serves as a place for defining macros, common imports and aliases. We also see a setup block for handling transactional tests. A transactional test runs a test and rolls back any changes made during the test. This transactional technique allows tests to reset the database to a known state quickly between tests.

Phoenix also generates an errors_on function for quickly accessing a list of error messages for attributes on a given schema. You’ll see that function come into play as we write tests for our contexts.

Testing User Accounts

Let’s start with user account registration. In truth, most context-related functionality will be tested with our integration tests as they insert and update records, but not all. Error and exception flows are some of the trickiest parts of our application to get right. We will explicitly try to catch some error conditions as close to the breaking point as possible. For us, since our context layer is the one that interacts directly with our database code, we’ll build such cases there. Create a new file test/rumbl/accounts_test.exs that looks like this:

1: defmodule​ Rumbl.AccountsTest ​do
use​ Rumbl.DataCase, ​async:​ true
alias Rumbl.Accounts
5:  alias Rumbl.Accounts.User
describe ​"​​register_user/1"​ ​do
@valid_attrs %{
name:​ ​"​​User"​,
10: username:​ ​"​​eva"​,
password:​ ​"​​secret"
}
@invalid_attrs %{}
15:  test ​"​​with valid data inserts user"​ ​do
assert {​:ok​, %User{​id:​ id}=user} = Accounts.register_user(@valid_attrs)
assert user.name == ​"​​User"
assert user.username == ​"​​eva"
assert [%User{​id:​ ^id}] = Accounts.list_users()
20: end
test ​"​​with invalid data does not insert user"​ ​do
assert {​:error​, _changeset} = Accounts.register_user(@invalid_attrs)
assert Accounts.list_users() == []
25: end
test ​"​​enforces unique usernames"​ ​do
assert {​:ok​, %User{​id:​ id}} = Accounts.register_user(@valid_attrs)
assert {​:error​, changeset} = Accounts.register_user(@valid_attrs)
30: 
assert %{​username:​ [​"​​has already been taken"​]} =
errors_on(changeset)
assert [%User{​id:​ ^id}] = Accounts.list_users()
35: end
test ​"​​does not accept long usernames"​ ​do
attrs = Map.put(@valid_attrs, ​:username​, String.duplicate(​"​​a"​, 30))
{​:error​, changeset} = Accounts.register_user(attrs)
40: 
assert %{​username:​ [​"​​should be at most 20 character(s)"​]} =
errors_on(changeset)
assert Accounts.list_users() == []
end
45: 
test ​"​​requires password to be at least 6 chars long"​ ​do
attrs = Map.put(@valid_attrs, ​:password​, ​"​​12345"​)
{​:error​, changeset} = Accounts.register_user(attrs)
50:  assert %{​password:​ [​"​​should be at least 6 character(s)"​]} =
errors_on(changeset)
assert Accounts.list_users() == []
end
end
55: end

On line 2, we use Rumbl.DataCase to set up our DB dependent tests. We pass the async: true option so the test runs concurrently. Then, on lines 15 and 22, we build valid and invalid users and assert the expected results of trying to register a new user account with Accounts.register_user. In the remaining tests, on lines 37, 46, and 27, our error checking is a bit more intentional. We set a username that’s too long and assert that we got a specific error back. Likewise, we set a password that is too short and then test for a specific error.

We close by setting a password that is too short and then test for a specific error. For these tests, we use the errors_on function defined on Rumbl.DataCase. errors_on is convenient for quickly retrieving errors from the changeset. Keep in mind errors_on is just a function. You can create a custom version if you need to test custom behavior.

Now let’s run our tests:

 $ ​​mix​​ ​​test​​ ​​test/rumbl/accounts_test.exs
 .......
 
 Finished in 0.1 seconds
 5 tests, 0 failures

All green.

Next, let’s introduce a testing feature in ExUnit called the describe block. Sometimes, you need to apply the same setup code to many different tests. Describe blocks allow us to apply different test setups to a whole block of tests. For example, we need a user record to test Accounts.authenticate_by_username_and_pass, so we create a describe block with its own setup and three tests, like this:

1: describe ​"​​authenticate_by_username_and_pass/2"​ ​do
@pass ​"​​123456"
setup ​do
5:  {​:ok​, ​user:​ user_fixture(​password:​ @pass)}
end
test ​"​​returns user with correct password"​, %{​user:​ user} ​do
assert {​:ok​, auth_user} =
10:  Accounts.authenticate_by_username_and_pass(user.username, @pass)
assert auth_user.id == user.id
end
15:  test ​"​​returns unauthorized error with invalid password"​, %{​user:​ user} ​do
assert {​:error​, ​:unauthorized​} =
Accounts.authenticate_by_username_and_pass(user.username, ​"​​badpass"​)
end
20:  test ​"​​returns not found error with no matching user for email"​ ​do
assert {​:error​, ​:not_found​} =
Accounts.authenticate_by_username_and_pass(​"​​unknownuser"​, @pass)
end
end

Let’s break it down. We start by defining a new describe block on line 4 which creates a user fixture having a hardcoded valid email and password.

Next, on line 8, we test that authenticate_by_username_and_pass returns our user when we provide a correct email and password. Following our valid authentication tests, we then test for the two possibile user authentication failures, a bad password or missing email on lines 15 and 20.

Now let’s run our tests again:

 $ ​​mix​​ ​​test​​ ​​test/rumbl/accounts_test.exs
 .......
 
 Finished in 0.1 seconds
 8 tests, 0 failures

We’re still happily green.

Testing the Multimedia Context

Let’s test the data access features in our Multimedia context. Create a new file at test/rumbl/multimedia_test.exs that looks like this:

 defmodule​ Rumbl.MultimediaTest ​do
 use​ Rumbl.DataCase, ​async:​ true
 
  alias Rumbl.Multimedia
  alias Rumbl.Multimedia.Category
 
  describe ​"​​categories"​ ​do
  test ​"​​list_alphabetical_categories/0"​ ​do
  for name <- ​~​w(Drama Action Comedy) ​do
  Multimedia.create_category!(name)
 end
 
  alpha_names =
  for %Category{​name:​ name} <-
  Multimedia.list_alphabetical_categories() ​do
 
  name
 end
 
  assert alpha_names == ​~​w(Action Comedy Drama)
 end
 end
 end

We programmatically create categories and later fetch them. Our tests verify that they are in alphabetical order. Now let’s run our tests:

 $ ​​mix​​ ​​test​​ ​​test/rumbl/multimedia_test.exs
 .
 
 Finished in 0.07 seconds
 1 test, 0 failures

Success!

Everything looks good, so let’s now test the video functions of our Multimedia context, which is a little more involved than the previous tests:

1: describe ​"​​videos"​ ​do
alias Rumbl.Multimedia.Video
@valid_attrs %{​description:​ ​"​​desc"​, ​title:​ ​"​​title"​, ​url:​ ​"​​http://local"​}
5:  @invalid_attrs %{​description:​ nil, ​title:​ nil, ​url:​ nil}
test ​"​​list_videos/0 returns all videos"​ ​do
owner = user_fixture()
%Video{​id:​ id1} = video_fixture(owner)
10:  assert [%Video{​id:​ ^id1}] = Multimedia.list_videos()
%Video{​id:​ id2} = video_fixture(owner)
assert [%Video{​id:​ ^id1}, %Video{​id:​ ^id2}] = Multimedia.list_videos()
end
15:  test ​"​​get_video!/1 returns the video with given id"​ ​do
owner = user_fixture()
%Video{​id:​ id} = video_fixture(owner)
assert %Video{​id:​ ^id} = Multimedia.get_video!(id)
end
20: 
test ​"​​create_video/2 with valid data creates a video"​ ​do
owner = user_fixture()
assert {​:ok​, %Video{} = video} =
25:  Multimedia.create_video(owner, @valid_attrs)
assert video.description == ​"​​desc"
assert video.title == ​"​​title"
assert video.url == ​"​​http://local"
30: end
test ​"​​create_video/2 with invalid data returns error changeset"​ ​do
owner = user_fixture()
assert {​:error​, %Ecto.Changeset{}} =
35:  Multimedia.create_video(owner, @invalid_attrs)
end
test ​"​​update_video/2 with valid data updates the video"​ ​do
owner = user_fixture()
40:  video = video_fixture(owner)
assert {​:ok​, video} =
Multimedia.update_video(video, %{​title:​ ​"​​updated title"​})
assert %Video{} = video
assert video.title == ​"​​updated title"
45: end
test ​"​​update_video/2 with invalid data returns error changeset"​ ​do
owner = user_fixture()
%Video{​id:​ id} = video = video_fixture(owner)
50: 
assert {​:error​, %Ecto.Changeset{}} =
Multimedia.update_video(video, @invalid_attrs)
assert %Video{​id:​ ^id} = Multimedia.get_video!(id)
55: end
test ​"​​delete_video/1 deletes the video"​ ​do
owner = user_fixture()
video = video_fixture(owner)
60:  assert {​:ok​, %Video{}} = Multimedia.delete_video(video)
assert Multimedia.list_videos() == []
end
test ​"​​change_video/1 returns a video changeset"​ ​do
65:  owner = user_fixture()
video = video_fixture(owner)
assert %Ecto.Changeset{} = Multimedia.change_video(video)
end
end

We started by grouping our tests together with a new describe block for testing video functionality. On line 7, we create a video, picking off the id field with a pattern match. When we fetch a video, we verify correctness by matching against the id key of the Video record. Then, we do the same with a second video record. This test handles fetching a list of videos. Next, on line 15, we test the get_video function. We use the same technique to fetch a single video using get_video.

Next, on line 21 and 32 we test creation of videos with both valid and invalid user input. We verify a few attributes for the valid test and match against an error tuple for the invalid one. We’re specifically testing our change set functionality. We used a similar approach to test video updates on lines 38 and 47.

Next, we tested delete_video. We ran assertions to verify both the :ok tuple and that the video no longer exists on a subsequent fetch.

Finally, we checked out the ability to return a changeset for tracking video changes on line 64. These tests cover a lot of ground, but they’re quite simple.

Now let’s run our tests:

 $ ​​mix​​ ​​test​​ ​​test/rumbl/multimedia_test.exs
 .........
 
 Finished in 0.2 seconds
 9 tests, 0 failures

As expected, they are all green. Before we write more tests, let’s take a short break and talk about the Ecto Sandbox.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.188.201