ex_united
ex_united copied to clipboard
Easily spawn Elixir nodes (supervising, Mix configured, easy asserted / refuted) within ExUnit tests
ExUnited 
Easily spawn Elixir nodes (supervising, Mix configured, easy asserted / refuted) within ExUnit tests
Introduction
ExUnited
is a Hex package designed to easily facilitate spawning supervising
local Elixir nodes within tests. Unfortunately, I was not able to properly setup
a spawned node for supervisioning with the Erlang :slave.start_link/1
function.
So I have written ExUnited
to accomplish that, along with supporting Mix.Config
configurations, additional loaded code, and a developer friendly way of writing
assertions and refutations in the context of a spawned node which really improved
the readability of the tests and more.
Features
- Spawn nodes for testing purposes
- Spin up "partially connected" vs "fully connected" nodes
- Run in "verbose" mode which prints a colorized STDOUT of the nodes
- Specify extra "code paths" which will be included (
config.exs
included) - Support child supervisioning within a spawned node
- Exclude certain dependencies for spawned nodes
- Easily(!) assert and refute within the context of spawned nodes
Enjoy the package! I would love to receive a shoutout and/or your feedback ;)
Installation
To install ExUnited, please add ex_united
to your list of dependencies in
mix.exs
:
def deps do
[
{:ex_united, "~> 0.1.5", only: :test}
]
end
Replace the default ExUnit.start()
invocation in the test helper file with
ExUnited.start()
:
# test/test_helper.exs
ExUnited.start()
Explicitly start ExUnit yourself
As of version 0.1.2
, you can also start ExUnit
yourself explicitly and add
ExUnited.start(false)
instead:
# test/test_helper.exs
ExUnit.start()
ExUnited.start(false)
ATTENTION: When also using meck-based packages
The following errors can occur when also using packages like
mock or MecksUnit
(which both use the Erlang library meck to
mock functions) and spawning the nodes with the default environment test
:
-
(UndefinedFunctionError) function Some.Module.some_function/1 is undefined
-
(ErlangError) Erlang error: {{:undefined_module, < Some.Module >}
To tackle this, you should configure any other (Mix) environment to spawn the nodes with. Configure it like so:
# config/test.exs
import Config
config :ex_united,
mix_env: :dev
You might also want to consider using a bogus environment (e.g. :void
) to skip
the non-relevant :dev
dependencies, like credo
or dialyxir
probably. That
will save some compile time.
And last but not least, please note that using a different environment within CI builds will require compiling the project in that particular environment on beforehand of the tests. Otherwise spawning the nodes will take too much time and that will cause timeout errors during the tests.
# .gitlab-ci.yml
before_script:
...
- MIX_ENV=void mix deps.get
- MIX_ENV=void mix run -e 'IO.puts("Done.")'
- epmd -daemon
script:
- mix test
Usage
For using ExUnited
, the two essential functions are:
-
ExUnited.spawn/2
- Spawns (Mix.Config
configured, additional code loaded, supervising) nodes -
ExUnited.teardown/0
- Kills the spawned nodes and it also cleans up their generated files
The most simplest setup
Nodes can be specified as a list of atoms, just like in the following example.
Their node names will be :"[email protected]"
and :"[email protected]"
respectively).
Please do not forget to invoke ExUnited.teardown/0
at the on_exit
hook.
setup do
{:ok, spawned} = ExUnited.spawn([:bruce, :clark])
on_exit(fn ->
ExUnited.teardown()
end)
spawned
end
"Partially versus Fully connected" and/or "Verbose" spawned nodes
As a second argument, you can pass a list of atoms for the options:
-
:connect
- iftrue
a "fully connected" node will be spawned (see theerl -connect_all
flag for more information). Defaults tofalse
-
:verbose
- iftrue
the STDOUT of the spawned node will be printed. Defaults tofalse
See ExUnited.spawn/2
for more information.
setup do
{:ok, spawned} = ExUnited.spawn([:roy], [:connect, :verbose])
on_exit(fn ->
ExUnited.teardown()
end)
spawned
end
Which results in the following when running tests:
PME-Legend ~/S/ex_united:master> mix test test/ex_united/supervised_test.exs:140
Excluding tags: [:test]
Including tags: [line: "140"]
iex([email protected])> Compiling 1 file (.ex)
iex([email protected])> Generated void app
iex([email protected])> Interactive Elixir (1.10.1) - press Ctrl+C to exit (type h() ENTER for help)
iex([email protected])1>
.
Finished in 0.9 seconds
2 tests, 0 failures, 1 excluded
Exclude certain dependencies for all spawned nodes
You can exclude certain (Mix) dependencies for ALL spawned nodes by for instance
adding exclude: [:inch_ex]
to the options. This can significantly improve
the speed of your tests.
setup do
{:ok, spawned} = ExUnited.spawn([:bruce, :clark], [:verbose, exclude: [:inch_ex]])
on_exit(fn ->
ExUnited.teardown()
end)
spawned
end
The following dependencies are excluded by default:
-
:credo
-
:dialyxir
-
:ex_doc
-
:ex_united
-
:excoveralls
Configuring the spawned nodes
Aside from the list of atoms, you can also specify nodes as a keyword list in case you want to configure them. The following options are available:
-
:code_paths
- a list of directories that will be included -
:exclude
- a list of dependencies that will be excluded -
:supervise
- the child spec(s) used for supervisioning
Including additional code
It would be a best practice to create a directory called test/nodes
in which
you put a directory containing code for a specific spawned node. Please note that
the file called config.exs
is supported for Mix.Config
:
setup do
{:ok, spawned} =
ExUnited.spawn(
eric: [
code_paths: [
"test/nodes/cantona"
]
]
)
on_exit(fn ->
ExUnited.teardown()
end)
spawned
end
See test/ex_united/supervised_test.exs with its corresponding test/nodes/ronaldo as an example.
Exclude certain dependencies for a specific spawned node
Add the :exclude
list as follows:
setup do
{:ok, spawned} =
ExUnited.spawn(
bruce: [
code_paths: [
"test/nodes/bruce"
],
exclude: [
:my_unused_dependency,
:my_current_project
],
supervise: [MyAwesomeGenServer]
],
clark: [
code_paths: [
"test/nodes/clark"
],
supervise: [MyOtherAwesomeGenServer]
]
)
on_exit(fn ->
ExUnited.teardown()
end)
spawned
end
Add supervisioning
Childspecs should be the same argument as if you are adding them to your classic
<app>/application.ex
file:
setup do
{:ok, spawned} =
ExUnited.spawn(
bruce: [
code_paths: [
"test/nodes/bruce"
],
supervise: [MyAwesomeGenServer]
],
clark: [
code_paths: [
"test/nodes/clark"
],
supervise: [MyOtherAwesomeGenServer]
]
)
on_exit(fn ->
ExUnited.teardown()
end)
spawned
end
Pay attention that functions within childspecs should be quoted.
setup do
{:ok, spawned} =
ExUnited.spawn(
[
roy: [
code_paths: [
"test/nodes/keane"
],
supervise: [
{
Roy,
talk:
quote do
fn
1 -> "Hi, I am Roy Keane"
2 -> "I am keen as mustard"
3 -> "I like to be peachy keen"
end
end
}
]
]
],
[:verbose]
)
on_exit(fn ->
ExUnited.teardown()
end)
spawned
end
Easily assert and refute within the context of spawned nodes
To seemlessly execute assertions and refutations within spawned nodes, you can
setup your test module by either using ExUnited.Case
instead of ExUnit.Case
:
defmodule MyNodesTest do
use ExUnited.Case
end
Or by importing the ExUnited.Case
module:
defmodule MyNodesTest do
use ExUnit.Case
import ExUnited.Case
end
Writing assertions and refutations within the context of a certain spawned is
pretty straight forward with the use of the ExUnited.Case.as_node/2
function
as if you are writing your class assert
and/or refute
statements:
defmodule MyNodesTest do
use ExUnited.Case
setup do
{:ok, spawned} = ExUnited.spawn([:bruce, :clark])
on_exit(fn ->
ExUnited.teardown()
end)
spawned
end
test "assertions and refutations within node contexts", spawned do
bruce = get_in(spawned, [:bruce, :node])
as_node(bruce) do
assert :"[email protected]" = Node.self()
refute :"[email protected]" == Node.self()
end
as_node(:clark) do
assert :"[email protected]" = Node.self()
refute :"[email protected]" == Node.self()
end
end
end
See ExUnited.Case.as_node/2
for more information.
Contact me
For support, remarks and requests, please mail me at [email protected].
License
Copyright (c) 2020 Paul Engel, released under the MIT License
http://github.com/archan937 – http://twitter.com/archan937 – [email protected]
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.