ExUnit is fine for small, simple applications, but when you want to do more complex test cases, it has limitations. ShouldI provides nested contexts to eliminate duplication in tests, and has better support for naming tests based on behavior. This API is based on the shoulda framework for Ruby on Rails.
Just add the hex dependency to your mix file:
defp deps do
[{:shouldi, only: :test}]
end
and add
...
use ShouldI
...
to your test script in place of
...
use ExUnit.Case
...
When you're testing behavior, you can get better names with a more descriptive macro. The test code...
test "should return ok on parse" do
assert :ok == Parser.parse
end
...can become more descriptive and shorter with...
should "return :ok on parse" do
assert :ok == Parser.parse
end
Say you have a test case that needs some setup. ExUnit has support for a context that can be set once, and passed to all clients. You can use the setup
method to pass a map to each of your test cases, like this:
defmodule MyFlatTest do
setup context do
{:ok, Dict.put context, :necessary_key, :necessary_value}
end
test( "this test needs :necessary_key", context ) do
assert context.necessary_key == :necessary_value
end
end
This approach breaks down when several, but not all, tests need the same set of values. ShouldI solves this problem with nested contexts, which you can provide with the having
keyword, like this:
defmodule MyFatTest do
having "necessary_key" do
setup context do
Dict.put context, :necessary_key, :necessary_value
end
should( "have necessary key", context ) do
assert context.necessary_key == :necessary_value
end
end
having "sometimes_necessary_key" do
setup context do
Dict.put context, :sometimes_necessary_key, :sometimes_necessary_value
end
should( "have necessary key", context ) do
assert context.sometimes_necessary_key == :sometimes_necessary_value
end
end
end
This approach is much nicer than the alternatives when you're testing something like a controller with dramatically different requirements across tests:
having "a logged in user" do
setup context do
login context, user
end
...
end
having "a logged out user" do
...
end
having "a logged in admin" do
setup context do
login context, admin
end
...
end
assign
is a macro that is syntactic sugar for updating the context
.
setup context do
Dict.put context, :necessary_key, :necessary_value
end
becomes
setup context do
assign context, necessary_key: :necessary_value
end
You can package macros that write your own tests. Matchers encode common assertion patterns. For example, our plug matchers
having "a logged in admin" do
setup context do
login context, admin
end
having "a get to :index" do
setup context do
# process get
end
should_respond_with :success
should_match_body_to "<html>"
end
end
The two matchers, should_respond_with
and should_match_body_to
, will run in a single test, against the context created in the setup
function (or setup functions, if you've used multiple contexts). Even if both of these tests fail, you'll see two failures in your output.
We come prepackaged with a set of matchers, but you can code your own as well. The following is the matcher to check for existence of a dictionary key in the context:
defmatcher should_assign_key([{key, value}]) do
quote do
assert var!(context)[unquote(key)] == unquote(value)
end
end
This macro allows you to build a matcher macro.
We'll have more information about creating matchers later. In the mean time, you can read through the matchers we've created in the project. Matchers should be stateless, as all matchers within a having
clause will run to completion, unless there is an error
, even if a test fails.
- Context
should_assign_key key, value
: assert that the value forkey
in the context isvalue
should_match_key key, expected
: assert that the value forkey
in the context satisfies the pattern matchexpected
should_have_key key
: assert thatkey
exists in the contextshould_not_have_key key
: assert thatkey
does not exist in the context
- Plug
should_respond_with expected
: Assert that the value forcontext.connection.status
in the context matches a reasonable value for:success
,:redirect
,:bad_request
,:unauthorized
,:missing
or:error
should_match_body_to expected
: Assert that the value forcontext.resp_body
contains the textexpected
.
When running tests asynchronously it can be useful to have a way to generate IDs or names that will not conflict with other tests that run concurrently. uid()
will generate an ID unique for the current test and setup. If it is called again during the same test it will return the same ID. An additional string can be given uid("some string")
so multiple IDs can be generated during the same test.
The philosophy is that experiments go in setup
and measurements go into matchers. shouldi
will make sure that the context is passed between them cleanly so that things compose correctly.
When you run a shouldi
test, for each context:
- one
should
test is created, collecting all of the matchers in ahaving
clause. - one exunit
test
is created for eachshould
block - for each test
-
- all of the ancestor
setup
functions will fire, from outermost to innermost.
- all of the ancestor
-
- the test will fire
-
- if a test is a matcher test, all of the matchers will run to completion, even if there is a failure, stopping only on errors.
-
- if a test is a
should
block, the first failure will halt the test, as inExUnit
.
- if a test is a
Happy testing. Open an issue if there are any matchers you'd like to see. Feedback and pull requests are welcome. Send a pull request if you'd like to contribute.
Special thanks to ThoughtBot's shoulda, which formed the foundation for this approach.