or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

code-generation.mdindex.mdpresence.mdreal-time.mdsecurity.mdtesting.mdweb-foundation.md

testing.mddocs/

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

```