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 usefull examples there!
- Installation
- Migration from previous versions
- Model specs
- Controller specs
- View specs
- Channel specs
- Extensions
- Contributing
Add espec_phoenix
to dependencies in the mix.exs
file:
def deps do
...
{:espec_phoenix, "~> 0.6.0", only: :test, app: false},
#{:espec_phoenix, github: "antonmi/espec_phoenix", only: :test, app: false}, 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.init
The task creates spec/spec_helper.exs
file.
Run:
MIX_ENV=test mix espec_phoenix.init
The task creates phoenix_helper.exs
and espec_phoenix_extend.ex
.
phoenix_helper.exs
has Phoenix related configurations.
You must require this helper in your spec_helper.exs
:
Code.require_file("spec/phoenix_helper.exs")
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.
You can easy copy/paste previous functionality from 'old' branch. There are two ways:
- Copy old-version assertions from
lib/assertions
with minimal changes. - Explicitly assert what you want in your specs.
Hint! One can place old-version assertions to its own project and add it to the Extensions section.
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.VideoControllerTest 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
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
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
.