Read about ESpec here.
ESpec.Phoenix is a lightweight wrapper around ESpec which brings BDD to Phoenix web framework.
Use ESpec.Phoenix the same way as ExUnit in you Phoenix application.
There is rumbrella project from great Programming Phoenix book. One can find a lot of useful examples there!
- Installation
- Migration from previous versions
- Model specs
- Controller specs
- View specs
- Channel specs
- LiveView specs
- Extensions
- Contributing
Add :espec_phoenix
to dependencies in the mix.exs
file:
def deps do
...
{:espec_phoenix, "~> 0.8.1", only: :test},
#{:espec_phoenix, github: "antonmi/espec_phoenix", only: :test}, to get the latest version
...
end
$ mix deps.get
Set :preferred_cli_env
for :espec
in the mix.exs
file:
def project do
...
preferred_cli_env: [espec: :test],
...
end
Run:
$ MIX_ENV=test mix espec_phoenix.init
The task creates spec/spec_helper.exs
, phoenix_helper.exs
and espec_phoenix_extend.ex
.
Also you need to checkout your Ecto
sandbox mode before each example and checkin it after. So spec_helper.exs
should look like:
#require phoenix_helper.exs
Code.require_file("#{__DIR__}/phoenix_helper.exs")
ESpec.configure fn(config) ->
config.before fn(_tags) ->
:ok = Ecto.Adapters.SQL.Sandbox.checkout(YourApp.Repo)
end
config.finally fn(_shared) ->
Ecto.Adapters.SQL.Sandbox.checkin(YourApp.Repo, [])
end
end
The espec_phoenix_extend.ex
file contains ESpec.Phoenix.Extend
module.
Use this module to import or alias additional modules in your specs.
I've decided to remove all the custom assertions for 'changeset', 'conn' and 'content'. The reason is to make specs more explicit like people used to see using ExUnit.
If you still want to use them, check out the espec_phoenix_helpers project.
Use 'model' tag to identify model specs:
use ESpec.Phoenix, model: YourModel
What ESpec.Phoenix does behind the scene is the following:
Uses ModelHelpers
:
defmodule ModelHelpers do
defmacro __using__(_args) do
quote do
import Ecto
import Ecto.Changeset, except: [change: 1, change: 2]
import Ecto.Query
end
end
end
Calls ESpec.Phoenix.Extend.model
function extending your spec module.
Note! We don't import change/1
and change/2
functions from Ecto.Changeset
because they conflicts with ESpec functions. If you want to use them, call them directly with module prefix (Ecto.Changeset.change
).
defmodule Rumbl.UserSpec do
use ESpec.Phoenix, model: User, async: true
alias Rumbl.User
@valid_attrs %{name: "A User", username: "eva", password: "secret"}
@invalid_attrs %{}
context "validation" do
it "checks changeset with valid attributes" do
changeset = User.changeset(%User{}, @valid_attrs)
assert changeset.valid?
end
it "checks changeset with long username" do
attrs = Map.put(@valid_attrs, :username, String.duplicate("a", 30))
assert {:username, "should be at most 20 character(s)"} in
errors_on(%User{}, attrs)
end
end
end
It is a good practice to place specs with side effects (db access) to another module:
defmodule Rumbl.UserRepoSpec do
use ESpec.Phoenix, model: User, async: true
alias Rumbl.User
@valid_attrs %{name: "A User", username: "eva"}
describe "converting unique_constraint on username to error" do
before do: insert_user(username: "eric")
let :changeset do
attrs = Map.put(@valid_attrs, :username, "eric")
User.changeset(%User{}, attrs)
end
it do: expect(Repo.insert(changeset)).to be_error_result
context "when name has been already taken" do
let :new_changeset do
{:error, changeset} = Repo.insert(changeset)
changeset
end
it "has error" do
error = {:username, {"has already been taken", []}}
expect(new_changeset.errors).to have(error)
end
end
end
end
Controller specs are integration tests that tests interactions among all parts of your application.
Use 'controller' tag to identify controller specs:
use ESpec.Phoenix, controller: YourController
Your module will be extended with ESpec.Phoenix.ModelHelpers
and also with ESpec.Phoenix.ControllerHelpers
:
defmodule ControllerHelpers do
defmacro __using__(_args) do
quote do
import Plug.Conn
import Phoenix.ConnTest, except: [conn: 0, build_conn: 0]
def build_conn, do: Phoenix.ConnTest.build_conn()
end
end
end
Below is an example of controller specs:
defmodule Rumbl.VideoControllerSpec do
use ESpec.Phoenix, controller: VideoController, async: true
describe "with logged user" do
let :user, do: insert_user(username: "max")
let! :user_video, do: insert_video(user, title: "funny cats")
let! :other_video, do: insert_video(insert_user(username: "other"), title: "another video")
let :response do
assign(build_conn, :current_user, user)
|> get(video_path(build_conn, :index))
end
it "lists all user's videos on index" do
expect(html_response(response, 200)).to match(~r/Listing videos/)
end
it "has user_video title" do
expect(response.resp_body).to have(user_video.title)
end
it "does not have other_video title" do
expect(response.resp_body).not_to have(other_video.title)
end
end
end
Please note that due to the fact it's integraton tests, you can actually use it without specifying controller:
defmodule Rumbl.VideoControllerRequestSpec do
use ESpec.Phoenix, controller: true
describe "with logged user" do
let! :user_video, do: insert_video(user, title: "funny cats")
let :response do
build_conn |> get("/videos")
end
it "lists all user's videos on index" do
expect(response.resp_body).to match(~r/Listing videos/)
end
end
end
View specs also are extended with ESpec.Phoenix.ControllerHelpers
and also imports Phoenix.View
.
defmodule Rumbl.VideoViewSpec do
use ESpec.Phoenix, async: true, view: VideoView
let :videos do
[%Rumbl.Video{id: "1", title: "dogs"},
%Rumbl.Video{id: "2", title: "cats"}]
end
describe "index.html" do
let :content do
render_to_string(Rumbl.VideoView, "index.html", conn: build_conn, videos: videos)
end
it do: expect(content).to have("Listing videos")
it "has video titles" do
for video <- videos do
expect(content).to have(video.title)
end
end
end
end
use ESpec.Phoenix, channel: YourChannel
Channel specs uses Phoenix.ChannelTest
and ESpec.Phoenix.ModelsHelpers
.
Use 'model' tag to identify model specs:
defmodule Rumbl.Channels.VideoChannelSpec do
use ESpec.Phoenix, channel: Rumbl.VideoChannel
before do
Ecto.Adapters.SQL.Sandbox.mode(Rumbl.Repo, {:shared, self()})
end
let! :user, do: insert_user(name: "Rebecca")
let! :video, do: insert_video(user, title: "Testing")
before do
token = Phoenix.Token.sign(@endpoint, "user socket", user.id)
{:ok, socket} = connect(Rumbl.UserSocket, %{"token" => token})
{:shared, socket: socket}
end
before do
for body <- ~w(one two) do
video
|> build_assoc(:annotations, %{body: body})
|> Repo.insert!()
end
end
before do
{:ok, reply, socket} = subscribe_and_join(shared[:socket], "videos:#{video.id}", %{})
{:shared, reply: reply, socket: socket}
end
it do: expect shared[:socket].assigns.video_id |> to(eq video.id)
it do: assert %{annotations: [%{body: "one"}, %{body: "two"}]} = shared[:reply]
end
use ESpec.Phoenix, live_view: YourLiveView, async: false, pid: self()
LiveView specs uses Phoenix.LiveViewTest
and ESpec.Phoenix.ModelsHelpers
.
Use 'model' tag to identify model specs:
defmodule LiveViewEspecWeb.AccountsLiveSpec do
use ESpec.Phoenix, live_view: LiveViewEspecWeb.UserLive.Index, async: false, pid: self()
describe "GET /accounts" do
it "displays the page" do
{:ok, page_live, disconnected_html} = live(live_conn(), "/live/accounts")
expect disconnected_html |> to(match "Listing Users")
expect render(page_live) |> to(match "Listing Users")
end
end
end
espec_phoenix_helpers - assertions and helpers that used to be part of this project but were extracted out test_that_json_espec - matchers for testing JSON
Request a new feature by creating an issue.
Create a pull request with new features or fixes.
To run specs:
$ mix espec
There is a rumbl application with specs inside.
Run mix deps.get
in rumbl
folder.
Change database settings in test_app/config/test.exs
.
Run tests with mix test
and mix espec
.
Copyright (c) 2015 Anton Mishchuk
This work is free. You can redistribute it and/or modify it under the terms of the MIT License. See the LICENSE.md file for more details.