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 three (!) types of expectation syntax:
-
RSpec syntax with
expecthelper:expect(smth1).to eq(smth2)oris_expected.to eq(smth)whensubjectis defined; expectsyntax with pipe operatorexpect smth1 |> to(eq smth2)oris_expected |> to(eq smth)whensubjectis defined;shouldsyntax:smth1 |> should(eq smth2)orshould eq smthwhensubjectis defined.
-
RSpec syntax with
beforeandfinallyblocks (like RSpecbeforeandafter).let,let!andsubject.- Shared examples.
- Async examples.
- Mocks with Meck.
- Doc specs.
- HTML and JSON outputs.
- Etc and etc.
Contents
- Installation
- Run specs
- Context blocks
- Examples
- Filters
beforeandfinallyshareddataletandsubject- Shared examples
- Async examples
- Matchers
assertandrefuteassert_receiveandrefute_receive- Custom matchers
described_module- Mocks
- Doc specs
- Configuration and options
- Contributing
Installation
Add espec to dependencies in the mix.exs file:
def deps do
...
{:espec, "~> 0.8.24", only: :test},
#{:espec, github: "antonmi/espec", only: :test}, to get the latest version
...
endmix deps.getThen run:
MIX_ENV=test mix espec.init
The task creates spec/spec_helper.exs and spec/example_spec.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 SomeSpec do
use ESpec
it do: expect true |> to(be_true)
it do: expect(1+1).to eq(2)
it do: (1..3) |> should(have 2)
endRun specs
mix especRun specific spec:
mix espec spec/some_spec.exs:25Read the help:
MIX_ENV=test mix help especContext blocks
There are three macros with the same functionality: context, describe, and example_group.
Context can have description and options.
defmodule SomeSpec 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
endAvailable options 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 options for this context after use ESpec:
defmodule SomeSpec do
use ESpec, skip: "Skip all examples in the module"
...
endExamples
example, it, and specify macros define the 'spec example'.
defmodule SomeSpec do
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 options 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 SomeSpec 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 SomeSpec 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
endmix espec spec/some_spec.exs --only context_tag:some_tag --exclude example_tagThis runs only one test "some example"
You can also filter examples by --string option which filter examples which contain given string it 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`:
elixir defmodule SomeSpec 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.
You can configure 'global' before and finally in spec_helper.exs:
elixir ESpec.start ESpec.configure fn(config) -> config.before fn -> {:shared, answer: 42} 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.
## '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 'somekey' 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 dictionay when return {:shared, key: value}.
The example bellow illustrate the life-cycle of shared:
spec_helper.exselixir ESpec.start ESpec.configure fn(config) -> config.before fn -> {:shared, answer: 42} end # shared == %{anwser: 42} config.finally fn(shared) -> IO.puts shared.answer end # it will print 46 endsome_spec.exs:
elixir defmodule SomeSpec do use ESpec before do: {:shared, answer: shared.answer + 1} # shared == %{anwser: 43} finally do: {:shared, answer: shared.answer + 1} # shared == %{anwser: 46} context do before do: {:shared, answer: shared.answer + 1} # shared == %{anwser: 44} finally do: {:shared, answer: shared.answer + 1} # shared == %{anwser: 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 subjectlet and let! have the same behaviour 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 not evaluated until the first time the function it defines is invoked. Use let! to force the invocation before each example.
The shared is available in 'lets' but neither let nor let! can modify the dictionary.
elixir defmodule SomeSpec do use ESpec before do: {:shared, a: 1} let! :a, do: shared.a let :b, do: shared.a + 1 it do: expect a |> to(eq 1) it do: expect b |> to(eq 2) endsubject and subject! are just aliases for let :subject, do: smth and let! :subject, do: smth. You can use is_expected macro (or a simple should expression) when subject is defined.
elixir defmodule SomeSpec do use ESpec subject(1+1) it do: is_expected |> to(eq 2) it do: should eq 2 context "with block" do subject do: 2+2 it do: is_expected |> to_not(eq 2) it do: should_not eq 2 end end
## Shared Examples
One can reuse the examples defined in spec module.
elixir defmodule SharedSpec do use ESpec, shared: true subject shared.hello it do: should eq("world!") endshared: true marks examples in the module as shared, so the examples will be skipped until you reuse them.
You can use the examples with it_behaves_like or its alias include_examples macro:
elixir defmodule UseSharedSpec do use ESpec before do: {:ok, hello: "world!"} it_behaves_like(SharedSpec) #or include_examples(SharedSpec) end
Be carefull with let and let! when using shared examples. You can't access let from 'spec module' in 'shared module'.
elixir defmodule SomeSharedSpec do use ESpec, shared: true it do: expect a |> to(eq "a") end defmodule SomeSpec do use ESpec let :a, do: "a" include_examples(SomeSharedSpec) end
Elixir will not compile SomeSharedSpec module with error: function a/0 undefined. So use shared dictionary in 'before' block to set the value.
## Async examples
There is an async: true option you can set for the context or for the individual example:
elixir defmodule AsyncSpec do use ESpec, async: true it do: "async example" context "Sync", async: false do it do: "sync example" it "async again", async: true do "async" end end end
The examples will be partitioned into two queries. Examples in asynchronous query will be executed in parallel in different processes.
Don't use async: true if you change the global state in your specs!
## Matchers
#### Equality
elixir expect actual |> to(eq expected) # passes if actual == expected expect actual |> to(eql expected) # passes if actual === expected expect actual |> to(be_close_to expected, delta) expect actual |> to(be_between hard_place, rock)
#### Comparisons
Can be used with :>, :<, :>=, :<=, and etc.
elixir expect actual |> to(be operator, value)
Passes if apply(Kernel, operator, [actual, value]) == true
#### Booleans
elixir expect actual |> to(be_true) expect actual |> to(be_truthy) expect actual |> to(be_false) expect actual |> to(be_falsy)
#### Regular expressions
elixir expect actual |> to(match ~r/expression/) expect actual |> to(match "string")
#### Enumerable
There are many helpers to test enumerable collections:
elixir expect collection |> to(be_empty) #Enum.count(collection) == 0 ... have value #Enum.member?(collection, value) ... have_all func #Enum.all?(collection, func) ... have_any func #Enum.any?(collection, func) ... have_at position, value #Enum.at?(collection, position) == value ... have_count value #Enum.count(collection) == value ... have_size value #alias ... have_length value #alias ... have_count_by func, value #Enum.count(collection, func) == value ... have_max value #Enum.max(collection) == value ... have_max_by func, value #Enum.max_by(collection, fun) == value ... have_min value #Enum.min(collection) == value ... have_min_by func, value #Enum.min_by(collection, fun) == value
#### List
elixir expect list |> to(have_first value) #List.first(list) == value ... have_last value #List.last(list) == value ... have_hd value #hd(list) == value ... have_tl value #tl(list) == value
#### String
elixir expect string |> to(have_first value) #String.first(string) == value ... have_last value #String.last(string) == value ... start_with value #String.starts_with?(string, value) ... end_with value #String.end_with?(string, value) ... have value #String.contains?(string, value) ... have_at pos, value #String.at(string, pos) == value ... have_length value #Stirng.length(string) == value ... have_size value #alias ... have_count value #alias ... be_valid_string #String.valid?(string) ... be_printable #String.printable?(string)
#### Dict
elixir expect dict |> to(have_key value) #Dict.has_key?(value) expect dict |> to(have_value value) #Enum.member?(Dict.values(dict), value)
#### Type checking
elixir expect :espec |> to(be_atom) #is_atom(:espec) == true ... be_binary ... be_bitstring ... be_boolean ... ... ... ... ... be_tuple ... be_function ... be_function arity ... be_struct ... be_struct StructExample
#### Exceptions
elixir expect function |> to(raise_exception) expect function |> to(raise_exception ErrorModule) expect function |> to(raise_exception ErrorModule, "message")
#### Throws
elixir expect function |> to(throw_term) expect function |> to(throw_term term)
#### Change state
Test if call of function1 change the function2 returned value to smth or from to smth
elixir expect function1 |> to(change function2, to) expect function1 |> to(change function2, from, to)
#### Check result
Test if function returns {:ok, result} or {:error, reason} tuple
elixir expect {:ok, :the_result} |> to(be_ok_result) expect {:error, :an_error} |> to(be_error_result)
## assert and refute
If you are missing ExUnit assert and refute, ESpec has such functions as aliases to be_truthy and be_falsyelixir defmodule SomeSpec do use ESpec it "asserts" do assert "ESpec" #expect "ESpec" |> to(be_truthy) end it "refutes" do refute nil #expect nil |> to(be_falsy) end end
## assert_receive and refute_receiveassert_receive (assert_received) and refute_receive (refute_received) work identically to ExUnit ones.
assert_receive asserts that a message matching pattern was or is going to be received within timeout.
assert_received asserts that a message was received and is in the current process mailbox. It is the same as assert_receive with 0 timeout.
refute_receive asserts that a message matching pattern was not received and won’t be received within the timeout.
refute_received asserts that a message was not received (refute_receive with 0 timeout).
The default timeout for assert_receive and refute_receive is 100ms. You can pass custom timeout as a second argument.
elixir defmodule SomeSpec do use ESpec it "demonstrates assert_received" do send(self(), :hello) assert_received :hello end it "demonstrates assert_receive with custom timeout" do parent = self() spawn(fn -> :timer.sleep(200); send(parent, :hello) end) assert_receive(:hello, 300) end it "demonstrates refute_receive" do send(self, :another_hello) refute_receive :hello_refute end end
## Custom matchers
You can define your own matchers!
The only functions you should implement is match/2, success_message/4, and error_message.
Read the wiki page for detailed instructions.
There is an example in custom_assertion_spec.exs.
## described_module
If you keep the naming convention 'module TheModuleSpec is spec for TheModule' you can access tested module by described_module helper.
elixir defmodule TheModule do def fun, do: :fun end defmodule TheModuleSpec do use ESpec it do: expect described_module.fun |> to(eq :fun) end
## Mocks
ESpec uses Meck to mock functions.
You can mock the module with 'allow accept':
elixir defmodule SomeSpec do use ESpec context "with old syntax" before do: allow(SomeModule).to accept(:func, fn(a, b) -> a + b end) it do: expect SomeModule.func(1, 2) |> to(eq 3) end context "with new syntax" before do: allow SomeModule |> to(accept :func, fn(a, b) -> a + b end) it do: expect SomeModule.func(1, 2) |> to(eq 3) end end
If you don't specify the function to return ESpec creates stubs with arity 0 and 1:
fn -> end and `fn() -> end, which returnnil. ```elixir defmodule SomeSpec do use ESpec before do: allow SomeModule |> to(accept :func) it do: expect SomeModule.func |> to(be_nil) it do: expect SomeModule.func(42) |> to(be_nil) end ``` You can also use pattern matching in your mocks: ```elixir defmodule SomeSpec do use ESpec before do args = {:some, :args} allow SomeModule |> to(accept :func, fn(^args) -> {:ok, :success} end) end it do: expect SomeModule.func({:some, :args}) |> to(be_ok_result) it "raises exception when does not match" do expect(fn -> SomeModule.func({:wrong, :args}) end) |> to(raise_exception FunctionClauseError) end end ``` Behind the scenes 'allow accept' makes the following: ```elixir :meck.new(module, [:non_strict, :passthrough]) :meck.expect(module, name, function) ``` Find the explanation aboute the:nonstrictand:passthroughoptions [here](https://github.com/eproxus/meck/blob/master/src/meck.erl). The default options ([:non_strict, :passthrough]) can be overridden: ```elixir allow SomeModule) |> to(accept :func, fn(a,b) -> a+b end, [:non_strict, :unstick]) ``` All the mocked modules are unloaded whith:meck.unload(modules)after each example. You can also pass a list of atom-function pairs to theacceptfunction: ```elixir allow SomeModule |> to(accept f1: fn -> :f1 end, f2: fn -> :f2 end) ``` One can usepassthrough/1` function to call the original function:
```elixir
before do
allow SomeModule |> to(accept(:fun, fn
:mocked -> "mock!"
-> passthrough([args])
end))
end
it do: expect SomeModule.fun(:mocked) |> to(eq "mock!")
it do: expect SomeModule.fun(2) |> to(eq 3)
The `passthrough/1` just calls the `:meck.passthrough/1` from the `:meck` module. There is also an expectation to check if the module accepted a function call:elixir
accepted(func, args \ :any, opts \ [pid: :any, count: :any])
So, the options are: - test if the function is called with some particular arguments of with `any`; - specify the `pid` of the process which called the function; - test the count of function calls.elixir
defmodule SomeSpec do
use ESpec
before do
allow SomeModule |> to(accept :func, fn(a,b) -> a+b end)
SomeModule.func(1, 2)
end
it do: expect SomeModule |> to(accepted :func)
it do: expect SomeModule |> to(accepted :func, [1,2])
describe "with options" do
defmodule Server do
def call(a, b) do
ESpec.SomeModule.func(a, b)
ESpec.SomeModule.func(a, b)
end
end
before do
pid = spawn(Server, :call, [1, 2])
:timer.sleep(100)
{:ok, pid: pid}
end
it do: expect ESpec.SomeModule |> to(accepted :func, [1,2], pid: shared.pid, count: 2)
end
end
`accepted` assertion checks `:meck.history(SomeModule)`. See [meck](https://github.com/eproxus/meck) documentation. Don't use `async: true` when using mocks! ### Limitations Meck has trouble mocking certain modules, such as `erlang`, `os`, and `timer`. Also, meck does not track module-local calls. For example, this will not be tracked:elixir
defmodule SomeModule
def somefunc, do: anotherfunc
def anotherfunc, do: nil
end
But this will:elixir
defmodule SomeModule
def somefunc, do: __MODULE.another_func
def another_func, do: nil
end
`` It is recommended to prefix module-local calls with__MODULEwhen using them with meck. See [this section in the meck README](https://github.com/eproxus/meck#caveats) for a more detailed explanation. ## Doc specs ESpec has functionality similar to [ExUnit.DocTest](http://elixir-lang.org/docs/stable/ex_unit/). Read more about docs syntax [here](http://elixir-lang.org/docs/stable/ex_unit/) The functionality is implemented by two modules:ESpec.DocExampleparses module documentation andESpec.DocTestcreates 'spec' examples for it.ESpec.DocExamplefunctions is just copy-paste ofExUnit.Doctestparsing functionality.ESpec.DocTestimplementdoctestmacro which identical toExUnitanalogue. ```elixir defmodule SomeSpec do use ESpec doctest MySuperModule end ``` There are three options (similar toExUnit.DocTest)::except- generate specs for all functions except those listed (list of {function, arity} tuples). ```elixir defmodule SomeSpec do use ESpec doctest MySuperModule, except: [fun: 1, func: 2] end ```:only— generate specs only for functions listed (list of {function, arity} tuples). And:importto test a function defined in the module without referring to the module name.Default isfalse. Use this option with care because you can clash with another modules. There are three types of specs can be generated based on docs. - Examples where input and output can be evaluated. For example: ```elixir @doc """ iex> Enum.map [1, 2, 3], fn(x) -> ...> x * 2 ...> end [2,4,6] """ ``` Such examples will be converted to: ```elixir it "Example description" do expect input |> to(eq output) end ``` - Examples which return complex structure so Elixir prints it as#Name<...>.: ```elixir @doc """ iex> Enum.into([a: 10, b: 20], HashDict.new) #HashDict<[b: 20, a: 10]> """ ``` The examples will be converted to: ```elixir it "Example description" do expect inspect(input) |> to(eq output) end ``` - Examples with exceptions: ```elixir @doc """ iex(1)> String.to_atom((fn() -> 1 end).()) ** (ArgumentError) argument error """ ``` The examples will be tested as: ```elixir it "Example description" do expect fn -> input end |> to(raise_exception error_module, error_message) end ``` ## Configuration and options ```shMIX_ENV=test mix help espec``` #### Spec paths and pattern You can change (inmix.exsfile) the folder where your specs are and the pattern to match the files. ```elixir def project do ... spec_paths: ["my_specs", "espec"], spec_pattern: "*_espec.exs", ... end ``` #### Coverage One can run specs with coverage: ```sh mix espec --cover ``` Find the results in/coverfolder. ESpec, like ExUnit, uses very simple wrapper around OTP's cover. But you can override this. Take a look to [coverex](https://github.com/alfert/coverex) as a perfect example. #### Output formats There are three formatters in ESpec: 'doc', 'json' and 'html'. Example: ```sh mix espec --format=doc ``` The 'doc' format will print detailed description of example and its context.--traceoption is an alias for--format=doc. ```sh mix espec --trace ``` 'html' and 'json' formatters prepare pretty HTML and JSON outputs. You may use--formatwith--out` option to write output to the file.
sh mix espec --format=html --out=spec.html
## Contributing
##### Contributions are welcome and appreciated!
Request a new feature by creating an issue.
Create a pull request with new features or fixes.
ESpec is tested using ExUnit and ESpec. So run:
sh mix test mix espec