Elixir: testes unitários e DocTest

Artigos - 10/Set/2020 - por André Kanamura

Nota: Neste artigo utilizamos versão do Elixir 1.10.2 (compilado com Erlang/OTP 21).

Elixir é uma linguagem de programação funcional que compila sobre Erlang, utilizada para desenvolver uma grande variedade de tipos de software. Inspirado em outras linguagens, seu criador, José Valim, incorporou ao Elixir algumas funcionalidades bastante úteis como um gerenciador de projetos, o Mix, um framework de testes, o ExUnit e um shell interativo, o IEx. Neste artigo, veremos brevemente como podemos iniciar um projeto simples e escrever testes desde o início utilizando essas ferramentas do Elixir. Vamos utilizar como exemplo um desafio de código clássico chamado Fizz Buzz, em que devemos iterar por uma sequência de números e fazer trocas dos valores divisíveis por 3 para "Fizz", os valores divisíveis por 5 para "Buzz" e os divisíveis por 3 e por 5 para "FizzBuzz". Para fazer isso, vamos começar pela criação do projeto.

O Mix é uma ferramenta que oferece mecanismos para criar, compilar e testar projetos Elixir. Tendo Elixir instalado no computador, para iniciar um novo projeto você pode usar com o comando:

$ mix new fizz_buzz

Esse comando cria um diretório com o nome do projeto, nesse caso fizz_buzz, e toda a estrutura de pastas e arquivos iniciais do seu projeto, entre eles, dentro do diretório lib um arquivo fizz_buzz.ex com módulo nomeado FizzBuzz e dentro do diretório test um arquivo fizz_buzz_test.exs, com o módulo FizzBuzzTest. Note que usamos o estilo de escrita em snake_case, usando underline no lugar de espaços e somente minúsculas, para nomear o projeto. O Mix entende esse padrão e cria os módulos com os nomes corretos em Pascal Case.

Como gosto de desenvolvimento guiado por testes, vamos começar implementando o código do nosso teste. O ExUnit é o framework nativo do Elixir que o Mix utiliza para os testes do projeto. Quando o projeto é iniciado, o Mix já faz a configuração para você poder fazer testes unitários. O Elixir também possui uma funcionalidade chamada DocTests, por meio dos quais podemos aproveitar trechos de código utilizados para documentar a aplicação e rodá-los no IEx. Primeiro, vamos mostrar como fazer os testes unitários com ExUnit, sem DocTests.

Testes unitários

O arquivo test/fizz_buzz_test.exs deve estar assim:

defmodule FizzBuzzTest do                                                        
  use ExUnit.Case                                                                
  doctest FizzBuzz                                                               

  test "greets the world" do                                                     
    assert FizzBuzz.hello() == :world                                            
  end                                                                            
end 

Dentro do módulo FizzBuzzTest vamos colocar os testes. Como não vamos usar DocTest agora, a linha doctest FizzBuzz pode ser apagada. Cada cenário de teste que vamos avaliar ficará definido dentro do bloco test. É uma boa prática incluir uma descrição do que está sendo verificado, assim usamos o describe para especificá-la. Neste caso, queremos testar se a função convert que vamos criar recebe um número inteiro e converte os valores de 1 até o valor desse número para "Fizz", "Buzz" ou "FizzBuzz" corretamente. A estrutura geral do arquivo deve ficar mais ou menos assim:

defmodule FizzBuzzTest do                                                        
  use ExUnit.Case

  describe "convert/1" do                                                                    
    test "números divisíveis por 3 trocar por Fizz, até o valor 3" do                                                     
      assert FizzBuzz.convert(3) == [1, 2, "Fizz"]                                           
    end
  end                                                                       
end 

No describe incluímos o nome da função que queremos testar (convert), seguido da quantidade de argumentos que ele recebe. Esse primeiro cenário testa somente os valores de 1 a 3, com a chamada de função FizzBuzz.convert(3) e verifica se o retorno é igual a[1, 2, "Code"]. Na linha que começa comtest`, procuramos descrever esse contexto de forma clara e objetiva. Talvez nosso exemplo não seja o melhor, mas dentro da sua aplicação provavelmente será mais fácil encontrar descrições adequadas.

Depois de escrito o primeiro teste, podemos pedir para o Mix executá-lo com o comando:

$ mix test

O terminal deve apresentar o resultado desse comando, que deve gerar uma mensagem assim:

Compiling 1 file (.ex)

  1) test convert/1 números divisíveis por 3 trocar por Code, até o valor 3 (FizzBuzzTest)
     test/fizz_buzz_test.exs:5
     ** (UndefinedFunctionError) function FizzBuzz.convert/1 is undefined or private
     code: assert FizzBuzz.convert(3) == [1, 2, "Code"]
     stacktrace:
       (fizz_buzz 0.1.0) FizzBuzz.convert(3)
       test/fizz_buzz_test.exs:6: (test)

Finished in 0.1 seconds
1 test, 1 failure

A mensagem de erro apresentada ** (UndefinedFunctionError) function FizzBuzz.convert/1 is undefined or private nos avisa que a função ainda não foi criada ou é privada, afinal, ainda não começamos sua implementação.

Ampliando essa mesma estrutura de teste, você pode incluir outros cenários e testes para outras funções, lembrando da organização e separando em diferentes blocos de describe para cada nova função.

Vamos incluir mais dois testes ao nosso exemplo:

defmodule FizzBuzzTest do                                                        
  use ExUnit.Case

  describe "convert/1" do
    test "números divisíveis por 3 trocar por Code até o valor 3" do
      assert FizzBuzz.convert(3) == [1, 2, "Fizz"]
    end

    test "números divisíveis por 5 trocar por Saga até o valor 5" do
      assert FizzBuzz.convert(5) == [1, 2, "Fizz", 4, "Buzz"]
    end

    test "trocar por Code ou Saga até o valor 10" do
      assert FizzBuzz.convert(10) == [1, 2, "Fizz", 4, "Buzz", "Fizz",
                                      7, 8, "Fizz", "Buzz"]
    end
  end                                            
end

Agora, você pode implementar o código da função no módulo FizzBuzz e rodar novamente o teste para vê-lo passar. Nosso exemplo ficou assim:

defmodule FizzBuzz do
  def convert(value) do
    1..value |> Enum.map(&evaluate_number/1)
  end

  defp evaluate_number(value) when rem(value, 15) == 0, do: "FizzBuzz"
  defp evaluate_number(value) when rem(value, 5) == 0, do: "Buzz"
  defp evaluate_number(value) when rem(value, 3) == 0, do: "Fizz"
  defp evaluate_number(value), do: value
end

Se você rodar os testes agora, todos eles devem estar passando. O correto seria fazer um teste que verifique se valores divisíveis por 3 e por 5 estão sendo convertidos corretamente, mas deixamos ele de lado para não alongar demais este artigo. Sugerimos que você mesmo escreva esse teste para praticar. ;)

No código do módulo FizzBuzz acima, apagamos o trecho que constrói a documentação do código. Neste mesmo local vamos montar o DocTest, sobre o qual falaremos a seguir.

DocTests

Antes de mostrar como DocTest funciona, precisamos falar um pouco sobre o IEx, o shell interativo do Elixir. Nele você pode rodar código Elixir livremente. Para iniciá-lo basta usar o comando iex no Terminal:

$ iex
Erlang/OTP 22 [erts-10.6.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Interactive Elixir (1.10.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 

Com o IEx rodando você pode verificar o resultado de código Elixir com facilidade, por exemplo:

iex(1)> 1 + 1   
2
iex(2)> "a" == "b"
false
iex(3)> frase = "Olá, mundo"
"Olá, mundo"
iex(4)> String.split(frase)
["Olá,", "mundo"]

Se você quiser testar módulos do projeto no diretório corrente, basta rodar o comando iex -S mix, que ele vai carregar o código do projeto junto com o IEx:

iex -S mix
Erlang/OTP 22 [erts-10.6.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Interactive Elixir (1.10.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> FizzBuzz.convert(3)
[1, 2, 'Fizz']

Para sair do IEx, aperte as teclas ctrl+c.

Conseguimos chamar a função convert do módulo FizzBuzz e o IEx nos mostra o resultado. É exatamente esse tipo de exemplo de código que podemos colocar no DocTest. Para usá-lo, você vai precisar usar a linha que apagamos do arquivo de testes no começo do do artigo: doctest FizzBuz. Além disso, no arquivo do módulo vamos incluir a descrição da função e um exemplo de execução do IEx usando o atributo @doc. Você também pode incluir uma descrição do módulo com o atributo @moduledoc, mas ele é opcional para os testes. Nosso arquivo ficou assim:

defmodule FizzBuzz do
  @moduledoc """
    Módulo do desafio FizzBuzz
  """

  @doc """
    Converte os valores de 1 até o argumento: divisíveis por 3,
    para Fizz; divisíveis por 5, para Buzz; divisíveis por 3 e 
    por 5 para FizzBuzz.

    ## Exemplo:

    iex> FizzBuzz.convert(5)
    [1, 2, "Fizz", 4, "Buzz"]
  """


  def convert(value) do
    1..value |> Enum.map(&evaluate_number/1)
  end

  defp evaluate_number(value) when rem(value, 15) == 0, do: "FizzBuzz"
  defp evaluate_number(value) when rem(value, 5) == 0, do: "Buzz"
  defp evaluate_number(value) when rem(value, 3) == 0, do: "Fizz"
  defp evaluate_number(value), do: value
end

Agora, se você rodar os testes, verá a indicação de que 1 doctest e 3 testes foram executados:

$ mix test
....

Finished in 0.05 seconds
1 doctest, 3 tests, 0 failures

A quantidade de . que aparece logo abaixo do comando mix test indica a quantidade de testes que foram executados. Nossos testes estão todos passando, mas em caso de falha, as mensagens dos resultados indicam se o erro ocorreu no DocTest:

 2) doctest FizzBuzz.convert/1 (1) (FizzBuzzTest)
     test/fizz_buzz_test.exs:3
     Doctest failed                         
     doctest:
       iex> FizzBuzz.convert(5)
       [1, 2, "Fizz", 4, "Buzz"]

Quando você inclui o atributo @doc no módulo e doctest FizzBuzz no arquivo de testes, o framework busca dentro da documentação do módulo os exemplos de comandos IEx, por isso a indentação e a indicação iex> são importantes. Depois de rodar os comandos, o framework compara a resposta obtida com a linha seguinte e apresenta o resultado. Isso permite que você crie testes simples e rápidos para verificar o funcionamento das sua funções antes de criar os testes unitários.

É importante lembrar que o principal objetivo do DocTest não é testar a sua aplicação inteira. Ele serve para manter sua documentação atualizada, enquanto os testes unitários servem o propósito de garantir o funcionamento correto da aplicação.

Referências

Foto de perfil do autor
André Kanamura

Dev na Campus Code