Unit-Testing Plugs

If your code is worth writing, it’s worth testing. Earlier, we bypassed our authentication plug, so we should test it now. The good news is that since our plug is essentially a function, it’s relatively easy to build a set of tests that will confirm that it does what we need.

Create a test/rumbl_web/controllers/auth_test.exs and key in the following contents. We’re going to break the test file into parts to keep things simple.

First, test the authenticate_user function that does the lion’s share of the work:

 defmodule​ RumblWeb.AuthTest ​do
 use​ RumblWeb.ConnCase, ​async:​ true
  alias RumblWeb.Auth
 
  test ​"​​authenticate_user halts when no current_user exists"​,
  %{​conn:​ conn} ​do
 
  conn = Auth.authenticate_user(conn, [])
  assert conn.halted
 end
 
  test ​"​​authenticate_user for existing current_user"​,
  %{​conn:​ conn} ​do
 
  conn =
  conn
  |> assign(​:current_user​, %Rumbl.Accounts.User{})
  |> Auth.authenticate_user([])
 
  refute conn.halted
 end
 end

That’s as simple as it gets. If we try to authenticate without a user, we shouldn’t authenticate. Otherwise, we should.

Let’s run that much to make sure things continue to work:

 $ ​​mix​​ ​​test​​ ​​test/rumbl_web/controllers/auth_test.exs
 .
 
 1)
 test authenticate_user halts when no current_user exists (RumblWeb.AuthTest)
 test/rumbl_web/controllers/auth_test.exs:5
 ** (KeyError) key :current_user not found in: %{}
 code: conn = Auth.authenticate_user(conn, [])
 stacktrace:
  (rumbl) lib/rumbl_web/controllers/auth.ex:47: Auth.authenticate_user/2
  test/rumbl_web/controllers/auth_test.exs:8: (test)
 
 Finished in 0.05 seconds
 2 tests, 1 failure

That was surprising. What happened?

Since our Auth plug assumes that a :current_user assign exists in the connection, the test errors.

Let’s try to quickly fix this by injecting a nil :current_user in our first test case, like this:

 conn =
  conn
  |> assign(​:current_user​, nil)
  |> Auth.authenticate_user([])

Now let’s rerun the tests:

 $ ​​mix​​ ​​test​​ ​​test/rumbl_web/controllers/auth_test.exs
 
 1)
 test authenticate_user halts when no current_user exists (RumblWeb.AuthTest)
 test/rumbl_web/controllers/auth_test.exs:5
 ** (ArgumentError) flash not fetched, call fetch_flash/2
 code: |> Auth.authenticate_user([])
 stacktrace:
  (phoenix) lib/phoenix/controller.ex:1265: Phoenix.Controller.get_flash/1
  (phoenix) lib/phoenix/controller.ex:1247: Phoenix.Controller.put_flash/3
  (rumbl) lib/rumbl_web/controllers/auth.ex:51: Auth.authenticate_user/2
  test/rumbl_web/controllers/auth_test.exs:11: (test)
 
 Finished in 0.06 seconds
 2 tests, 1 failure

Another error.

It looks like our authenticate_user raised an error because it puts a message in the flash, which isn’t available. If you look at the :browser pipeline in the router, you see that it plugs fetch_flash to set up the flash.

So let’s do the same:

 conn =
  conn()
  |> fetch_flash()
  |> Auth.authenticate_user([])

We receive a ** (ArgumentError) session not fetched, call fetch_session/2 error. We could attempt to solve this one too, but we would get yet another error about the Plug.Session not being configured.

The issue here is that we want to unit test authenticate_user but it depends on other functionality from the Plug pipeline. These are the kinds of issues that integration testing through the endpoint avoids.

We could reimplement our whole endpoint and router pipeline in order to test authenticate_user but Phoenix gives us another option. For unit tests, Phoenix includes a bypass_through test helper that allows us to do a request that goes through the whole pipeline but bypasses the router dispatch. This approach gives you a connection wired up with all the transformations your specific tests require, such as fetching the session and adding flash messages:

 setup %{​conn:​ conn} ​do
  conn =
  conn
  |> bypass_through(RumblWeb.Router, ​:browser​)
  |> get(​"​​/"​)
 
  {​:ok​, %{​conn:​ conn}}
 end
 
 test ​"​​authenticate_user halts when no current_user exists"​, %{​conn:​ conn} ​do
  conn = Auth.authenticate_user(conn, [])
  assert conn.halted
 end
 
 test ​"​​authenticate_user for existing current_user"​, %{​conn:​ conn} ​do
  conn =
  conn
  |> assign(​:current_user​, %Rumbl.Accounts.User{})
  |> Auth.authenticate_user([])
 
  refute conn.halted
 end

We add a setup block, which calls bypass_through, passing our router and the :browser pipeline to invoke. Then we perform a request with get, which accesses the endpoint and stops at the browser pipeline, as requested. The path given to get isn’t used by the router when bypassing; it’s simply stored in the connection. This gives us all the requirements for a plug with a valid session and flash message support. Next, we pull the conn from the context passed to the test macro and use our bypassed conn as the base for our test blocks.

Now let’s rerun our tests:

 $ ​​mix​​ ​​test​​ ​​test/rumbl_web/controllers/auth_test.exs
 ..
 
 Finished in 0.08 seconds
 2 tests, 0 failures

And boom. Now test the rest of our Auth plug, like the login and logout features:

 test ​"​​login puts the user in the session"​, %{​conn:​ conn} ​do
  login_conn =
  conn
  |> Auth.login(%Rumbl.Accounts.User{​id:​ 123})
  |> send_resp(​:ok​, ​"​​"​)
 
  next_conn = get(login_conn, ​"​​/"​)
  assert get_session(next_conn, ​:user_id​) == 123
 end

Here, we test our ability to log in. We create a new connection called login_conn. We take a basic conn, log the user in with Auth.login, and call send_resp, which sends the response to the client with a given status and response body. To make sure that our new user survives the next request, we make a new request with that connection and make sure the user is still in the session. That’s easy enough. A test for logout is similar:

 test ​"​​logout drops the session"​, %{​conn:​ conn} ​do
  logout_conn =
  conn
  |> put_session(​:user_id​, 123)
  |> Auth.logout()
  |> send_resp(​:ok​, ​"​​"​)
 
  next_conn = get(logout_conn, ​"​​/"​)
  refute get_session(next_conn, ​:user_id​)
 end

We create a connection, put a user_id into our session, and then call Auth.logout. To make sure the logout will persist through a request, we then make a request with get, and finally make sure that no user_id is in the session.

Now, let’s test the main interface for our plug—the call function, which calls the plug directly to wire up the current_user from the session:

1: test ​"​​call places user from session into assigns"​, %{​conn:​ conn} ​do
user = user_fixture()
conn =
conn
5:  |> put_session(​:user_id​, user.id)
|> Auth.call(Auth.init([]))
assert conn.assigns.current_user.id == user.id
end
10: 
test ​"​​call with no session sets current_user assign to nil"​, %{​conn:​ conn} ​do
conn = Auth.call(conn, Auth.init([]))
assert conn.assigns.current_user == nil
end

The tests are simple and light. On line 2, we create a user for the test. Next, on line 5, we place that user’s ID in the session. On line 6, we call Auth.call, and then assert that the current_user in conn.assigns matches our seeded user. We know that logged-in users can get in.

We have a workable positive test, but it’s also important to test the negative condition. We want to make sure that logged-out users stay out. The test looks a lot like the positive test, but we never put any user in the session, and we match on nil instead.

Now let’s run our new tests:

 $ ​​mix​​ ​​test​​ ​​test/rumbl_web/controllers/auth_test.exs
 .........
 
 
 Finished in 1.9 seconds
 9 tests, 0 failures

All pass, but if you look closely, we have a problem. We are waiting two seconds for nine small tests. The test time is growing quickly. You have probably been noticing how the test times have crept up as we have seeded more and more users. If your tests are slow, you won’t run them as much. We have to fix it.

The reason our tests are slow is that we seed users with our registration changeset, which hashes passwords. Hashing passwords is intentionally expensive. Doing this extra bit of work makes our passwords harder to crack, but we don’t need all of that security in the test environment.

Let’s ease up the number of hashing rounds to speed up our test suite by adding this configuration line to config/test.exs:

 config ​:pbkdf2_elixir​, ​:rounds​, 1

Now let’s rerun our authentication tests:

 $ ​​mix​​ ​​test​​ ​​test/rumbl_web/controllers/auth_test.exs
 .........
 
 
 Finished in 0.1 seconds
 9 tests, 0 failures

One-tenth of a second! Time to shift into views.

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

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