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.2", only: :test},
#{:espec_phoenix, github: "antonmi/espec_phoenix", only: :test}, to get the latest version
...
end$ mix deps.getSet :preferred_cli_env for :espec in the mix.exs file:
def project do
...
preferred_cli_env: [espec: :test],
...
endRun:
$ MIX_ENV=test mix espec_phoenix.initThe 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
endThe 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: YourModelWhat 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
endCalls 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
endIt 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
endController specs are integration tests that tests interactions among all parts of your application.
Use 'controller' tag to identify controller specs:
use ESpec.Phoenix, controller: YourControllerYour 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
endBelow 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
endPlease 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
endView 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
enduse ESpec.Phoenix, channel: YourChannelChannel 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]
enduse 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
endespec_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 especThere 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.