{∅}
Tags: phoenix, elixir, pharchive, testing

Goals:

Up until now, I have not touched upon testing of Pharchive, today that changes! We will shore up Pharchive with tests, and be ready to to build new features using TDD.

An aside on TDD

TDD has never truely resonated with me. I know that there are many many arguments as to how writing tests first can help guide the design of your application; and I think that those arguments are correct! For me, it has always been a balance between throwing code at something to see if an idea is viable(as quickly as possible), then using TDD to guide refactoring. In the case of Pharchive, because so much of the current functionality is provided by Phoenix generated code, I decided that my viable stopping point was “Can I create collections and their associated parent records in sane way”.

This is 100% a personal preference, and I can say freely that I am probably wrong in that I don’t “TDD all the things all the time”.

Requirements

Content

Since we have ignored testing up to this point, this is a great point to take a step back and ensure that all of our auto generated tests are passing.

Starting with the page_controller we change the test string to reflect the changes to our index (we moved from the default index to using CollectionController#index).

file: test/controllers/page_controller_test.exs

Our next error comes from the use of Integer.parse. There exist cases where params["film_id"] wont exist so we are trying to parse a nil value, this of course does not work. I decided to fix this in the bluntest way possible, guarding with a conditional, because I know we are going to have to do this same process in our other two controllers, and this is about the time that I start looking for shared behavior and think about extraction. Notice that we move our previous conditional into our else clause. This is not ideal, but lets keep running with it until all our tests are passing.

file: web/controllers/collection_controller.ex

The next error is a little esoteric. We have a failing truthy assertion. I figured that this is likely because we looking up a single record in too specific a manner. The autogenerated code says: "give me the collection whos attributes look exactly like the @valid_attrs map. Loosening this constraint to ensure that we are doing a lookup only based on the physical_id solves this quickly.

file: test/controllers/collection_controller_test.exs

Next we fix the integer parse error in our FilmController in the same way as we just did in the CollectionController. Hey, this is starting to get repetitive!

file: web/controllers/film_controller.ex

Next we need to make sure that the :collection_id is always present in a form for Film. So I decided to put a conn = assign(conn, :collection_id, "") in the three places that need to have it assigned: the case of validation errors on create, the case of editing a film, and the case of validation errors on update.

file: web/controllers/film_controller.ex

Next we have to ensure that the :collection_id is available in the film edit form. Somehow I missed this last time!

file: web/templates/film/edit.html.eex

Now we can move on to the ManufacturerController which is a bit simpler. We only need to ensure that film_id is always present. We have the same cases as in the FilmController

film: web/controllers/manufacturer_controller.ex

Now we make sure that :film_id is available in the edit form.

film: web/templates/manufacturer/edit.html.eex

As a small refactor, I would like to see get parameter names be homogeneous. Let’s change the get parameter on the “film created successfully” branch to be called film_id rather than film.

file: web/controllers/film_controller.ex

Lets apply the same change to the CollectionController as in the Manufacturer redirect.

file: web/controllers/collection_controller.ex

It seems rather silly that we have all of these assigns everywhere doesnt it? Lets use a controller level plug and set a default where we need to. In lovely fashion, Map#get takes an optional third parameter which acts as a default! This means that in the case where film_id is present its value is preserved, and in the case where film_id is not present, it is initialized to a default. Im going to display the full patch diff here because I think its easier to follow. In a quick pre summary, we add a plug to handle the assignment, then we remove all of the conn = assign(conn, :film_id, film_id) that we added in our last step.

file: web/controllers/manufacturer_controller.ex

defmodule Pharchive.ManufacturerController do
   alias Pharchive.Film
 
   plug :scrub_params, "manufacturer" when action in [:create, :update]
+  plug :default_creation_connection_variables when action in [:new, :edit, :create, :update]
+
+  def default_creation_connection_variables(conn, _) do
+    assign(conn, :film_id, Map.get(conn.params, "film_id", ""))
+  end
 
   def index(conn, _params) do
     manufacturers = Repo.all(Manufacturer)
     render(conn, "index.html", manufacturers: manufacturers)
   end
 
-  def new(conn, %{"film" => film_id}) do
-    changeset = Manufacturer.changeset(%Manufacturer{})
-    conn = assign(conn, :film_id, film_id)
-    render(conn, "new.html", changeset: changeset)
-  end
-
   def new(conn, _params) do
     changeset = Manufacturer.changeset(%Manufacturer{})
-    conn = assign(conn, :film_id, "")
     render(conn, "new.html", changeset: changeset)
   end
 
@@ -37,7 +35,6 @@ defmodule Pharchive.ManufacturerController do
         |> put_flash(:info, "Manufacturer created successfully.")
         |> redirect(to: manufacturer_path(conn, :index))
       {:error, changeset} ->
-        conn = assign(conn, :film_id, "")
         render(conn, "new.html", changeset: changeset)
     end
   end
@@ -49,7 +46,6 @@ defmodule Pharchive.ManufacturerController do
 
   def edit(conn, %{"id" => id}) do
     manufacturer = Repo.get!(Manufacturer, id)
-    conn = assign(conn, :film_id, "")
     changeset = Manufacturer.changeset(manufacturer)
     render(conn, "edit.html", manufacturer: manufacturer, changeset: changeset)
   end
@@ -64,7 +60,6 @@ defmodule Pharchive.ManufacturerController do
         |> put_flash(:info, "Manufacturer updated successfully.")
         |> redirect(to: manufacturer_path(conn, :show, manufacturer))
       {:error, changeset} ->
-        conn = assign(conn, :film_id, "")
         render(conn, "edit.html", manufacturer: manufacturer, changeset: changeset)
     end
   end

Since the plug strategy worked so well for the ManufacturerController, lets do the same thing to the FilmController.

file: /web/controllers/film_controller.ex

defmodule Pharchive.FilmController do
 
   plug :scrub_params, "film" when action in [:create, :update]
   plug :load_manufacturers when action in [:new, :create, :edit, :update]
+  plug :default_creation_connection_variables when action in [:new, :edit, :create, :update]
+
+  def default_creation_connection_variables(conn, _) do
+    assign(conn, :collection_id, Map.get(conn.params, "collection_id", ""))
+  end
 
   def load_manufacturers(conn, _) do
     manufacturers = Repo.all from(m in Pharchive.Manufacturer,
@@ -26,13 +31,11 @@ defmodule Pharchive.FilmController do
 
   def new(conn, %{"collection" => collection_id}) do
     changeset = Film.changeset(%Film{})
-    conn = assign(conn, :collection_id, collection_id)
     render(conn, "new.html", changeset: changeset)
   end
 
   def new(conn, _params) do
     changeset = Film.changeset(%Film{})
-    conn = assign(conn, :collection_id, "")
     render(conn, "new.html", changeset: changeset)
   end
 
@@ -69,7 +72,6 @@ defmodule Pharchive.FilmController do
           |> redirect(to: film_path(conn, :index))
         end
       {:error, changeset} ->
-        conn = assign(conn, :collection_id, "")
         render(conn, "new.html", changeset: changeset)
     end
   end
@@ -81,7 +83,6 @@ defmodule Pharchive.FilmController do
 
   def edit(conn, %{"id" => id}) do
     film = Repo.get!(Film, id)
-    conn = assign(conn, :collection_id, "")
     changeset = Film.changeset(film)
     render(conn, "edit.html", film: film, changeset: changeset)
   end
@@ -96,7 +97,6 @@ defmodule Pharchive.FilmController do
         |> put_flash(:info, "Film updated successfully.")
         |> redirect(to: film_path(conn, :show, film))
       {:error, changeset} ->
-        conn = assign(conn, :collection_id, "")
         render(conn, "edit.html", film: film, changeset: changeset)
     end
   end

The test suite is giving me some flak for unsafe variables because we are defining film_id and changeset within conditional statements (thanks Elixir, good catch!) in all three controllers. So we should rewrite these

file: web/controllers/collection_controller.ex

file: web/controllers/film_controller.ex

Dict was recently depricated with Elixir 1.2, so we should stop using it (sorry about the churn!). We will just replace occurences of Dict with Map.

I was not pleased with the creation logic related to our “create parent” flag. This is a change which makes things easier to follow, and avoids uneccessary integer parsing. I remove a level of conditionals by setting an initial value, and changing that value if certain conditions are met.

file: web/controllers/collection_controller.ex

Since our refactor went so well for collections, we should apply it to the FilmController

film: web/controllers/film_controller.ex

And then finally the CollectionController

file: web/controllers/collection_controller.ex

Now we have fixed our failing tests and refactored some of our code to be better! Now lets use ExMachina to setup some model factories to make testing easier. Model factories are very useful for testing basic suppositions of model logic, and DRYing up repetitive tests. Following the directions from the ExMachina README, we add the :ex_machina application to our application list in mix.es as well as adding it as a dependency.

file: mix.exs

We also need to update our test helper to make sure that ExMachina will run on test.

file: test/test_helper.exs

After a quick mix deps.get, we can move on to declaring our model factories. The nice thing about ExMachina is that it supports relations and generated attributes (sequences of unique emails for instance)

file: test/support/factory.ex

Now that the factories are declared, we are going to add a test to the CollectionController for the case of “creates Collection and associates Film when asked, redirects when data is valid”.

File: test/controllers/collection_controller_test.exs

We will add a similar test for the FilmController which tests two cases:

file: test/controllers/film_controller_test.exs

Finally we will add a test case for the ManufacturerController for “creates manufacturer and associates film when asked, redirects when data is valid”

file: test/controllers/manufacturer_controller_test.exs

Conclusion

This has been a pretty gnarly step with an unfortunate amount of churn. Since this is an application that I am actually using day to day; I am glad to find issues like this early rather than later! I have tagged the code related to this post here

Next time we will be adding a user model and authentication!