0
# Testing Support
1
2
Phoenix provides comprehensive testing utilities for controllers, channels, and integration testing. The testing framework is built on ExUnit with specialized helpers for web applications and real-time features.
3
4
## Controller and Connection Testing
5
6
Phoenix.ConnTest provides utilities for testing HTTP requests, responses, and controller actions.
7
8
### Phoenix.ConnTest
9
10
```elixir { .api }
11
defmodule Phoenix.ConnTest do
12
# Connection building
13
def build_conn() :: Plug.Conn.t()
14
def build_conn(atom, binary, term) :: Plug.Conn.t()
15
16
# HTTP request functions
17
def get(Plug.Conn.t(), binary, term) :: Plug.Conn.t()
18
def post(Plug.Conn.t(), binary, term, keyword) :: Plug.Conn.t()
19
def put(Plug.Conn.t(), binary, term, keyword) :: Plug.Conn.t()
20
def patch(Plug.Conn.t(), binary, term, keyword) :: Plug.Conn.t()
21
def delete(Plug.Conn.t(), binary, term) :: Plug.Conn.t()
22
def options(Plug.Conn.t(), binary, term) :: Plug.Conn.t()
23
def head(Plug.Conn.t(), binary, term) :: Plug.Conn.t()
24
def trace(Plug.Conn.t(), binary, term) :: Plug.Conn.t()
25
26
# Request dispatch
27
def dispatch(Plug.Conn.t(), atom, atom, atom, keyword) :: Plug.Conn.t()
28
29
# Response assertions
30
def response(Plug.Conn.t(), atom | integer) :: binary
31
def html_response(Plug.Conn.t(), atom | integer) :: binary
32
def json_response(Plug.Conn.t(), atom | integer) :: map
33
def text_response(Plug.Conn.t(), atom | integer) :: binary
34
35
# Response status
36
def response_content_type(Plug.Conn.t(), atom) :: binary
37
def redirected_to(Plug.Conn.t(), integer) :: binary
38
def redirected_params(Plug.Conn.t()) :: map
39
40
# Assertions
41
def assert_error_sent(atom | integer, (() -> any)) :: any
42
def assert_redirected_to(Plug.Conn.t(), binary) :: Plug.Conn.t()
43
44
# Flash helpers
45
def clear_flash(Plug.Conn.t()) :: Plug.Conn.t()
46
def fetch_flash(Plug.Conn.t()) :: Plug.Conn.t()
47
def put_flash(Plug.Conn.t(), atom, binary) :: Plug.Conn.t()
48
49
# Session helpers
50
def init_test_session(Plug.Conn.t(), map) :: Plug.Conn.t()
51
def put_req_header(Plug.Conn.t(), binary, binary) :: Plug.Conn.t()
52
def put_req_cookie(Plug.Conn.t(), binary, binary) :: Plug.Conn.t()
53
def delete_req_cookie(Plug.Conn.t(), binary, keyword) :: Plug.Conn.t()
54
55
# Route testing
56
def bypass_through(Plug.Conn.t(), atom | [atom]) :: Plug.Conn.t()
57
end
58
```
59
60
### Usage Examples
61
62
```elixir
63
defmodule MyAppWeb.UserControllerTest do
64
use MyAppWeb.ConnCase
65
66
import MyApp.AccountsFixtures
67
68
describe "index" do
69
test "lists all users", %{conn: conn} do
70
user = user_fixture()
71
72
conn = get(conn, ~p"/users")
73
74
assert html_response(conn, 200) =~ "Users"
75
assert response(conn, 200) =~ user.name
76
end
77
end
78
79
describe "show user" do
80
setup [:create_user]
81
82
test "displays user when found", %{conn: conn, user: user} do
83
conn = get(conn, ~p"/users/#{user}")
84
85
assert html_response(conn, 200) =~ user.name
86
assert html_response(conn, 200) =~ user.email
87
end
88
89
test "returns 404 when user not found", %{conn: conn} do
90
assert_error_sent 404, fn ->
91
get(conn, ~p"/users/999999")
92
end
93
end
94
end
95
96
describe "create user" do
97
test "redirects to show when data is valid", %{conn: conn} do
98
create_attrs = %{name: "John Doe", email: "john@example.com"}
99
100
conn = post(conn, ~p"/users", user: create_attrs)
101
102
assert %{id: id} = redirected_params(conn)
103
assert redirected_to(conn) == ~p"/users/#{id}"
104
105
conn = get(conn, ~p"/users/#{id}")
106
assert html_response(conn, 200) =~ "John Doe"
107
end
108
109
test "renders errors when data is invalid", %{conn: conn} do
110
invalid_attrs = %{name: nil, email: "invalid"}
111
112
conn = post(conn, ~p"/users", user: invalid_attrs)
113
114
assert html_response(conn, 200) =~ "New User"
115
assert response(conn, 200) =~ "can't be blank"
116
assert response(conn, 200) =~ "has invalid format"
117
end
118
end
119
120
describe "JSON API" do
121
test "creates user and returns JSON", %{conn: conn} do
122
create_attrs = %{name: "Jane Doe", email: "jane@example.com"}
123
124
conn =
125
conn
126
|> put_req_header("accept", "application/json")
127
|> post(~p"/api/users", user: create_attrs)
128
129
assert %{"id" => id} = json_response(conn, 201)["data"]
130
131
conn = get(conn, ~p"/api/users/#{id}")
132
data = json_response(conn, 200)["data"]
133
134
assert data["name"] == "Jane Doe"
135
assert data["email"] == "jane@example.com"
136
end
137
end
138
139
defp create_user(_) do
140
user = user_fixture()
141
%{user: user}
142
end
143
end
144
```
145
146
## Channel Testing
147
148
Phoenix.ChannelTest provides utilities for testing real-time channel communication.
149
150
### Phoenix.ChannelTest
151
152
```elixir { .api }
153
defmodule Phoenix.ChannelTest do
154
# Socket management
155
def connect(atom, map, map) :: {:ok, Phoenix.Socket.t()} | :error
156
def close(Phoenix.Socket.t()) :: :ok
157
158
# Channel operations
159
def subscribe_and_join(Phoenix.Socket.t(), atom, binary, map) ::
160
{:ok, map, Phoenix.Socket.t()} | {:error, map}
161
def subscribe_and_join!(Phoenix.Socket.t(), atom, binary, map) ::
162
{map, Phoenix.Socket.t()}
163
164
def join(Phoenix.Socket.t(), atom, binary, map) ::
165
{:ok, map, Phoenix.Socket.t()} | {:error, map}
166
167
def leave(Phoenix.Socket.t()) :: :ok
168
169
# Message operations
170
def push(Phoenix.Socket.t(), binary, map) :: reference
171
def broadcast_from!(Phoenix.Socket.t(), binary, map) :: :ok
172
def broadcast_from(Phoenix.Socket.t(), binary, map) :: :ok
173
174
# Assertions
175
def assert_push(binary, map, timeout \\ 100)
176
def assert_push(binary, map, timeout)
177
def refute_push(binary, map, timeout \\ 100)
178
179
def assert_reply(reference, atom, map, timeout \\ 100)
180
def assert_reply(reference, atom, timeout \\ 100)
181
def refute_reply(reference, atom, timeout \\ 100)
182
183
def assert_broadcast(binary, map, timeout \\ 100)
184
def refute_broadcast(binary, map, timeout \\ 100)
185
186
# Socket reference
187
def socket_ref(Phoenix.Socket.t()) :: reference
188
end
189
```
190
191
### Usage Examples
192
193
```elixir
194
defmodule MyAppWeb.RoomChannelTest do
195
use MyAppWeb.ChannelCase
196
197
import MyApp.AccountsFixtures
198
199
setup do
200
user = user_fixture()
201
{:ok, socket} = connect(MyAppWeb.UserSocket, %{user_id: user.id})
202
%{socket: socket, user: user}
203
end
204
205
describe "joining rooms" do
206
test "successful join to public room", %{socket: socket} do
207
{:ok, reply, socket} = subscribe_and_join(socket, MyAppWeb.RoomChannel, "room:lobby")
208
209
assert reply == %{}
210
assert socket.assigns.room_id == "lobby"
211
end
212
213
test "join requires authentication", %{socket: socket} do
214
{:ok, socket} = connect(MyAppWeb.UserSocket, %{})
215
216
assert subscribe_and_join(socket, MyAppWeb.RoomChannel, "room:lobby") == {:error, %{}}
217
end
218
219
test "cannot join private room without permission", %{socket: socket} do
220
{:error, reply} = subscribe_and_join(socket, MyAppWeb.RoomChannel, "room:private")
221
222
assert reply.reason == "unauthorized"
223
end
224
end
225
226
describe "sending messages" do
227
setup %{socket: socket} do
228
{:ok, _, socket} = subscribe_and_join(socket, MyAppWeb.RoomChannel, "room:lobby")
229
%{socket: socket}
230
end
231
232
test "new_message broadcasts to all subscribers", %{socket: socket} do
233
push(socket, "new_message", %{"body" => "Hello, World!"})
234
235
assert_broadcast "new_message", %{body: "Hello, World!"}
236
end
237
238
test "new_message replies with success", %{socket: socket} do
239
ref = push(socket, "new_message", %{"body" => "Hello, World!"})
240
241
assert_reply ref, :ok, %{status: "sent"}
242
end
243
244
test "empty message returns error", %{socket: socket} do
245
ref = push(socket, "new_message", %{"body" => ""})
246
247
assert_reply ref, :error, %{errors: %{body: "can't be blank"}}
248
end
249
end
250
251
describe "typing indicators" do
252
setup %{socket: socket} do
253
{:ok, _, socket} = subscribe_and_join(socket, MyAppWeb.RoomChannel, "room:lobby")
254
%{socket: socket}
255
end
256
257
test "typing event broadcasts to others but not sender", %{socket: socket, user: user} do
258
push(socket, "typing", %{"typing" => true})
259
260
refute_push "typing", %{user: user.name, typing: true}
261
assert_broadcast "typing", %{user: user.name, typing: true}
262
end
263
end
264
265
describe "presence tracking" do
266
setup %{socket: socket} do
267
{:ok, _, socket} = subscribe_and_join(socket, MyAppWeb.RoomChannel, "room:lobby")
268
%{socket: socket}
269
end
270
271
test "tracks user presence on join", %{socket: socket, user: user} do
272
# Presence is tracked after join
273
users = MyApp.Presence.list(socket)
274
275
assert Map.has_key?(users, to_string(user.id))
276
assert users[to_string(user.id)].metas == [%{online_at: inspect(System.system_time(:second))}]
277
end
278
279
test "removes presence on leave", %{socket: socket, user: user} do
280
# Initially present
281
users = MyApp.Presence.list(socket)
282
assert Map.has_key?(users, to_string(user.id))
283
284
# Leave channel
285
leave(socket)
286
287
# No longer present
288
users = MyApp.Presence.list("room:lobby")
289
refute Map.has_key?(users, to_string(user.id))
290
end
291
end
292
end
293
```
294
295
## Integration Testing
296
297
Integration tests verify complete workflows across multiple components.
298
299
### Integration Test Patterns
300
301
```elixir
302
defmodule MyAppWeb.UserRegistrationTest do
303
use MyAppWeb.ConnCase
304
305
import Phoenix.LiveViewTest
306
import MyApp.AccountsFixtures
307
308
describe "user registration flow" do
309
test "successful registration via HTML form", %{conn: conn} do
310
# Visit registration page
311
conn = get(conn, ~p"/users/register")
312
assert html_response(conn, 200) =~ "Register"
313
314
# Submit registration form
315
conn = post(conn, ~p"/users/register", user: %{
316
name: "John Doe",
317
email: "john@example.com",
318
password: "password123"
319
})
320
321
# Should redirect to welcome page
322
assert redirected_to(conn) == ~p"/"
323
324
# Should be logged in
325
conn = get(conn, ~p"/")
326
assert html_response(conn, 200) =~ "Welcome, John!"
327
328
# User should exist in database
329
user = MyApp.Repo.get_by(MyApp.Accounts.User, email: "john@example.com")
330
assert user.name == "John Doe"
331
assert Bcrypt.verify_pass("password123", user.password_hash)
332
end
333
334
test "registration with invalid data shows errors", %{conn: conn} do
335
conn = post(conn, ~p"/users/register", user: %{
336
name: "",
337
email: "invalid-email",
338
password: "123"
339
})
340
341
assert html_response(conn, 200) =~ "Register"
342
assert response(conn, 200) =~ "can't be blank"
343
assert response(conn, 200) =~ "has invalid format"
344
assert response(conn, 200) =~ "should be at least 8 character"
345
end
346
end
347
348
describe "authentication flow" do
349
setup do
350
user = user_fixture()
351
%{user: user}
352
end
353
354
test "login and logout flow", %{conn: conn, user: user} do
355
# Login
356
conn = post(conn, ~p"/users/login", user: %{
357
email: user.email,
358
password: "password123"
359
})
360
361
assert redirected_to(conn) == ~p"/"
362
363
# Should be authenticated
364
conn = get(conn, ~p"/users/settings")
365
assert html_response(conn, 200) =~ "Settings"
366
367
# Logout
368
conn = delete(conn, ~p"/users/logout")
369
assert redirected_to(conn) == ~p"/"
370
371
# Should no longer be authenticated
372
conn = get(conn, ~p"/users/settings")
373
assert redirected_to(conn) == ~p"/users/login"
374
end
375
end
376
end
377
```
378
379
## LiveView Testing
380
381
Phoenix provides specialized testing for LiveView components.
382
383
### LiveView Test Helpers
384
385
```elixir
386
# Example LiveView test
387
defmodule MyAppWeb.UserLiveTest do
388
use MyAppWeb.ConnCase
389
390
import Phoenix.LiveViewTest
391
import MyApp.AccountsFixtures
392
393
describe "user management" do
394
test "lists users with live updates", %{conn: conn} do
395
user = user_fixture()
396
397
{:ok, view, html} = live(conn, ~p"/users")
398
399
assert html =~ "Users"
400
assert html =~ user.name
401
402
# Create new user via form
403
assert view
404
|> form("#user-form", user: %{name: "New User", email: "new@example.com"})
405
|> render_submit()
406
407
# Should see new user in list
408
assert render(view) =~ "New User"
409
end
410
411
test "real-time updates from other processes", %{conn: conn} do
412
{:ok, view, _html} = live(conn, ~p"/users")
413
414
# Create user in separate process
415
user = user_fixture()
416
417
# Should automatically appear in LiveView
418
assert render(view) =~ user.name
419
end
420
end
421
end
422
```
423
424
## Test Configuration and Setup
425
426
### Test Configuration
427
428
```elixir
429
# config/test.exs
430
import Config
431
432
config :my_app, MyAppWeb.Endpoint,
433
http: [ip: {127, 0, 0, 1}, port: 4002],
434
secret_key_base: "test_secret_key",
435
server: false
436
437
config :my_app, MyApp.Repo,
438
username: "postgres",
439
password: "postgres",
440
hostname: "localhost",
441
database: "my_app_test#{System.get_env("MIX_TEST_PARTITION")}",
442
pool: Ecto.Adapters.SQL.Sandbox
443
444
config :logger, level: :warning
445
446
# Test-specific configuration
447
config :bcrypt_elixir, :log_rounds, 1 # Fast hashing for tests
448
config :my_app, :email_backend, MyApp.Email.TestAdapter
449
```
450
451
### Test Case Modules
452
453
```elixir
454
# test/support/conn_case.ex
455
defmodule MyAppWeb.ConnCase do
456
use ExUnit.CaseTemplate
457
458
using do
459
quote do
460
use MyAppWeb, :verified_routes
461
462
import Plug.Conn
463
import Phoenix.ConnTest
464
import MyAppWeb.ConnCase
465
466
alias MyAppWeb.Router.Helpers, as: Routes
467
468
@endpoint MyAppWeb.Endpoint
469
end
470
end
471
472
setup tags do
473
MyApp.DataCase.setup_sandbox(tags)
474
{:ok, conn: Phoenix.ConnTest.build_conn()}
475
end
476
end
477
478
# test/support/channel_case.ex
479
defmodule MyAppWeb.ChannelCase do
480
use ExUnit.CaseTemplate
481
482
using do
483
quote do
484
import Phoenix.ChannelTest
485
import MyAppWeb.ChannelCase
486
487
@endpoint MyAppWeb.Endpoint
488
end
489
end
490
491
setup tags do
492
MyApp.DataCase.setup_sandbox(tags)
493
:ok
494
end
495
end
496
```
497
498
### Test Fixtures
499
500
```elixir
501
# test/support/fixtures/accounts_fixtures.ex
502
defmodule MyApp.AccountsFixtures do
503
@moduledoc """
504
Test fixtures for accounts context.
505
"""
506
507
def unique_user_email, do: "user#{System.unique_integer()}@example.com"
508
def valid_user_password, do: "password123"
509
510
def user_fixture(attrs \\ %{}) do
511
attrs = Enum.into(attrs, %{
512
name: "Test User",
513
email: unique_user_email(),
514
password: valid_user_password()
515
})
516
517
{:ok, user} = MyApp.Accounts.create_user(attrs)
518
user
519
end
520
521
def admin_fixture(attrs \\ %{}) do
522
user_fixture(Map.put(attrs, :role, :admin))
523
end
524
525
def extract_user_token(fun) do
526
{:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
527
[_, token | _] = String.split(captured_email.text_body, "[TOKEN]")
528
token
529
end
530
end
531
```
532
533
## Testing Patterns and Best Practices
534
535
### Database Testing
536
537
```elixir
538
# Async tests for isolated database operations
539
defmodule MyApp.AccountsTest do
540
use MyApp.DataCase, async: true
541
542
test "create_user/1 with valid data creates user" do
543
attrs = %{name: "Test User", email: "test@example.com", password: "password123"}
544
545
assert {:ok, user} = MyApp.Accounts.create_user(attrs)
546
assert user.name == "Test User"
547
assert user.email == "test@example.com"
548
assert Bcrypt.verify_pass("password123", user.password_hash)
549
end
550
end
551
552
# Synchronous tests for operations that affect shared state
553
defmodule MyApp.IntegrationTest do
554
use MyApp.DataCase, async: false
555
556
test "email sending integration" do
557
user = user_fixture()
558
559
MyApp.Accounts.send_welcome_email(user)
560
561
assert_delivered_email MyApp.Email.welcome_email(user)
562
end
563
end
564
```
565
566
### Mocking and Stubbing
567
568
```elixir
569
# Using Mox for mocking external services
570
defmodule MyApp.PaymentTest do
571
use MyApp.DataCase
572
import Mox
573
574
# Set up mocks
575
setup :verify_on_exit!
576
577
test "processes payment successfully" do
578
MyApp.PaymentGateway.Mock
579
|> expect(:charge, fn %{amount: 1000, token: "tok_123"} ->
580
{:ok, %{id: "ch_123", status: "succeeded"}}
581
end)
582
583
assert {:ok, charge} = MyApp.Payments.process_charge(1000, "tok_123")
584
assert charge.id == "ch_123"
585
assert charge.status == "succeeded"
586
end
587
end
588
```