# assert_value for Elixir
[![Build Status](https://travis-ci.org/assert-value/assert_value_elixir.svg?branch=master)](https://travis-ci.org/assert-value/assert_value_elixir)
[![Hex Version](https://img.shields.io/hexpm/v/assert_value.svg)](https://hex.pm/packages/assert_value)
`assert_value` is ExUnit's `assert` on steroids. It writes and updates tests for you.
* `assert_value` allows you not to think about the correct expected values when writing
tests
* Gets rid of manual tests maintenance
* Makes Elixir tests interactive and lets you to create and update expected values
with a single key press
* Improves test readability
## Screencast
![Screencast](https://github.com/assert-value/assert_value_screencasts/raw/master/elixir-0.9.0/screencast.gif)
## Usage
```elixir
assert_value :foo
assert_value 2 + 2 == 4
assert_value "foo" == File.read!("test/log/foo.log")
```
You can use `assert_value` instead of ExUnit's `assert`. When writing a new test
you don't have to enter expected value. When you run it the first time `assert_value`
will generate it, show it to you, and will automatically update test source if
you accept it.
When you run an existing test and the actual value does not match expected,
`assert_value` will show the diff and ask you what to do. You then tell it
if the new actual value is correct. If it is, `assert_value` will update the test
source code with it. If not `assert_value` will fail the test just like builtin `assert`.
`assert_value` also lets you store expected value in a separate file using File.read!
This makes sense for longer values. `assert_value` will create and update file contents.
## Requirements
Elixir ~> 1.7
## Installation
Add this to mix.exs:
```elixir
defp deps do
[
{:assert_value, ">= 0.0.0", only: [:dev, :test]}
]
end
```
Add this to config/test.exs to avoid timeouts:
```elixir
# Avoid timeouts while waiting for user input in assert_value
config :ex_unit, timeout: :infinity
config :my_app, MyApp.Repo,
timeout: :infinity,
ownership_timeout: :infinity
```
Add this to .formatter.exs:
```elixir
[
# don't add parens around assert_value arguments
import_deps: [:assert_value],
# use this line length when updating expected value
line_length: 98 # whatever you prefer, default is 98
]
```
## HOWTO
Tests are code. They should be readable, maintainable, and reusable.
Tests should break when behaviour changes and should not break randomly.
We will look at how to do this in Elixir project.
### Traditional Way
Let's create a new Phoenix project:
```bash
mix phx.new example --no-brunch --no-ecto
```
This generates a controller test:
```elixir
defmodule ExampleWeb.PageControllerTest do
use ExampleWeb.ConnCase
test "GET /", %{conn: conn} do
conn = get conn, "/"
assert html_response(conn, 200) =~ "Welcome to Phoenix!"
end
end
```
Now we want to add tests for page content. Traditional way to to it looks
like this:
```elixir
# Use Floki to parse html
# mix.exs
defp deps do
[
{:floki, "~> 0.7", only: :test}
]
end
# test/example_web/controllers/page_controller_test.exs
defmodule ExampleWeb.PageControllerTest do
use ExampleWeb.ConnCase
test "GET /", %{conn: conn} do
conn = get conn, "/"
assert html_response(conn, 200)
body = html_response(conn, 200)
title = Floki.find(body, "title")
|> Floki.text
assert title == "Hello Example!"
header = Floki.find(body, "h2")
|> Floki.text
assert header == "Welcome to Phoenix!"
sections = Floki.find(body, "h4")
|> Enum.map(&Floki.text/1)
assert sections == ["Resources", "Help"]
end
end
```
Why is this bad?
* It is hard to read
* You have a lot of code duplication when testing multiple pages
* It makes it expensive to create new tests and even more expensive
to update them
* This will hurt tests coverage and will make it more expensive to
create new features or refactor code
Let's fix this.
### Serialization
We will write a serialization function that extracts
parts of the page and presents them in a human readable format:
```elixir
# Add assert_value to mix.exs
defp deps do
[
{:floki, "~> 0.7", only: :test},
{:assert_value, ">= 0.0.0", only: [:dev, :test]}
]
end
# test/example_web/controllers/page_controller_test.exs
defmodule ExampleWeb.PageControllerTest do
use ExampleWeb.ConnCase
import AssertValue
defp serialize_response(conn) do
%Plug.Conn{status: status, resp_body: body} = conn
"Status: #{status}" <>
"\nTitle: " <>
(body
|> Floki.find("title")
|> Floki.text) <>
"\n" <>
(body
|> Floki.find("h1,h2,h3,h4")
|> Enum.map(fn({tagName, _attrs, content}) ->
"#{tagName}: #{Floki.text(content)}"
end)
|> Enum.join("\n")) <>
"\n"
end
test "GET /", %{conn: conn} do
conn = get conn, "/"
assert_value serialize_response(conn)
end
end
```
Note, we don't need to specify expected value when writing the test.
`assert_value` will generate it, show it to us, and will automatically
update test source if we accept it.
```diff
~/> mix test
...
test/example_web/controllers/page_controller_test.exs:23:"test GET /" assert_value serialize_response(conn) failed
-
+Status: 200
+Title: Hello Example!
+h2: Welcome to Phoenix!
+h4: Resources
+h4: Help
Accept new value? [y,n,?] y
.
Finished in 1.5 seconds
4 tests, 0 failures
```
```elixir
test "GET /", %{conn: conn} do
conn = get conn, "/"
assert_value serialize_response(conn) == """
Status: 200
Title: Hello Example!
h2: Welcome to Phoenix!
h4: Resources
h4: Help
"""
end
```
And in the future when you update your page content all you need to do
is accept new diff to update the test.
### Reuse
The best part of using serializers is that you can reuse them.
Let's add one more page to our sample app:
```elixir
# lib/example_web/controllers/page_controller.ex
defmodule ExampleWeb.PageController do
use ExampleWeb, :controller
def index(conn, _params) do
render conn, "index.html"
end
def hello(conn, _params) do
render conn, "hello.html"
end
end
# lib/example_web/templates/page/hello.html.eex
<h2>Hello World</h2>
# Add route to lib/example_web/router.ex
scope "/", ExampleWeb do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
get "/hello", PageController, :hello
end
```
Now it is easy to add a new test
```elixir
# test/example_web/controllers/page_controller_test.exs
test "GET /hello", %{conn: conn} do
conn = get conn, "/hello"
assert_value serialize_response(conn)
end
```
And run tests
```diff
~> mix test
....
test/example_web/controllers/page_controller_test.exs:34:"test GET /hello" assert_value serialize_response(conn) failed
-
+Status: 200
+Title: Hello Example!
+h2: Hello World
Accept new value? [y,n,?] y
.
Finished in 1.9 seconds
5 tests, 0 failures
..
Finished in 8.8 sec
```
And your tests are automatically updated
```elixir
test "GET /hello", %{conn: conn} do
conn = get conn, "/hello"
assert_value serialize_response(conn) == """
Status: 200
Title: Hello Example!
h2: Hello World
"""
end
```
### Easy Refactoring
Now let's say you change the title in the layout.
```diff
diff --git a/lib/example_web/templates/layout/app.html.eex b/lib/example_web/templates/layout/app.html.eex
index f10cd22..b70a0ae 100644
--- a/lib/example_web/templates/layout/app.html.eex
+++ b/lib/example_web/templates/layout/app.html.eex
@@ -7,7 +7,7 @@
<meta name="description" content="">
<meta name="author" content="">
- <title>Hello Example!</title>
+ <title>My App Example!</title>
<link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
</head>
```
Without `assert_value` you would have had to manually update all tests.
With assert_value all you need is to accept new diffs:
```diff
~> mix test
...
test/example_web/controllers/page_controller_test.exs:23:"test GET /" assert_value serialize_response(conn) == "Status: 200... failed
Status: 200
-Title: Hello Example!
+Title: My App Example!
h2: Welcome to Phoenix!
h4: Resources
h4: Help
Accept new value [y/n/Y/N/d/?]? y
.
test/example_web/controllers/page_controller_test.exs:34:"test GET /hello" assert_value serialize_response(conn) == "Status: 200... failed
Status: 200
-Title: Hello Example!
+Title: My App Example!
h2: Hello World
Accept new value [y/n/Y/N/d/?]? y
.
Finished in 4.3 seconds
5 tests, 0 failures
```
Combining serialization and assert_value makes it easy to write and _maintain_
tests. Especially when your software is changing fast.
### Canonicalization
A common problem with testing serialized output that it may contain unpredictable
or changing data (like tokens, timestamps, ids, etc). The solution for this problem
is canonicalization.
Let's add timestamps to all our pages:
```diff
--- a/lib/example_web/templates/layout/app.html.eex
+++ b/lib/example_web/templates/layout/app.html.eex
@@ -28,6 +28,7 @@
<main role="main">
<%= render @view_module, @view_template, assigns %>
</main>
+ <h4>Created: <%= DateTime.utc_now |> to_string %></h4>
</div> <!-- /container -->
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>
```
No when you run `mix test` you will always get diffs
```diff
test/example_web/controllers/page_controller_test.exs:35:"test GET /hello" assert_value serialize_response(conn) == "Status: 200... failed
Status: 200
Title: My App Example!
h2: Hello World
-h4: Created: 2017-10-25 12:35:23.144635Z
+h4: Created: 2017-10-25 12:35:24.268836Z
Accept new value? [y,n,?] n
```
To fix this we will add canonicalization to the serializer
```elixir
defp serialize_response(conn) do
%Plug.Conn{status: status, resp_body: body} = conn
"Status: #{status}" <>
"\nTitle: " <>
(body
|> Floki.find("title")
|> Floki.text) <>
"\n" <>
(body
|> Floki.find("h1,h2,h3,h4")
|> Enum.map(fn({tagName, _attrs, content}) ->
"#{tagName}: #{Floki.text(content)}"
end)
|> Enum.join("\n")) <>
"\n"
|> canonicalize_response
end
defp canonicalize_response(text) do
text
|> String.replace(~r/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+Z/, "<TIMESTAMP>")
end
```
Then you just need to run `mix test` and accept the diffs
```diff
~/> mix test
...
test/example_web/controllers/page_controller_test.exs:30:"test GET /" assert_value serialize_response(conn) == "Status: 200... failed
Status: 200
Title: My App Example!
h2: Welcome to Phoenix!
h4: Resources
h4: Help
-h4: Created: 2017-10-25 12:35:25.268836Z
+h4: Created: <TIMESTAMP>
Accept new value? [y,n,?] y
.
test/example_web/controllers/page_controller_test.exs:41:"test GET /hello" assert_value serialize_response(conn) == "Status: 200... failed
Status: 200
Title: My App Example!
h2: Hello World
-h4: Created: 2017-10-25 12:35:25.936541Z
+h4: Created: <TIMESTAMP>
Accept new value? [y,n,?] y
.
Finished in 10.0 seconds
5 tests, 0 failure
```
## API
### No Expected Value
It is better to start with no expected value
```elixir
assert_value "foo"
```
When you run it the first time `assert_value` will generate it, show it to us,
and will automatically update test source if we accept it.
### Expected Value in the Source Code
```elixir
assert_value 2 + 2 == 4
```
`assert_value` will update expected values for you in the source code.
`assert_value` supports all Elixir types except not serializable (Function,
PID, Port, Reference).
When expected is a multi-line string `assert_value` will format it as a heredoc
for better code diff readability. Heredocs in Elixir always end with a newline
character. When expected value does not end with a newline `assert_value` will
append a special ```<NOEOL>``` string to indicate that last newline should be
ignored.
### Expected Value in a File
Sometimes test values are too large to be inlined into the test source.
Put them into the file instead.
```elixir
assert_value "foo" == File.read!("test/log/reference.txt")
```
assert_value is smart enough to recognize File.read! and will update file contents
instead of test source. If file does not exists it will be created and no error
will be raised despite default File.read! behaviour.
### Running Tests Interactively
assert_value will autodetect whether it is running interactively (in a
terminal), or non-interactively (e.g. continuous integration).
When running interactively it will ask about each diff.
You can accept or reject new value with ```y``` or ```n```. If you use ```Y```
and ```N``` (uppercase) assert_value will remember the answer and will not ask
again during this test run. ```?``` will show help with all available actions.
```
Accept new value? [y,n,?] ?
y - Accept new value as correct. Will update expected value. Test will pass
n - Reject new value. Test will fail
Y - Accept all. Will accept this and all following new values in this run
N - Reject all. Will reject this and all following new values in this run
d - Show diff between actual and expected values
? - This help
```
### Running Tests Non-interactively
When running non-interactively assert_value will reject all diffs by default, and will
work like default ExUnit's assert.
To override autodetection use ASSERT_VALUE_ACCEPT_DIFFS environment variable
with one of three values: "ask", "y", "n"
```
# Ask about each diff. Useful to force interactive behavior despite
# autodetection.
ASSERT_VALUE_ACCEPT_DIFFS=ask mix test
# Reject all diffs. Useful to force default non-interactive mode when running
# in an interactive terminal.
ASSERT_VALUE_ACCEPT_DIFFS=n mix test
# Accept all diffs. Useful to update all expected values after a refactoring.
ASSERT_VALUE_ACCEPT_DIFFS=y mix test
# Automatically reformat all expected values. Useful to reformat all tests
# when a new assert_value version improves formatter.
ASSERT_VALUE_ACCEPT_DIFFS=reformat mix test
```
## Notes and Known Issues
* assert_value supports all Elixir types except not serializable (Function,
PID, Port, Reference). To compare values of theese types use ```inspect```
and [Serialization](#serialization) techniques.
* assert_value's formatter is primitive and does not understand operator
precedence. When creating a new expected value from scratch it simply
appends "== <expected_value>" to the expression. This usually works but can
produce incorrect source code in unusual cases. For example
`assert_value foo = 1` will become `assert_value foo = 1 == 1` instead of
`assert_value (foo = 1) == 1`. To workaround this wrap actual expression
in parentheses. In practice you are unlikely to run into this problem.
* assert_value works only with the default ExUnit formatter (ExUnit.CLIFormatter).
Chances are that it is what you are using.
## Contributing
We appreciate any contribution to assert_value
To file a bug report create a [GitHub issue](https://github.com/assert-value/assert_value_elixir/issues).
To create a feature requests add a comment to the [Roadmap](https://github.com/assert-value/assert_value_elixir/issues/1)
To make a pull request:
* Fork https://github.com/assert-value/assert_value_elixir and clone your fork
* Create a new topic branch (off of master) to contain your feature, change, or fix.
`git checkout -b my-feature`
* Make sure to add tests for your code
* Run `mix test`. Make sure all the tests are still passing.
* Run `mix credo` to make sure your code is following our code guidelines.
* Commit your changes. Keep your commit messages organized, with a short description
in the first line and more detailed information on the following lines.
* Check TravisCI build on your repository
* Open a pull request
## License
This software is licensed under [the MIT license](LICENSE).