antonmi/espec
{ "createdAt": "2015-03-17T16:06:01Z", "defaultBranch": "main", "description": "Elixir Behaviour Driven Development", "fullName": "antonmi/espec", "homepage": "", "language": "Elixir", "name": "espec", "pushedAt": "2025-06-02T21:00:13Z", "stargazersCount": 815, "topics": [ "bdd", "bdd-specs", "elixir", "testing" ], "updatedAt": "2025-10-12T16:52:07Z", "url": "https://github.com/antonmi/espec"}ESpec is a BDD testing framework for Elixir.
Section titled “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
Section titled “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
Section titled “Note: RSpec syntax expect(smth1).to eq(smth2) is deprecated and won’t work with OTP 21.”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
Section titled “Contents”- [Installation]!(#installation)
- [Run specs]!(#run-specs)
- [Context blocks and tags]!(#context-blocks-and-tags)
- [Examples]!(#examples)
- [Filters]!(#filters)
- [
beforeandfinally]!(#before-and-finally) - [
before_allandafter_all]!(#before_all-and-after_all) - [
shareddata]!(#shared-data) - [
letandsubject]!(#let-and-subject) - [Shared examples]!(#shared-examples)
- [Generated examples]!(#generated-examples)
- [Async examples]!(#async-examples)
- [Matchers]!(#matchers)
- [
assertandrefute]!(#assert-and-refute) - [
assert_receiveandrefute_receive]!(#assert_receive-and-refute_receive) - [
capture_ioandcapture_log]!(#capture_io-and-capture_log) - [Custom matchers]!(#custom-matchers)
- [
described_module]!(#described_module) - [Mocks]!(#mocks)
- [Datetime Comparison]!(#datetime-comparison)
- [Doc specs]!(#doc-specs)
- [Configuration and options]!(#configuration-and-options)
- [Formatters]!(#formatters)
- [Changelog]!(#changelog)
- [Contributing]!(#contributing)
Installation
Section titled “Installation”Add espec to dependencies in the mix.exs file:
def deps do ... {:espec, "~> 1.10.0", only: :test}, ...endmix deps.getThen run:
MIX_ENV=test mix espec.initThe 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], ...endOr run with MIX_ENV=test:
MIX_ENV=test mix especPlace 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)endRun specs
Section titled “Run specs”mix especRun specific spec:
mix espec spec/some_spec.exs:25Read the help:
MIX_ENV=test mix help especContext blocks and tags
Section titled “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 endendAvailable 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" ...endExamples
Section titled “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"endYou 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"endFilters
Section titled “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 endendmix 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 in their nested description.
mix espec spec/some_spec.exs --string 'context with tag'before and finally
Section titled “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]!(#shared-data).
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" endendNote, 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 tobefore 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 assignsendThese 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} endendbefore_all and after_all
Section titled “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! endendNote, 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
Section titled “‘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 46endsome_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) endendSo, ‘config.finally’ will print 46.
Pay attention to how finally blocks are defined and evaluated.
let and subject
Section titled “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: 1before do: a()In example below, let! :a will be evaluated just after before a: 1. But let :b will be invoked only in the last test.
defmodule LetSpec do use ESpec
before 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)endNote, The shared is available in lets but neither let nor let! can modify the dictionary.
You can pass a keyword list to let or let! to define several ‘lets’ at once:
defmodule LetSpec do use ESpec
let a: 1, b: 2
it do: expect a() |> to(eq 1) it do: expect b() |> to(eq 2)endNote, subject 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.
defmodule SubjectSpec 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) endendThere are helpers that can help you assign values from expressions that return {:ok, result} or {:error, result} tuples. For example, File.read\1 returns {:ok, binary} or {:error, reason}.
There are let_ok (let_ok!) and let_error (let_error!) functions that allow you assign values easily:
let_ok :file_binary, do: File.read("file.txt")let_error :error_reason, do: File.read("error.txt")Shared Examples
Section titled “Shared Examples”One can reuse the examples defined in spec module.
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:
defmodule UseSharedSpec do use ESpec
before hello: "world!" it_behaves_like(SharedSpec) #or include_examples(SharedSpec)endYou can also use let variables from parent module in shared examples.
Use let_overridable macro to define let which will be overridden.
You can pass single atom, list of atoms, or keyword with default values.
See examples below.
defmodule SharedSpec do use ESpec, shared: true, async: true
let_overridable a: 10, b: 20 let_overridable [:c, :d] let_overridable :e
let :internal_value, do: :shared_spec
it "will be overridden" do expect a() |> to(eq 1) expect c() |> to(eq 3) expect e() |> to(eq 5) end
it "returns defaults" do expect b() |> to(eq 20) expect d() |> to(eq nil) end
it "does not override internal 'lets'" do expect internal_value() |> to(eq :shared_spec) endend
defmodule LetOverridableSpec do use ESpec, async: true
let :internal_value, do: :some_spec
it_behaves_like(SharedSpec, a: 1, c: 3, e: 5)endShared Examples in separate files
Section titled “Shared Examples in separate files”In case you want to add some “global” shared specs which you want to use in multiple specs, ESpec has you covered. Simply add these files to your spec/shared folder. The place where mix espec.init generates you a placeholder folder and file.
By default ESpec loads all files contained in <your_spec_paths>/shared which match your spec_pattern.
The [Configuration and options]!(#configuration-and-options) chapter contains details on how to control this behaviour.
In case you already use Code.require_file/1 in your spec_helper.exs don’t sweat. ESpec makes sure to require each file only once, it ignores files which already have been included.
Generated examples
Section titled “Generated examples”Examples can be generated from code “templates”. This should help with making the code more DRY:
defmodule GeneratedExamplesSpec do use ESpec, async: true
subject(24)
Enum.map 2..4, fn(n) -> it "is divisible by #{n}" do expect rem(subject(), unquote(n)) |> to(eq 0) end endendPlease mind the unquote call above - if you forget to unquote the n variable the compiler will show some warnings about it missing and eventually stop with an error: undefined function n/0.
Async examples
Section titled “Async examples”There is an async: true option you can set for the context or for the individual example:
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 endendThe 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
Section titled “Matchers”Equality
Section titled “Equality”expect actual |> to(eq expected) # passes if actual == expectedexpect actual |> to(eql expected) # passes if actual === expectedexpect actual |> to(be_close_to expected, delta)expect actual |> to(be_between hard_place, rock)Comparisons
Section titled “Comparisons”Can be used with :>, :<, :>=, :<=, and etc.
expect actual |> to(be operator, value)Passes if apply(Kernel, operator, [actual, value]) == true
Patterns
Section titled “Patterns”expect actual |> to(match_pattern {:ok, _}) # {:ok, _} = actualIt’s not possible to call functions in the pattern and use the return value as
pattern ({:ok, function()}), this obviously means no let functions. If you
neeed to use the return value of a function, use a variable:
value = function()
expect actual |> to(match_pattern {:ok, ^value})Booleans
Section titled “Booleans”expect actual |> to(be_true())expect actual |> to(be_truthy())expect actual |> to(be_false())expect actual |> to(be_falsy())Regular expressions
Section titled “Regular expressions”expect actual |> to(match ~r/expression/)expect actual |> to(match "string")Enumerable
Section titled “Enumerable”There are many helpers to test enumerable collections:
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) == valueexpect 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... contain_exactly value # Keyword.equals?(list, value)... match_list value # alias for contain_exactlyBinary
Section titled “Binary”expect binary |> to(have_byte_size value) # byte_size(binary) == valueString
Section titled “String”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 # String.length(string) == value... have_size value # alias... have_count value # alias... be_valid_string() # String.valid?(string)... be_printable() # String.printable?(string)... be_blank() # String.length(string) == 0... be_empty() # String.length(string) == 0expect map |> to(have foo: "bar") # Map.get(map, :foo) == "bar"expect map |> to(have {:foo, "bar"}) # Map.get(map, :foo) == "bar"expect map |> to(have {"foo", "bar"}) # Map.get(map, :foo) == "bar"expect map |> to(have_key value) # Map.has_key?(map, value)expect map |> to(have_value value) # Enum.member?(Map.values(map), value)expect pid |> to(be_alive) # Process.alive?(pid)have also works for Structs.
Type checking
Section titled “Type checking”expect :espec |> to(be_atom) #is_atom(:espec) == true... be_binary()... be_bitstring()... be_boolean()... be_integer()... be_float()... be_number()... ...... ...... be_tuple()... be_list()... be_map()... be_port()... be_pid()... be_reference()... be_function()... be_function arity... be_struct()... be_struct StructExampleExceptions
Section titled “Exceptions”expect function |> to(raise_exception())expect function |> to(raise_exception ErrorModule)expect function |> to(raise_exception ErrorModule, "message")Throws
Section titled “Throws”expect function |> to(throw_term())expect function |> to(throw_term term)Change function’s return value
Section titled “Change function’s return value”Test if call of function1 change the function2 returned value to smth or from to smth
expect function1 |> to(change function2)expect function1 |> to(change function2, to)expect function1 |> to(change function2, from, to)expect function1 |> to(change function2, by: value)Check result
Section titled “Check result”Test if function returns {:ok, result} or {:error, reason} tuple
expect {:ok, :the_result} |> to(be_ok_result())expect {:error, :an_error} |> to(be_error_result())assert and refute
Section titled “assert and refute”If you are missing ExUnit assert and refute, ESpec has such functions as aliases to be_truthy and be_falsy
defmodule AssertAndRefuteSpec do use ESpec
it "asserts" do assert "ESpec" #expect "ESpec" |> to(be_truthy()) end
it "refutes" do refute nil #expect nil |> to(be_falsy()) endendassert_receive and refute_receive
Section titled “assert_receive and refute_receive”assert_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.
defmodule AssertReceviveSpec 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 endendcapture_io and capture_log
Section titled “capture_io and capture_log”capture_io and capture_log are just copied from ExUnit and designed to test IO or Logger output:
defmodule CaptureSpec do use ESpec
it "tests capture_io" do message = capture_io(fn -> IO.write "john" end) message |> should(eq "john") end
it "tests capture_log" do message = capture_log(fn -> Logger.error "log msg" end) expect message |> to(match "log msg") endendCustom matchers
Section titled “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.
Extensions
Section titled “Extensions”There are community supported projects with sets of mathers:
described_module
Section titled “described_module”If you keep the naming convention ‘module TheModuleSpec is spec for TheModule’ you can access tested module by described_module() helper.
defmodule TheModule do def fun, do: :funend
defmodule TheModuleSpec do use ESpec it do: expect described_module().fun |> to(eq :fun)endESpec uses Meck to mock functions. You can mock the module with ‘allow accept’:
defmodule MocksSpec do use ESpec context "with old syntax" do 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" do before do: allow SomeModule |> to(accept :func, fn(a, b) -> a + b end) it do: expect SomeModule.func(1, 2) |> to(eq 3) endendIf you don’t specify the function to return ESpec creates stubs with arity 0 and 1:
fn -> end and fn(_) -> end, which return nil.
defmodule DefaultMocksSpec 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())endYou can also use pattern matching in your mocks:
defmodule PatternMockSpec 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) endendBehind the scenes ‘allow accept’ makes the following:
:meck.new(module, [:non_strict, :passthrough]):meck.expect(module, name, function)Find the explanation about the :non_strict and :passthrough options here.
The default options ([:non_strict, :passthrough]) can be overridden:
allow SomeModule |> to(accept :func, fn(a,b) -> a + b end, [:non_strict, :unstick])All the mocked modules are unloaded with :meck.unload(modules) after each example.
You can also pass a list of atom-function pairs to the accept function:
allow SomeModule |> to(accept f1: fn -> :f1 end, f2: fn -> :f2 end)One can use passthrough/1 function to call the original function:
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:
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
pidof the process which called the function; - test the count of function calls.
defmodule MockOptionsSpec 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) endendaccepted assertion checks :meck.history(SomeModule). See meck documentation.
Don’t use async: true when using mocks!
Datetime Comparison
Section titled “Datetime Comparison”ESpec has comparison support for Elixir’s date(time) related structs. Specifically, it has support for Date, Time, NaiveDateTime, and DateTime structs using ESpec’s be_close_to and be assertions. It allows you to compare using the lowest-level granularity available in the struct. For example, since the lowest level of granularity available in a NaiveDateTime is the microsecond, you can compare how close to NaiveDateTime structs are with respect to microseconds.
Datetime be assertion(s) syntax
Section titled “Datetime be assertion(s) syntax”For the be assertions, ESpec supports a syntax with a granularity tuple (or keyword list) or a syntax without it. The following examples are shown with a Date struct.
Be assertion syntax without granularity
Section titled “Be assertion syntax without granularity”it do: expect ~D[2020-08-07] |> to(be :>=, ~D[2017-08-07])Be assertion syntax with granularity
Section titled “Be assertion syntax with granularity”it do: expect ~D[2020-08-07] |> to(be :>=, ~D[2017-08-07], {:years, 3})
# or alternatively, you can do:it do: expect ~D[2020-08-07]) |> to(be :>=, ~D[2017-08-07], years: 3)Datetime be_close_to asssertion(s) syntax
Section titled “Datetime be_close_to asssertion(s) syntax”Date Struct Comparison Example(s)
Section titled “Date Struct Comparison Example(s)”expect ~D[2017-08-07] |> to(be_close_to(~D[2018-08-07], {:years, 1}))
# or alternatively, you can do:it do: expect ~D[2017-08-07] |> to(be_close_to(~D[2020-08-07], {:years, 3}))NaiveDateTime Struct Comparison Example
Section titled “NaiveDateTime Struct Comparison Example”expect ~N[2017-08-07 01:10:10.000001] |> to(be_close_to(~N[2017-08-07 01:10:10.000003], {:microseconds, 2}))
# or alternatively, you can do:it do: expect ~N[2017-08-07 01:10:10.000001] |> to(be_close_to(~N[2017-08-07 01:10:10.000003], {:microseconds, 2}))Time Struct Comparison Example
Section titled “Time Struct Comparison Example”expect ~T[01:10:10] |> to(be_close_to(~T[01:50:10], {:minutes, 40}))
# or alternatively, you can do:it do: expect ~T[01:10:10] |> to(be_close_to(~T[01:50:10], {:minutes, 40}))DateTime Struct Comparison Example
Section titled “DateTime Struct Comparison Example”Note the example shows a DateTime comparison with utc and std offsets for time zone differences. It is up to the user to be aware of the time zone utc and std offsets.
context "Success with DateTime with utc and std offsets to represent time zone differences" do let :datetime_pst, do: %DateTime{year: 2017, month: 3, day: 15, hour: 1, minute: 30, second: 30, microsecond: {1, 6}, std_offset: 1*3600, utc_offset: -8*3600, zone_abbr: "PST", time_zone: "America/Los_Angeles"} let :datetime_est, do: %DateTime{year: 2017, month: 3, day: 15, hour: 6, minute: 30, second: 30, microsecond: {1, 6}, std_offset: 1*3600, utc_offset: -5*3600, zone_abbr: "EST", time_zone: "America/New_York"}
it do: expect datetime_pst() |> to(be_close_to(datetime_est(), {:hours, 2}))endLimitations
Section titled “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:
defmodule SomeModule def some_func, do: another_func()
def another_func, do: nilendBut this will:
defmodule SomeModule def some_func, do: __MODULE__.another_func()
def another_func, do: nilendIt is recommended to prefix module-local calls with __MODULE__ when using them with meck.
See this section in the meck README for a more detailed explanation.
Doc specs
Section titled “Doc specs”ESpec has functionality similar to ExUnit.DocTest.
Read more about docs syntax here
The functionality is implemented by two modules:
ESpec.DocExample parses module documentation and ESpec.DocTest creates ‘spec’ examples for it.
ESpec.DocExample functions are just copy-pasted from ExUnit.Doctest parsing functionality.
ESpec.DocTest implements doctest macro which is identical to ExUnit.
defmodule DoctestSpec do use ESpec doctest MySuperModuleendThere are three options (similar to ExUnit.DocTest):
:except - generate specs for all functions except those listed (list of {function, arity} tuples).
defmodule DoctestOptionsSpec do use ESpec doctest MySuperModule, except: [fun: 1, func: 2]end:only — generate specs only for functions listed (list of {function, arity} tuples).
And :import to test a function defined in the module without referring to the module name.Default is false. Use this option with care because you can clash with other modules.
There are three types of specs can be generated based on docs.
- Examples where input and output can be evaluated. For example:
@doc """iex> Enum.map [1, 2, 3], fn(x) ->...> x * 2...> end[2,4,6]"""Such examples will be converted to:
it "Example description" do expect input |> to(eq output)end- Examples which return complex structure so Elixir prints it as
#Name<...>.:
@doc """iex> Enum.into([a: 10, b: 20], HashDict.new)#HashDict<[b: 20, a: 10]>"""The examples will be converted to:
it "Example description" do expect inspect(input) |> to(eq output)end- Examples with exceptions:
@doc """iex(1)> String.to_atom((fn() -> 1 end).())** (ArgumentError) argument error"""The examples will be tested as:
it "Example description" do expect fn -> input end |> to(raise_exception error_module, error_message)endConfiguration and options
Section titled “Configuration and options”`MIX_ENV=test mix help espec`Spec paths and pattern
Section titled “Spec paths and pattern”You can change (in the mix.exs file) the folder where your specs are and the pattern to match these files.
def project do ... spec_paths: ["my_specs", "espec"], spec_pattern: "*_espec.exs", ... endShared spec paths and pattern
Section titled “Shared spec paths and pattern”In addition to specifying the spec paths you can also tell ESpec where to find your SharedSpecs.
def project do ... shared_spec_paths: ["my_specs/shared", "espec/my_shared"], shared_spec_pattern: "*_shared.exs", ... endCoverage
Section titled “Coverage”One can run specs with coverage:
mix espec --coverFind the results in /cover folder.
ESpec, like ExUnit, uses very simple wrapper around OTP’s cover. But you can override this.
Take a look to coverex as a perfect example.
Similar to ExUnit, the --stale command line option attempts to run only those test files which reference modules that have changed since the last time you ran this task with --stale.
Running the whole test suite:
mix espec --staleRunning individual file(s):
mix espec spec/assertions/be_spec.exs spec/assertions/be_close_to_spec.exs --staleFormatters
Section titled “Formatters”There are three formatters in ESpec:
- ESpec.Formatters.Doc
- ESpec.Formatters.Json
- ESpec.Formatters.Html
The Doc formatter tries to help you read the failed tests results by doing diffs between the expected and actual values in some cases (the eq and eql assertions, for example). If you don’t want this you can disable it like this:
ESpec.configure fn(config) -> config.formatters [ {ESpec.Formatters.Doc, %{diff_enabled?: false}} ]endBy default ESpec uses ‘Doc’ with empty options. In order to use another one, you must specify formatters in ‘ESpec.config’
For example:
ESpec.configure fn(config) -> config.formatters [ {ESpec.Formatters.Json, %{out_path: "results.json"}}, {ESpec.Formatters.Html, %{out_path: "results.html"}}, {ESpec.YouCustomFormatter, %{a: 1, b: 2}}, ]endESpec design allows custom formatters of test results.
The custom formatter is a module which use ESpec.Formatters.Base and implement 3 functions:
init/1handle_cast/2forexample_finishedeventhandle_cast/2forfinal_resultevent
Take a look at lib/espec/formatters and spec_formatters folders to see examples
Other formatters
Section titled “Other formatters”There are community supported formatters:
Changelog
Section titled “Changelog”- 0.2.0:
- Basic functionality (contexts, ‘before’ and ‘let’, mocking, basic matchers)
- 0.3.0:
- Tags for examples and contexts
- ‘config.before’ and ‘config.finally’
- 0.4.0:
- Lots of internal changes
- Shared examples
- 0.5.0:
- ‘count’, ‘pid’ and ‘args’ options in ‘accepted’ assertion
- ‘async’ option for parallel execution
- 0.6.0:
- String and Dictionary matchers
- Doctests
- 0.7.0:
- Mocking options
- Html and Json outputs
- capture_io
- 0.8.0:
- ‘only’ and ‘exclude’ options
- ‘double_underscore’ replaced by ‘shared’
- 1.0.0:
- ‘let’ implementation rewritten completely
- ‘assert_receive’ and ‘refute_receive’
- ‘let_overridable’ for shared examples
- ‘let_ok’ and ‘let_error’
- new syntax with pipe
- 1.1.0:
- capture_log
- ‘let’ and ‘before’ with keyword
- 1.1.1:
- Fix ‘finally’ execution order
- 1.1.2:
- Added support for unicode characters in example names
- 1.2.0:
- before_all and after_all callbacks
- 1.2.1:
- removed module name duplication in example description
- fix statistic output for async examples
- 1.2.2:
- Elixir 1.4.0 warnings ware fixed
- 1.3.0
- Formatters were refactored to support custom ones
- 1.3.1
- Structure diffs were added to ‘eq’ matcher
- 1.3.2
- Generated examples were added
- 1.3.3
- Bug fix structure diff were fixed
- 1.3.4
- Line number option for contexts
- 1.4.0
- Pretty diffs for failed specs
- ‘let’ agent fix
- ‘contain_exactly’, ‘match_list’ and ‘change_by’ assertions
- Elixir 1.2 is no longer supported
- 1.4.1
- Configurable timeouts for output formatters
- 1.4.2
- Fix Elixir 1.5.0 issues
- 1.4.3
- Fix options issues
- 1.4.4
- Stacktrace for failed examples
- 1.4.5
- Update ‘meck’ to fix issue with elrang 20 mocks
- Fixed options passing
- 1.4.6
- Fix doctests (allow “strings”)
- allow keywords in
let_okandlet_error - Fix
beforeto ignore not enumerables
- 1.5.0
- Add
beandbe_close_toassertions forDate,Time,NaiveDateTime, andDateTime - ‘have’ matcher for Map
- ‘let’ works for generated examples
- ‘match_pattern’ macro
- Add
- 1.5.1
- Code formatting
- Improve
haveandeqassertions - Fix
letcaching for shared examples
- 1.6.0
- Compatibility with OTP 21
- 1.6.1
- Doctest fix for Elixir >= 1.7
- 1.6.2
- Fix ‘let’ caching in shared specs
- 1.6.3
- Use ‘Task.async_stream’ for async examples
- 1.6.3
- Use ‘Task.async_stream’ for async examples
- 1.6.4
- Run every test in separate process
- 1.6.5
- Update
meckdependency
- Update
- 1.7.0
- Follows deprecation of plural time units (:seconds, :microseconds, etc). Allows only singular.
- Remove support of Elixir 1.5.
- 1.8.0
- Add —stale option.
- Remove support of Elixir 1.6.
- 1.8.1
- Fix compilation warnings.
- 1.8.2
- Elixir 1.10 compatibility.
- 1.8.3
- Elixir 1.11 compatibility.
- Fails with compilation error if spec can’t be compiled
- 1.9.0
- Erlang 25 and Elixir 1.13 compatibility.
- 1.9.1
- Simplify espec.init.
- 1.9.2
- OTP 26 and Elixir 1.16 compatibility.
- 1.10.0
- OTP 27 and Elixir 1.18 compatibility.
Contributing
Section titled “Contributing”Contributions are welcome and appreciated!
Section titled “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:
mix testmix espec