Elixir |> BDD

When it comes to testing, as with most things in life if there are two alternatives, you’ll probably find at least three different opinions.

In Elixir when it comes to testing ExUnit seems to be the natural choice as it is part of the Elixir release.

ExUnit has really interesting features, being doctests one of my favourites, but I was never a great fan of this type of testing. Being a fan of BDD I was really happy when I came across espec and espec_phoenix. I am using espec in a current project and since I am really enjoying it I thought I should also try the Phoenix Framework oriented library.

In this blog post I will be iterating through a new phoenix application, setting up espec with test coverage and will write some basic specs.

The application

One of the great things we have here at Onfido is a pool table. Where there is a pool table a pool ladder will eventually come to life. Since I need a phoenix app to test espec on a pool ladder app seems quite appropriate.

The generated application is a plain phoenix app, with a Player resource added using mix phoenix.gen.html, and you can find it here.

Setting up the dependencies

To get started we need to add espec_phoenix and excoveralls in mix.exs.

{:phoenix, "~> 0.13.1"}, 
    {:phoenix_ecto, "~> 0.4"} 
    {:postgrex, ">= 0.0.0"}
    {:phoenix_html, "~> 1.0"}
    {:phoenix_live_reload, "~> 0.4", only: :dev}
    {:cowboy, "~> 1.0"}
    {:espec_phoenix, github: "antonmi/espec_phoenix", only: :test, app: false}
    {:excoveralls, "~> 0.3.10", only: [:dev, :test]}                    
  end 

To avoid having to run mix espec prepending MIXENV=test we will set the preferredcli_env and we will also configure excoveralls as our code coverage tool.

def project do  
    [
      app: :pool_ladder,
      version: "0.0.1",
      elixir: "~> 1.0",
      elixirc_paths: elixirc_paths(Mix.env),
      compilers: [:phoenix] ++ Mix.compilers,
      build_embedded: Mix.env == :prod,
      start_permanent: Mix.env == :prod,
      deps: deps,
      preferred_cli_env: [espec: :test],
      test_coverage: [tool: ExCoveralls]
    ] end

Note that the espec_phoenix version I am using is the latest from Github since at the time I’m writing this post the version available in Hex doesn’t seem to have all helpers working.

Setting up espec

After getting the dependencies and compiling (mix do dependencies.get, compile) we need to configure espec. In order to do so we need to run two commands:

MIX_ENV=test mix espec.init

MIXENV=test mix especphoenix.init

This will generate the spec dir and add some setup files and a few spec examples.
In the poolladder application you can get from Github I have removed all the generated example specs from controllers, views, models and requests folders. The only generated spec file I kept was examplespec.ex. This file is non-phoenix specific and it is a good example on how to use espec.

The specs

As the author of the library states espec is inspired by rspec so the experience using it is quite similar.
I have added 4 spec files for the player resource under controllers, views, requests and models.
I will list here the model and the request specs so you may have a feeling on what is like to use espec.

 defmodule PoolLadder.PlayerSpec do

  use ESpec.Phoenix, model: PoolLadder.Player

  let :valid_attributes do
    %{
      email: "demo@demo.com",
      first_name: "first name",
      last_name: "last name"
    }
  end
  let :invalid_attributes, do: %{}

  let :valid_changeset, do: Player.changeset(%Player{}, valid_attributes)
  let :invalid_changeset, do: Player.changeset(%Player{}, invalid_attributes)

  describe "Models#Player" do
    context "valid attributes" do
      it "should create a model instance" do
        expect(valid_changeset).to be_valid
      end
    end

    context "invalid attributes" do
      it "should not create a model instance" do
        expect(invalid_changeset).not_to be_valid
      end

      it "should have a list of errors" do
        error_list = [first_name: "can't be blank", last_name: "can't be blank", email: "can't be blank"]
        expect(invalid_changeset).to have_errors(error_list)
      end
    end

    context "default attributes" do
      let :player, do: Repo.insert(valid_changeset)

      it "should set default value for wins" do
        expect(player.wins).to eq(0)
      end

      it "should set default value for losses"  do
        expect(player.losses).to eq(0)
      end

      it "should set default value for challenged" do
        expect(player.challenged).to eq(0)
      end

      it "should set default value for challenger" do
        expect(player.challenger).to eq(0)
      end
    end 
  end
end  
defmodule PoolLadder.PlayerSpec do  
   use ESpec.Phoenix, model: PoolLadder.Player

  let :valid_attributes do
    %{
      email: "demo@demo.com",
      first_name: "first name",
      last_name: "last name"
    }
  end
  let :invalid_attributes, do: %{}

  let :valid_changeset, do: Player.changeset(%Player{}, valid_attributes)
  let :invalid_changeset, do: Player.changeset(%Player{}, invalid_attributes)

  describe "Models#Player" do
    context "valid attributes" do
      it "should create a model instance" do
        expect(valid_changeset).to be_valid
      end
    end

    context "invalid attributes" do
      it "should not create a model instance" do
        expect(invalid_changeset).not_to be_valid
      end

      it "should have a list of errors" do
        error_list = [first_name: "can't be blank", last_name: "can't be blank", email: "can't be blank"]
        expect(invalid_changeset).to have_errors(error_list)
      end
    end

    context "default attributes" do
      let :player, do: Repo.insert(valid_changeset)

      it "should set default value for wins" do
        expect(player.wins).to eq(0)
      end

      it "should set default value for losses"  do
        expect(player.losses).to eq(0)
      end

      it "should set default value for challenged" do
        expect(player.challenged).to eq(0)
      end

      it "should set default value for challenger" do
        expect(player.challenger).to eq(0)
      end
    end
  end
end  
defmodule PoolLadder.PostsRequestsSpec do

  use ESpec.Phoenix, request: PoolLadder.Endpoint

  before do
    player_one = %Player{first_name: "Player", last_name: "One", email: "one@demo.com"} |> PoolLadder.Repo.insert
    player_two = %Player{first_name: "Player", last_name: "Two", email: "two@demo.com"} |> PoolLadder.Repo.insert
    {:ok, player_one: player_one, player_two: player_two}
  end

  describe "index" do
    subject! do: get(conn(), player_path(conn(), :index))
about  
    it do: should be_success
    it do: should use_view(PoolLadder.PlayerView)
    it do: should render_template("index.html")

    context "body" do
      let :html, do: subject.resp_body

      it do: html |> should have_content __.player_one.email
      it do: html |> should have_content __.player_two.last_name
    end
  end
end  

Running the tests

In order to run the specs all we have to do is to invoke the espec mix task:

mix espec --cover

The --cover option will output stats about code coverage.

By default the code coverage tool will also report the files under test directory. I have left the generated ExUnit tests there so we may compare the two testing approaches.
In order to exclude those files from the code coverage report we need to include the file coveralls.json in our project root directory.

{
  "skip_files": [
    "test/*"
  ]
}

The output from espec looks like this:
output

The messages for failing tests are also quite helpful. Here is an example:
Screen Shot 2015-06-04 at 09.22.45

I find espec quite easy to use and it fits really well to my workflow. Hope you also give it a try !

Author image
Author, blogger, father and Ruby/Rails/Elixir guru, there's very little Paulo doesn't do. And best of all, he does it with a smile and (not so funny) jokes to boot!
top