How to test the update action for Phoenix's nested forms?

This is one of those cases where I can get my actual code to work, but can’t get the test for it working (at least not in any sort of elegant way). My form allows the parent object and its multiple child objects to be updated simultaneously. I set up my test by creating the parent and a pair of child objects, using a @create_attrs map, and then try to call the update action with an @update_attrs map, which updates both the parent and the two children.

  describe "update action" do
    setup [:create_parent_and_child_objects]

    test "updates parent and children and redirects", %{conn: conn, parent: parent} do
      update_conn = put(conn, Routes.parent_path(conn, :update, parent), parent: @update_attrs)
      assert redirected_to(update_conn) == Routes.parent_path(conn, :show, parent)
    
      conn = get conn, Routes.parent_path(conn, :show, parent.id)
      assert html_response(conn, 200) =~ "updated info"
    end
end

Initially my @update_attrs map looked like this:

@update_attrs %{
  "name" =>"Updated Parent Name",
  "kids" => [
    %{ "name" => "Updated Kid 1 Name" },
    %{ "name" => "Updated Kid 2 Name" }
  ]
}

However, this gives a RunTimeError that states (among other things):

     However, if you don't want to allow data to be replaced or
     deleted, only updated, make sure that:
     
       * If you are attempting to update an existing entry, you
         are including the entry primary key (ID) in the data.

Fair enough, I’d be happy to add the IDs to the data. But since the parent and child are being created dynamically on each test run, the IDs can’t be hard coded.

To make this test work I have to suck the child objects’ IDs out of the parent object with Enum.map and then build up the update_attrs map myself. This works, but it seems unusually complicated. Am I missing a simpler way to do this?

:wave:

I’d probably cheat and fetch the kids’ ids from the actual parent record:

describe "update action" do
    setup [:create_parent_and_child_objects]

    test "updates parent and children and redirects", %{conn: conn, parent: parent} do
+     new_kids = Enum.map(parent.kids, fn kid -> %{"id" => kid.id, "name" => "Updated " <> kid.name} end)
-     update_conn = put(conn, Routes.parent_path(conn, :update, parent), parent: @update_attrs)
+     update_conn = put(conn, Routes.parent_path(conn, :update, parent), parent: Map.put(@update_attrs, "kids", new_kids))
      assert redirected_to(update_conn) == Routes.parent_path(conn, :show, parent)
    
      conn = get conn, Routes.parent_path(conn, :show, parent.id)
      assert html_response(conn, 200) =~ "updated info"
    end
end

or maybe use something like https://github.com/boydm/phoenix_integration.

3 Likes