SkillAgentSearch skills...

Espec

Elixir Behaviour Driven Development

Install / Use

/learn @antonmi/Espec
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

ESpec

Hex.pm

ESpec is a BDD testing framework for Elixir.

ESpec is inspired by RSpec and the main idea is to be close to its perfect DSL.

It is NOT a wrapper around ExUnit but a completely new testing framework written from scratch.

Features

  • Test organization with describe, context, it, and etc blocks.

  • Familiar matchers: eq, be_close_to, raise_exception, etc.

  • Possibility to add custom matchers.

  • There are two types of expectation syntax:

    • expect syntax with pipe operator expect smth1 |> to(eq smth2) or is_expected |> to(eq smth) when subject is defined;
    • should syntax: smth1 |> should(eq smth2) or should eq smth when subject is defined.
    Note: RSpec syntax expect(smth1).to eq(smth2) is deprecated and won't work with OTP 21.
  • before and finally blocks (like RSpec before and after).

  • let, let! and subject.

  • Shared examples.

  • Generated examples.

  • Async examples.

  • Mocks with Meck.

  • Doc specs.

  • HTML and JSON outputs.

  • Etc and etc.

Contents

Installation

Add espec to dependencies in the mix.exs file:

def deps do
  ...
  {:espec, "~> 1.10.0", only: :test},
  ...
end
mix deps.get

Then run:

MIX_ENV=test mix espec.init

The task creates spec/spec_helper.exs

Set preferred_cli_env for espec in the mix.exs file:

def project do
  ...
  preferred_cli_env: [espec: :test],
  ...
end

Or run with MIX_ENV=test:

MIX_ENV=test mix espec

Place your _spec.exs files into spec folder. use ESpec in the 'spec module'.

defmodule SyntaxExampleSpec do
  use ESpec
  it do: expect true |> to(be_true())
  it do: (1..3) |> should(have 2)
end

Run specs

mix espec

Run specific spec:

mix espec spec/some_spec.exs:25

Read the help:

MIX_ENV=test mix help espec

Context blocks and tags

There are three macros with the same functionality: context, describe, and example_group.

Context can have description and tags.

defmodule ContextSpec do
  use ESpec

  example_group do
    context "Some context" do
      it do: expect "abc" |> to(match ~r/b/)
    end

    describe "Some another context with opts", focus: true do
      it do: 5 |> should(be_between 4, 6)
    end
  end
end

Available tags are:

  • skip: true or skip: "Reason" - skips examples in the context;
  • focus: true - sets focus to run with --focus option.

There are also xcontext, xdescribe, xexample_group macros to skip example groups. And fcontext, fdescribe, fexample_group for focused groups.

'spec' module is also a context with module name as description. One can add tags for this context after use ESpec:

defmodule ContextTagsSpec do
  use ESpec, skip: "Skip all examples in the module"
  ...
end

Examples

example, it, and specify macros define the 'spec example'.

defmodule ExampleAliasesSpec do
  use ESpec

  example do: expect [1,2,3] |> to(have_max 3)

  it "Test with description" do
    4.2 |> should(be_close_to 4, 0.5)
  end

  specify "Test with options", [pending: true], do: "pending"
end

You can use skip, pending or focus tags to control evaluation. There are also macros:

  • xit, xexample, xspecify - to skip;
  • fit, fexample, fspecify, focus - to focus;
  • pending/1, example/1, it/1, specify/1 - for pending examples.
defmodule ExampleTagsSpec do
  use ESpec

  xit "skip", do: "skipped"
  focus "Focused", do: "Focused example"

  it "pending example"
  pending "it is also pending example"
end

Filters

The are --only, --exclude and --string command line options.

One can tag example or context and then use --only or --exclude option to run (or exclude) tests with specific tag.

defmodule FiltersSpec do
  use ESpec

  context "context with tag", context_tag: :some_tag do
    it do: "some example"
    it "example with tag", example_tag: true do
     "another example"
    end
  end
end
mix espec spec/some_spec.exs --only context_tag:some_tag --exclude example_tag

This runs only one test "some example"

You can also filter examples by --string option which filter examples which contain given string in their nested description.

mix espec spec/some_spec.exs --string 'context with tag'

before and finally

before blocks are evaluated before the example and finally runs after the example.

The blocks can return {:shared, key: value, ...} or (like in ExUnit) {:ok, key: value, ...}, so the keyword list will be saved in the dictionary and can be accessed in other before blocks, in the example, and in finally blocks through shared. You can also use a map as a second term in returned tuple: {:shared, %{key: value, ...}}.

Example:

defmodule BeforeAndFinallySpec do
  use ESpec

  before do: {:shared, a: 1}

  context "Context" do
    before do: {:shared, %{b: shared[:a] + 1}}
    finally do: "#{shared[:b]} == 2"

    it do: shared.a |> should(eq 1)
    it do: shared.b |> should(eq 2)

    finally do: "This finally will not be run. Define 'finally' before the example"
  end
end

Note, that finally blocks must be defined before the example. Also note that finally blocks are executed in reverse order. Please see 'spec/before_finally_order_spec.exs' to figure out details.

There is also a short form of 'before' macro which allows to fill in shared dictionary:

before a: 1, b: 2
# which is equivalent to
before do: {shared: a: 1, b: 2}

You can configure 'global' before and finally in spec_helper.exs:

ESpec.configure fn(config) ->
  config.before fn(tags) -> {:shared, answer: 42, tags: tags} end  #can assign values in dictionary
  config.finally fn(shared) -> shared.answer  end     #can access assigns
end

These functions will be called before and after each example which ESpec runs.

config.before accepts example tags as an argument. So all example tags (including tags from parent contexts) are available in config.before. This allows you to run some specific pre-configuration based on tags.

ESpec.configure fn(config) ->
  config.before fn(tags) ->
    if tags[:async] || tags[:custom_tag] == :do_like_async
      PrepareAsyncExecution.setup
    end
    {:shared, tags: tags}
  end
end

before_all and after_all

There are hooks that evaluate before and after all the examples in a module. Use this hooks for complex system setup and tear down.

defmodule BeforeAllSpec do
  use ESpec

  before_all do
    RocketLauncher.start_the_system!
  end

  it do: ...
  it do: ...

  after_all do
    RocketLauncher.stop_the_system!
  end
end

Note, before_all and after_all hooks do not set shared data and do not have access to them. Also note that you can define only one before_all and one after_all hook in a spec module.

'shared' data

shared is used to share data between spec blocks. You can access data by shared.some_key or shared[:some_key]. shared.some_key will raise exception if the key 'some_key' does not exist, while shared[:some_key] will return nil.

The shared variable appears in your before, finally, in config.before and config.finally, in let and example blocks.

before and finally blocks (including 'global') can modify the dictionary when return {:shared, key: value}. The example below illustrates the life-cycle of shared:

spec_helper.exs

ESpec.start

ESpec.configure fn(config) ->
  config.before fn -> {:shared, answer: 42} end         # shared == %{answer: 42}
  config.finally fn(shared) -> IO.puts shared.answer  end    # it will print 46
end

some_spec.exs:

defmodule SharedBehaviorSpec do
  use ESpec

  before do: {:shared, answer: shared.answer + 1}          # shared == %{answer: 43}
  finally do: {:shared, answer: shared.answer + 1}         # shared == %{answer: 46}

  context do
    before do: {:shared, answer: shared.answer + 1}        # shared == %{answer: 44}
    finally do: {:shared, answer: shared.answer + 1}       # shared == %{answer: 45}
    it do: shared.answer |> should(eq 44)
  end
end

So, 'config.finally' will print 46. Pay attention to how finally blocks are defined and evaluated.

let and subject

let and let! have the same behavior as in RSpec. Both defines memoizable functions in 'spec module'. The value will be cached across multiple calls in the same example but not across examples. let is lazy-evaluated, it is not evaluated until the first time the function it defines is invoked. Use let! to force the invocation before each example. A bang version is just a shortcut for:

let :a, do: 1
before do: a()

In example below, let! :a will be evaluated just after before a: 1.

View on GitHub
GitHub Stars816
CategoryDevelopment
Updated3d ago
Forks65

Languages

Elixir

Security Score

85/100

Audited on Mar 28, 2026

No findings