Current section
Files
Jump to
Current section
Files
lib/json_api_assert.ex
defmodule JsonApiAssert do
import ExUnit.Assertions, only: [assert: 2, refute: 2]
@moduledoc """
JsonApiAssert is a collection of composable test helpers to ease
the pain of testing [JSON API](http://jsonapi.org) payloads.
You can use the functions individually but they are optimally used in a composable
fashion with the pipe operator:
payload
|> assert_data(user1)
|> assert_data(user2)
|> refute_data(user3)
|> assert_relationship(pet1, as: "pets", for: user1)
|> assert_relationship(pet2, as: "pets", for: user2)
|> assert_included(pet1)
|> assert_included(pet2)
If you've tested JSON API payloads before the benefits of this pattern should
be obvious. Hundreds of lines of codes can be reduced to just a handful. Brittle tests are
now flexible and don't care about inseration / render order.
"""
@doc """
Asserts that the "jsonapi" object exists in the payload
payload
|> assert_jsonapi(version: "1.0")
The members argument should be a key/value pair of members you expect to be be in
the "jsonapi" object of the payload.
"""
@spec assert_jsonapi(map, list) :: map
def assert_jsonapi(payload, members \\ [])
def assert_jsonapi(%{"jsonapi" => jsonapi} = payload, members) do
Enum.reduce(members, [], fn({key, value}, unmatched) ->
actual_value = jsonapi[Atom.to_string(key)]
if actual_value != value do
unmatched = unmatched ++ ["Expected:\n `#{key}` \"#{value}\"\nGot:\n `#{key}` \"#{actual_value}\""]
end
unmatched
end)
|> case do
[] -> payload
unmatched ->
raise ExUnit.AssertionError, "jsonapi object mismatch\n#{Enum.join(unmatched, ", ")}"
end
end
def assert_jsonapi(_payload, _members),
do: raise ExUnit.AssertionError, "jsonapi object not found"
@doc """
Asserts that a given record is included in the `data` object of the payload.
payload
|> assert_data(user1)
"""
@spec assert_data(map, map | struct) :: map
def assert_data(payload, record) do
assert_record(payload["data"], record)
payload
end
@doc """
Refutes that a given record is included in the `data` object of the payload.
payload
|> refute_data(user1)
"""
@spec refute_data(map, map | struct) :: map
def refute_data(payload, record) do
refute_record(payload["data"], record)
payload
end
@doc """
Asserts that a given record is included in the `included` object of the payload.
payload
|> assert_included(pet1)
"""
@spec assert_included(map, map | struct) :: map
def assert_included(payload, record) do
assert_record(payload["included"], record)
payload
end
@doc """
Refutes that a given record is included in the `included` object of the payload.
payload
|> refute_included(pet1)
"""
@spec refute_included(map, map | struct) :: map
def refute_included(payload, record) do
refute_record(payload["included"], record)
payload
end
@doc """
Asserts that the proper relationship meta data exists between a parent and child record
payload
|> assert_relationship(pet1, as: "pets", for: owner1)
The `as:` atom must be passed the name of the relationship. It will not be derived from the child.
"""
@spec assert_relationship(map, map, [as: binary, for: binary]) :: map
def assert_relationship(payload, child_record, [as: as, for: parent_record]) do
parent_record =
merge_data([], payload["data"])
|> merge_data(payload["included"])
|> assert_record(parent_record)
relationships = parent_record["relationships"]
if !relationships do
raise ExUnit.AssertionError, "could not find any relationships for record matching `id` #{parent_record["id"]} and `type` \"#{parent_record["type"]}\""
end
relationship = relationships[as]
if !relationship do
raise ExUnit.AssertionError, "could not find the relationship `#{as}` for record matching `id` #{parent_record["id"]} and `type` \"#{parent_record["type"]}\""
end
relationship =
get_in(parent_record, ["relationships", as, "data"])
|> List.wrap()
|> Enum.find(&(meta_data_compare(&1, child_record)))
assert relationship, "could not find relationship `#{as}` with `id` #{child_record["id"]} and `type` \"#{child_record["type"]}\" for record matching `id` #{parent_record["id"]} and `type` \"#{parent_record["type"]}\""
end
def assert_relationship(_, _, [for: _]),
do: raise ExUnit.AssertionError, "you must pass `as:` with the name of the relationship"
def assert_relationship(_, _, [as: _]),
do: raise ExUnit.AssertionError, "you must pass `for:` with the parent record"
@doc """
Refutes a relationship between two records
payload
|> refute_relationship(pet1, as: "pets", for: owner1)
The `as:` atom must be passed the name of the relationship. It will not be derived from the child.
"""
@spec refute_relationship(map, map, [as: binary, for: binary]) :: map
def refute_relationship(payload, child_record, [as: as, for: parent_record]) do
merge_data([], payload["data"])
|> merge_data(payload["included"])
|> assert_record(parent_record)
|> get_in(["relationships", as, "data"])
|> List.wrap()
|> Enum.find(&(meta_data_compare(&1, child_record)))
|> refute("was not expecting to find the relationship `#{as}` with `id` #{child_record["id"]} and `type` \"#{child_record["type"]}\" for record matching `id` #{parent_record["id"]} and `type` \"#{parent_record["type"]}\"")
end
def refute_relationship(_, _, [for: _]),
do: raise ExUnit.AssertionError, "you must pass `as:` with the name of the relationship"
def refute_relationship(_, _, [as: _]),
do: raise ExUnit.AssertionError, "you must pass `for:` with the parent record"
defp assert_record(data, record) do
find_record(data, record)
|> case do
nil ->
assert nil, "could not find a record with matching `id` #{record["id"]} and `type` \"#{record["type"]}\""
%{"attributes" => attributes} = found_record ->
Enum.reduce(attributes, [], fn({key, value}, attrs) ->
if value != record["attributes"][key] do
attrs ++ [key]
else
attrs
end
end)
|> case do
[] -> found_record
keys ->
opts = [
left: Enum.into(keys, %{}, fn(key) -> {key, attributes[key]} end),
right: Enum.into(keys, %{}, fn(key) -> {key, record["attributes"][key]} end),
message: "record with `id` #{record["id"]} and `type` \"#{record["type"]}\" was found but had mis-matching attributes"
]
raise(ExUnit.AssertionError, opts)
end
end
end
defp refute_record(data, record) do
find_record(data, record)
|> case do
%{"attributes" => attributes} ->
matching =
attributes
|> Enum.reduce([], fn({key, value}, attrs) ->
if value == record["attributes"][key] do
attrs ++ [{key, value}]
else
attrs
end
end)
refute Map.keys(attributes) |> length() == length(matching), "did not expect #{inspect Map.delete(record, "attributes")} to be found."
nil -> nil
end
end
defp find_record(data, record) do
data
|> List.wrap
|> Enum.find(&(meta_data_compare(&1, record)))
end
defp meta_data_compare(record_1, record_2) do
record_1["id"] == record_2["id"] && record_1["type"] == record_2["type"]
end
defp merge_data(payload, nil),
do: merge_data(payload, %{})
defp merge_data(payload, data) when is_list(data),
do: payload ++ data
defp merge_data(payload, data) when is_map(data),
do: merge_data(payload, [data])
end