Testing the controllers

Let us resume our work now at the level of unit test. Note that at this level, we do not have direct interaction with the browser but rather with the set of functions that are implemented by the controller. This is the main difference between a unit test (where we focus our attention over one single component) versus integration/acceptance test (where we test several or the whole set of components of the application).

In the simplest form, we are going to test a controller with code that looks like the following one:

defmodule TacsiWeb.BookingControllerTest do
  use TacsiWeb.ConnCase

  test "POST /bookings", %{conn: conn} do
    conn = post conn, "/bookings", %{booking: [pickup_address: "Liivi 2", dropoff_address: "Lõunakeskus"]}
    assert html_response(conn, 200) =~ ~r/Your taxi will arrive in \d+ minutes/
  end
end

However, the code above will not necessarily work. The problem is the following. In our setting, we are still working with multi-page applications. That means that we expect that we will be redirected to another page after executing the action :create on the BookingController. If you take a look at the application that we implemented last time. We set the page flow so as to redirect the user to the index page after creating a resource. Another option would be to redirect to the show page. In this case, I have decided to redirect the flow to the index page.

Let us then fix the test. Replace the previous code with the following snippet.

defmodule TacsiWeb.BookingControllerTest do
  use TacsiWeb.ConnCase

  test "POST /bookings", %{conn: conn} do
    conn = post conn, "/bookings", %{booking: [pickup_address: "Liivi 2", dropoff_address: "Lõunakeskus"]}
    conn = get conn, redirected_to(conn)
    assert html_response(conn, 200) =~ ~r/Your taxi will arrive in \d+ minutes/
  end
end

Now, it is your turn. Complete the implementation of action :create on the moduleBookingController. Please note that, at this moment, the test does not imply any interaction with the database. Do not be eager. Wait until the moment where the requirement comes to light.

Let us go back to BDD

In this point, we just completed our first BDD/TDD cycle: we cannot continue changing the implementation unless we start guessing the requirements. That is why I will ask you to go back to the outer cycle, that is, the BDD cycle.

In fact, let me first show you that we completed the BDD/TDD cycle. Run White bread (use mix white_bread.run). You will see that all the steps run and, particularly, the assertion on the last step passes.

Let us now add the second scenario to the feature file. You can find the specification below:

    Scenario: Booking via STRS' web page (with rejection)
        Given the following taxis are on duty
            | username	| location	| status	|
            | juhan85	| Kaubamaja	| busy		|
            | peeter88	| Kaubamaja | busy  	|
        And I want to go from "Liivi 2" to "Lõunakeskus"
	    And I open STRS' web page
	    And I enter the booking information
	    When I summit the booking request
	    Then I should receive a rejection message

If you run White bread, you will see that there is one step still missing. Copy the skeleton of step provided by White bread as part of the feedback. Propose an assertion for this step.

Note that now we have one scenario in GREEN and the other one in RED. However, this time we have enough information to figure out what is happening. The current implementation returns the confirmation by default. How can we distinguish the cases where the system must return a confirmation instead of a rejection and vice versa? Easy: The booking request will be reject if there is no taxi available in the system. So now, we have enter this information into the equation. However, the changes are not necessarily minimal. Please replace the code in file features/contexts/white_bread_context.exs as required:

  alias Tacsi.{Repo,Sales.Taxi}
  
  feature_starting_state fn  ->
    Application.ensure_all_started(:hound)    
    %{}
  end
  scenario_starting_state fn state ->
    Hound.start_session
    Ecto.Adapters.SQL.Sandbox.checkout(Tacsi.Repo)
    Ecto.Adapters.SQL.Sandbox.mode(Tacsi.Repo, {:shared, self()})
    %{}
  end
  scenario_finalize fn _status, _state ->
    Ecto.Adapters.SQL.Sandbox.checkin(Tacsi.Repo)
    Hound.end_session
  end

  given_ ~r/^the following taxis are on duty$/, fn state, %{table_data: table} ->
    table
    |> Enum.map(fn taxi -> Taxi.changeset(%Taxi{}, taxi) end)
    |> Enum.each(fn changeset -> Repo.insert!(changeset) end)
    {:ok, state}
  end

Firstly, you will notice that we added two lines of code to function scenario_starting_state/1. The presence of SQL should give us a hint, we are configuring the access to the database. In a nutshell, we are configuring the database for testing purposes: the database starts empty when a scenario is started and it is cleaned up at the end of the scenario.

Secondly, you will see that we changed the implementation of the step given_: In the spec, such step is associated with an data table. White bread parses such data table and passes that table, in the form of a list of maps, into variable table (using pattern matching). Then, we iterated over the set of maps in the table in order to create a changeset and then to insert the information into the corresponding database table.

Well, this is the goal but we first need to create the Phoenix model for taxi. Create the model and migration with the command mix phx.gen.schema Sales.Taxi sales_taxis and copy the following snippets where they belong:

  def change do
    create table(:taxis) do
      add :username, :string
      add :location, :string
      add :status, :string  
      timestamps()
    end
  end
  schema "taxis" do
    field :username, :string
    field :location, :string
    field :status, :string
    timestamps()
  end
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:username, :location, :status])
  end

The above should complete the setup of code at BDD level. We need now to move to TDD.

defmodule Tacsi.BookingControllerTest do
  use Tacsi.ConnCase

  alias Tacsi.{Repo,Sales.Taxi}

  test "Booking rejection", %{conn: conn} do
    Repo.insert!(%Taxi{status: "busy"})
    conn = post conn, "/bookings", %{booking: [pickup_address: "Liivi 2", dropoff_address: "Lõunakeskus"]}
    conn = get conn, redirected_to(conn)
    assert html_response(conn, 200) =~ ~r/At present, there is no taxi available!/
  end

  test "Booking aceptance", %{conn: conn} do
    Repo.insert!(%Taxi{status: "available"})
    conn = post conn, "/bookings", %{booking: [pickup_address: "Liivi 2", dropoff_address: "Lõunakeskus"]}
    conn = get conn, redirected_to(conn)
    assert html_response(conn, 200) =~ ~r/Your taxi will arrive in \d+ minutes/
  end
end

Ecto queries

We have reached the point where we need to write down at least one query. I think you would need to query the database table for bookings, to count the number of available taxis. To that end, we will use Ecto which provides a DSL that resembles a lot SQL. For instance, a query like the one below:

    select * from taxis where status = "available;

can be rewritten in Ecto as follows:

    from t in Taxi, where: t.status == "available", select: t

The clauses are a bit reshuffled, but in essence they are the same. Well, the above is just a function call, which results in the definition of a query. I have to say that I simplified the call. The full version name of the function would be Ecto.Query.from/2, but I propose you to add import Ecto.Query, only: [from: 2] at the top of the module and get the simplified syntax.

Now, to execute the query we would need additionally to pass such query to Ecto.Repo. The following snippet summarizes the fragments to add to the controller if you want to add the query.

  # You should have the following calls somewhere on the top of the module
  import Ecto.Query, only: [from: 2]
  alias Tacsi.Sales.Taxi
  
  ... 
    query = from ...
    available_taxis = Repo.all(query)
  ...

Replace the code of the test cases with the fragment above. Use the test cases to guide you in completing the implementation of our BookingController.

Testing model validations

In this moment, we are not checking the validity of the booking information. For instance, we can submit a booking request without specifying the pickup address or without a drop-off address or even if both of them are missing.

Create the model for handling booking information. Create its corresponding migration/model skeletons and manually add the model fields.

The following snippet exemplifies the typical test for model validations.

    test "booking requires a 'pickup address'" do
        changeset = Booking.changeset(%Booking{}, %{pickup_address: nil, dropoff_address: "Liivi 2"})
        assert Keyword.has_key? changeset.errors, :pickup_address
    end

Please note that a validation function is just another function: one that takes as input the changeset, analyzes it and returns a boolean indicating whether the change included in a changeset complies with a given criterion or not.

Consider the blog available here and write a validation function that check whether the pickup and drop-off addresses are different or not. You will agree with me that it does not make sense to book a taxi for a trip that starts and ends in the same place.