Updating controller test for JSON API

I’m a noob and am currently creating a little test project: a read-only API and CRUD admin pages. Somewhere along the line a controller test started failing, which I think is because I added attributes other than the ID of the model to the API JSON output.

The API controller test:

  test "shows chosen resource", %{conn: conn} do
    material = Repo.insert! %Material{}
    conn = get conn, api_material_path(conn, :show, material)
    assert json_response(conn, 200)["data"] == %{"id" => material.id}

The test failing:

  1) test shows chosen resource (AppName.Api.MaterialControllerTest)
     Assertion with == failed
     code: json_response(conn, 200)["data"] == {}
     lhs:  %{"id" => 28, "name" => nil, "price_sq_m" => nil}
     rhs:  {}
       test/controllers/api/material_controller_test.exs:21: (test)

The relevant view function:

  def render("material.json", %{material: material}) do
    |> Map.take([:id, :price_sq_m, :name])

If I change the test’s assert to compare against %{"id" => material.id, "name" => material.name, "price_sq_m" => material.price_sq_m} it passes.

I’ve not worked with tests before. Is updating the test in this manner a reasonable means of getting it to pass, or is there a better/easier/more robust way? Thanks.

1 Like


assert json_response(conn, 200)["data"]["id"] === material.id

or with Map.equal?/2 and Map.from_struct/1

result = json_response(conn, 200)["data"]
assert Map.equal?(result, Map.from_struct(material))


I’m on my phone, so I’m not sure if the latter method would work, but if it does then it would not need to be updated if you change the struct again. Of course it assumes that your fetching function creates a deeply equivalent map. Otherwise I think you’d always have to change the test to cherry pick which properties to test (which may be the best/more explicit route anyway).

1 Like

Many thanks. The latter does work at this stage.

I don’t fully understand how this is all being pulled together, but am I right in thinking that the maps are equal because the test controller uses the API controller’s show function (and, as a result, its view)? If so, would it actually be a better test to use the former of your two options, which does at least use data (the id) from the test’s own material = Repo.insert! %Material{}?

Edit: I think the above paragraph is flawed. Never mind, and thanks again. :slight_smile: Current code is below, but I think I’m sorted for now.

(By the way, I’ve since changed the root of the JSON output from ‘data’ to ‘material’.)

Test Controller:

  test "shows chosen resource", %{conn: conn} do
    material = Repo.insert! %Material{}
    conn = get conn, api_material_path(conn, :show, material)
    assert json_response(conn, 200)["material"]["id"] === material.id
    # or perhaps:
    # result = json_response(conn, 200)["material"]
    # assert Map.equal?(result, Map.from_struct(material))


  def show(conn, %{"id" => id}) do
    material = Repo.get!(Material, id)
    render(conn, "show.json", material: material)


defmodule AppName.Api.MaterialView do
  use AppName.Web, :view

  def render("index.json", %{materials: materials}) do
    %{materials: render_many(materials, AppName.Api.MaterialView, "material.json")}

  def render("show.json", %{material: material}) do
    %{material: render_one(material, AppName.Api.MaterialView, "material.json")}

  def render("material.json", %{material: material}) do
    %{id: material.id, name: material.name, price: Money.to_string(material.price_sq_m)}

Looks like you probably have a good grip on things! :smile:

You will build up more and more tests that cover more and more use cases as you go, so maybe you do just id for now, and later checking other values will be a matter of course.

I would say that maybe keeping "data" as the JSON key would be a good idea, since it is a common convention, as well as the JSON API spec’s primary container for the “data” of the object (as opposed to metadata and errors - which are just a specific kind of metadata).

1 Like

Bwahaha! :blush:

Thanks for the note on the JSON specs; I was wondering about that. Looks like technically I should have "type" as well.