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, "Fizz"]
. Na linha que começa com test
, 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 Fizz, 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, "Fizz"]
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 Fizz até o valor 3" do
assert FizzBuzz.convert(3) == [1, 2, "Fizz"]
end
test "números divisíveis por 5 trocar por Buzz até o valor 5" do
assert FizzBuzz.convert(5) == [1, 2, "Fizz", 4, "Buzz"]
end
test "trocar por Fizz ou Buzz 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.