De la documentation et des tests en Elixir

Entre Valorant et ma dernière mission freelance, ça fait un peu plus d’un mois que je n’ai pas touché à ce projet en Elixir dont je parlais dans un précédent article. Et ça se voit. Pourquoi ça se voit me direz-vous ? Eh bien, je n’ai rien documenté (bon j’ai des notes de travail mais c’est pas ouf) et surtout je n’ai rien testé. Ces quelques lignes de code que j’ai écrites pourraient commencer à ressembler à du code legacy si je n’y faisais pas attention.

Comme j’en ai un peu marre de dire projet je vais l’appeler par son nom : Stargazer. C’est une application web qui récupère mes stars sur GitHub pour les mettre dans MeiliSearch afin de pouvoir les rechercher plus tard. J’utilise beaucoup GitHub pour ma veille et l’idée de sortir mes données dans un outil dédié me plaît beaucoup. Je ne sais pas encore tout ce que je veux en faire mais j’y prends du plaisir et c’est sûrement le plus important :)

Alors histoire de me faciliter la reprise (et l'écriture de cet article), je vais détailler point par point ce que je fais pour dé-legacyfier Stargazer. Les exemples qui suivent sont liés aux modules de l’application. Pour la petite info, un module en Elixir est simplement un moyen d’organiser des fonctions. Pour en savoir plus sur les modules, lire cet article sur Elixir School.

Documentation

En Elixir, on utilise des attributs pour documenter notre code. Dans ce cas-ci j’utilise les attributs suivants :

  • @moduledoc pour documenter les modules
  • @doc pour documenter les fonctions
  • @typedoc pour documenter un type
  • @type pour définir un type
  • @spec pour typer les fonctions

Les trois derniers attributs de cette liste sont un peu troublants car Elixir est typé dynamiquement. Donc à quoi servent-ils ? C’est tout simple, c’est encore de la documentation pour préciser la signature des fonctions.

Voici ce que donne le module GitHubStarsCrawler complètement documenté :

defmodule Stargazer.GitHubStarsCrawler do
  @moduledoc """
  Stargazer.GitHubStarsCrawler role as its name suggests is to crawl repositories starred by
  the current user connected to the GitHub API.

  Current user is the user related to the personal access token used to connect to the GitHub API.
  """

  alias Stargazer.GitHubAPIWrapper, as: GitHub

  @typedoc """
  An integer representing a page number.
  """
  @type page :: integer

  @typedoc """
  A range of pages to loop over for retrieving all starred repositories.
  """
  @type page_range :: Range.t()

  @typedoc """
  A list of starred repositories.
  """
  @type stars :: list(star)

  @typedoc """
  A starred repository. For details on its content have a look on the [API documentation](https://developer.github.com/v3/activity/starring/#list-repositories-starred-by-a-user).
  """
  @type star :: map

  @doc """
  Return starred repositories for the given page number and pages range in a tuple.

  ## Example
  GitHubStarsCrawler.call()
  # => { stars, page_range }

  """
  @spec call(page) :: {stars, page_range}
  def call(page \\ 1) do
    {:ok, %HTTPoison.Response{body: stars, headers: page_range}} =
      GitHub.get("/user/starred?page=#{page}")

    {stars, page_range}
  end
end

Une fois que tous mes modules sont documentés, je peux générer la documentation au format HTML avec ExDoc, via la commande mix docs.

Tests

Écrire toute la documentation m’a permis de me rendre compte que mon code n'était pas si bien organisé que ça. Mais il est encore trop tôt avant de modifier quoi que ce soit. Eh oui, avant de faire du refactoring et toujours dans une optique de ne pas créer du code legacy, je vais écrire des tests. Et ce pour tous les modules en utilisant ExUnit un framework de tests unitaires pour Elixir.

Donc sans plus attendre, voici le fichier de test correspondant à mon module GitHubStarsCrawler:

defmodule Stargazer.GitHubStarsCrawlerTest do
  use ExUnit.Case
  import Mock
  alias Stargazer.{GitHubAPIWrapper, GitHubStarsCrawler}

  @headers [1..5]
  @body [
    %{
      "id" => 1,
      "name" => "cool-name",
      "description" => "cool-description",
      "html_url" => "https://github.com/cool-someone/cool-name"
    }
  ]

  test "call/1 returns stars and page_range" do
    with_mock GitHubAPIWrapper,
      get: fn _url ->
        {
          :ok,
          %HTTPoison.Response{
            headers: @headers,
            body: @body
          }
        }
      end do
      {stars, page_range} = GitHubStarsCrawler.call()

      assert page_range == @headers
      assert stars == @body
    end
  end
end

Dans ce test, je vérifie que GitHubStarsCrawler.call/1 retourne bien les attributs @headers et @body. Comme GitHubStarsCrawler utilise le module GitHubAPIWrapper pour interagir avec l’API de GitHub, j’ai choisi de mocker GitHubAPIWrapper. Je fais cela afin qu’aucune requête réseau ne soit effectuée et pour circonscrire mon test à GitHubStarsCrawler.call/1. La partie mock est réalisée avec Mock 🥁. Pour en savoir plus sur les mocks, je vous invite à lire mon dernier article : Travailler sur du code legacy.

Pour ce qui est de mon environnement de test, j’ai pour habitude d’utiliser mix_test_watch afin de lancer les tests automatiquement à chaque modification. Je configure des notifications avec ex_unit_notifier pour savoir si tout se passe bien sans polluer mon écran avec le terminal.

Voilà c’est tout pour aujourd’hui. Mon application est documentée et testée. Je peux maintenant poursuivre son développement tout en me laissant aller à faire du refactoring !

David Authier

David Authier
Développeur freelance