Testes

Testes são uma parte importante do desenvolvimento de software. Nesta lição nós veremos como testar nosso código Elixir com ExUnit e algumas das melhores práticas de como fazer isto.

ExUnit

O framework de testes integrado do Elixir é o ExUnit e ele inclui tudo o que precisamos para testar exaustivamente o nosso código. Antes de avançar, é importante notar que testes são implementados como scripts Elixir, por isso precisamos usar a extensão de arquivo .exs. Antes de podermos executar nossos testes nós precisamos iniciar o ExUnit com ExUnit.start(), e isto é mais comumente feito em test/test_helper.exs.

Quando geramos nosso projeto de exemplo na lição anterior, o mix foi útil o suficiente para criar um teste simples para nós e podemos encontrá-lo em test/example_test.exs:

defmodule ExampleTest do
  use ExUnit.Case
  doctest Example

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

Podemos executar testes do nosso projeto com mix test. Se fizermos isso agora devemos ver uma saída semelhante a:

..

Finished in 0.03 seconds
2 tests, 0 failures

Porque há dois testes na saída? Além do teste em test/example_test.exs, Mix também criou um doctest em lib/example.ex.

defmodule Example do
  @moduledoc """
  Documentation for Example.
  """

  @doc """
  Hello world.

  ## Examples

      iex> Example.hello
      :world

  """
  def hello do
    :world
  end
end

assert

Se você escreveu testes antes, então você está familiarizado com assert; em alguns frameworks should ou expect preenchem o papel de assert.

Usamos o assert macro para testar se a expressão é verdadeira. No caso em que não é, um erro vai ser gerado e os nossos testes irão falhar. Para testar uma falha, vamos mudar nossa amostra e em seguida executar o mix test.

defmodule ExampleTest do
  use ExUnit.Case
  doctest Example

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

Agora nós devemos ver uma saída bem diferente:

  1) test greets the world (ExampleTest)
     test/example_test.exs:5
     Assertion with == failed
     code:  assert Example.hello() == :word
     left:  :world
     right: :word
     stacktrace:
       test/example_test.exs:6 (test)

.

Finished in 0.03 seconds
2 tests, 1 failures

ExUnit nos diz exatamente onde nossas asserções falharam, qual era o valor esperado e qual o valor atual.

refute

refute é para assert como unless é para if. Use refute quando você quiser garantir que uma declaração é sempre falsa.

assert_raise

Às vezes pode ser necessário afirmar que um erro foi gerado. Podemos fazer isso com assert_raise. Vamos ver um exemplo de assert_raise na lição sobre Plug.

assert_receive

Em Elixir, aplicações consistem em atores/processos que enviam mensagens um para o outro, portanto você irá querer testar mensagens sendo enviadas. Como o ExUnit é executado no seu próprio processo, ele pode receber mensagem como qualquer outro processo e você pode afirmar nele mesmo com a macro assert_received:

defmodule SendingProcess do
  def run(pid) do
    send(pid, :ping)
  end
end

defmodule TestReceive do
  use ExUnit.Case

  test "receives ping" do
    SendingProcess.run(self())
    assert_received :ping
  end
end

assert_received não espera mensagens, com assert_receive você pode especificar um tempo limite.

capture_io e capture_log

Capturar uma saída da aplicação é possível com ExUnit.CaptureIO sem mudar a aplicação original. Basta passar a função gerando a saída em:

defmodule OutputTest do
  use ExUnit.Case
  import ExUnit.CaptureIO

  test "outputs Hello World" do
    assert capture_io(fn -> IO.puts("Hello World") end) == "Hello World\n"
  end
end

ExUnit.CaptureLog é o equivalente para capturar a saída de Logger.

Configuração de Teste

Em alguns casos, pode ser necessária a realização de configuração antes de nossos testes. Para fazer isso acontecer, nós podemos usar as macros setup e setup_all. setup será executado antes de cada teste e setup_all uma vez antes do suíte de testes. Espera-se que eles vão retornar uma tupla de {:ok, state}, o estado estará disponível para os nossos testes.

Por uma questão de exemplo, vamos mudar o nosso código para usar setup_all:

defmodule ExampleTest do
  use ExUnit.Case
  doctest Example

  setup_all do
    {:ok, recipient: :world}
  end

  test "greets", state do
    assert Example.hello() == state[:recipient]
  end
end

Simulações de Teste (Mocks)

Queremos ter cuidado com a forma que pensamos sobre “mocks”. Quando simulamos certas interações criando funções de simulação exclusivas em um determinado exemplo de teste, estabelecemos um padrão perigoso. Acoplamos a execução dos nossos testes ao comportamento de uma dependência específica, como um cliente de API. Evitamos definir o comportamento compartilhado entre nossas funções simuladas. Tornamos mais difícil iterar nossos testes.

Em vez disso, a comunidade Elixir nos encoraja a mudar como pensamos sobre simulações de teste; para pensarmos em mock como um substantivo, em vez de um verbo.

Para uma discussão mais longa neste tópico, veja este excelente artigo.

A essência é que, em vez de simular dependências em testes (mock como verbo), existem muitas vantagens em explicitamente definir interfaces (comportamentos) para código fora da aplicação e usar implementações de simulações (mock como substantivo) no seu código para testes.

Para aproveitar esse padrão “mock-como-substantivo” você pode:

Para um mergulho mais profundo em simulações de teste em Elixir, e uma olhada na biblioteca Mox que permite definir simulações simultâneas, confira nossa lição sobre Mox aqui.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!