Espec
Elixir Behaviour Driven Development
Install / Use
/learn @antonmi/EspecREADME
ESpec
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:
expectsyntax with pipe operatorexpect smth1 |> to(eq smth2)oris_expected |> to(eq smth)whensubjectis defined;shouldsyntax:smth1 |> should(eq smth2)orshould eq smthwhensubjectis defined.
Note: RSpec syntax
expect(smth1).to eq(smth2)is deprecated and won't work with OTP 21. -
beforeandfinallyblocks (like RSpecbeforeandafter). -
let,let!andsubject. -
Shared examples.
-
Generated examples.
-
Async examples.
-
Mocks with Meck.
-
Doc specs.
-
HTML and JSON outputs.
-
Etc and etc.
Contents
- Installation
- Run specs
- Context blocks and tags
- Examples
- Filters
beforeandfinallybefore_allandafter_allshareddataletandsubject- Shared examples
- Generated examples
- Async examples
- Matchers
assertandrefuteassert_receiveandrefute_receivecapture_ioandcapture_log- Custom matchers
described_module- Mocks
- Datetime Comparison
- Doc specs
- Configuration and options
- Formatters
- Changelog
- Contributing
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: trueorskip: "Reason"- skips examples in the context;focus: true- sets focus to run with--focusoption.
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.
