Excoveralls: Testing and Test Coverage

DANA 325 T Q P M S R N F G L C A U W K D I J E B X O V H

Objectives

  • lists planets or planet names either sorted by name or default order (1 Point)

  • lists planet by id if n is a valid id, it show information on that planet (1 Point)

  • should randomly select a planet and show its data (1 Point)

  • You managed to display a TestCoverage report of your project. (1 Point)

  • TestCoverage report should indicate 100% test coverage for the PlanetController (3 Points)

Let's start by creating a new Elixir module with the following content. If you have watched the videos, you should know what the filename and location should be within the project directory:

defmodule App.Planets do
  @moduledoc """
  A context to retrieve data of our solar system.

  - `distance` is the relative distance to the Sun compared to Earth's distance to the sun.
  - `orbital_period` is the time it takes for a planet to orbit the sun (in Earth years)
  """

  @planets [
    %{id: 1, name: "Mercury", distance: 0.39, orbital_period: 0.24},
    %{id: 2, name: "Venus", distance: 0.72, orbital_period: 0.61},
    %{id: 3, name: "Earth", distance: 1.0, orbital_period: 1.0},
    %{id: 4, name: "Mars", distance: 1.52, orbital_period: 1.88},
    %{id: 5, name: "Jupiter", distance: 5.02, orbital_period: 11.86},
    %{id: 6, name: "Saturn", distance: 9.54, orbital_period: 29.46},
    %{id: 7, name: "Uranus", distance: 19.18, orbital_period: 84.01},
    %{id: 8, name: "Neptune", distance: 30.06, orbital_period: 164.8}
  ]

  def list, do: @planets

  def list(:sorted_by_name) do
    Enum.sort_by(@planets, fn planet -> planet.name end)
  end

  @doc """
  Returns a string containing all planet names, sorted alphabetically in ascending order, separated by semicolon.

  ## Examples
      Planets.names()
      "Earth, Jupiter, Mars, ..."
  """
  def names do
    :sorted_by_name
    |> list
    |> Enum.map(& &1.name)
    |> Enum.join(", ")
  end

  @doc """
  Returns a planet by id.

  ## Examples
      Planets.get(1)
      %{id: 1, name: "Mercury", distance: 0.39, orbital_period: 0.24}
  """
  def get(id), do: Enum.find(@planets, &(&1.id == id))
end

Next look up every Enum function you can find in that file in the documentation. Have a lenghty discussion with your partner on what they do and inspect the examples in the documentation. Ask your professor or AI if you need more clarification. It is very important that we get familar with those Enum functions as soon as possible and also remember them so we can connect the dots and use them when we need to.

Notice also that some function require a function as a parameter:

Enum.sort_by(@planets, fn planet -> planet.name end)

When that happens the function usually iterates through each element in a collection, in this case we iterate through planets and make planet.name the sort key. We could have added an optional parameter to change sorting to descending :desc but we left it at the default :asc.

Between fn and -> we find the parameters of the anonymous function. The function is anonymous because it doesn't have a name. Notice also that we used fn instead of def to further make that clear. The function body rests between -> and end. In Elixir we use functions as paramters a lot so there is short form for that:

Enum.sort_by(@planets, & &1.name)

This is the same function call as before. & indicates a function-parameter will be used and &1 represent the first parameter of that function, which was the planet. &1.name thus means we are retrieving the name of the current planet. &2, &3 and so on do not exist in this function so attempting to use them will result in an exception.

There can be only one & (at the very beginning) and what follows (&1.planet) is always the function body.

If at this point you feel confused, rest assured, I totally get it. With time you will learn to appreciate the patterns. 3-4 years you have learned about object-oriented langugages and functional programming can be a learning curve.

Objectives (1-3)

So now that we have done the legwork its time to discuss what to deliver for next lecture. Your mission, should you choose to accept it, is to create a new controller PlanetController yourself that will handle three different scenarios:

  • /planets should show a list of planets or planet names similar to the previous exercise.
  • /planets/random should show data of a random planet
  • /planets/:n should show data of the planet with the given id (such as 1) if an matching planet exists.

Here is how you could start your Controller:

defmodule AppWeb.PlanetController do
  use AppWeb, :controller

  alias App.Planets

  # TODO
end
  • Don't forget to add routes to the router.
  • Don't forget to create a view planet_html.ex similar to the page_html
  • Don't forget to add template files. You might need two of them such as: list.html.heex (for showing all planets) and show.html.heex (for showing a single entry)

Regarding the /planets/random part the random function might be what you are looking for.

This should yield you the first three points.

Objectives 4 and 5

The videos were about testing so this is the time we want to put that to practice. Once your functions work as expected (or even if not) you can work on your tests.

Start by installing excoveralls like I showed in the video and open the coveralls.html page under /cover once you have ran MIX_ENV=test mix coveralls for the first time.

I highly recommend you to bookmark that html file in your browser. You are going to open this file a lot in the future. If you see a report great. 1 point. That was easy.

Last we want to write a few tests in a similar fashion as seen in the videos with the ultimate objective to achieve 100% test coverage on our PlanetController. Ignore all other untested files.

Think about how you could test the randomness of the /planet/random` url. Some ideas:

  test "testing /planets/random", %{conn: conn} do
    # Set a seed so we always get the same "random" numbers when testing
    :rand.seed(:exsss, {100, 101, 102})

    conn = get(conn, ~p"/planets/random")
    # assert for as specific planet in your output (the one determined by seed)

    # call conn = get(...) again
    # assert for a different planet in your output
  end

Generally testing randomness is tricky and usually requires you to set a seed. The reason I have added this part of the excersise is, to show you how it can be done.

Copyright © 2025 Alexander Fuchsberger, Bucknell University. All rights reserved.