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
...
endmix 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.initThe task creates spec/spec_helper.exs file.
Run:
MIX_ENV=test mix espec_phoenix.initThe 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
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.
You can easy copy/paste previous functionality from 'old' branch. There are two ways:
- Copy old-version assertions from
lib/assertionswith 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: 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
end- Calls
ESpec.Phoenix.Extend.modelfunction 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
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: 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.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: 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]
endtest_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.