BDD em projetos Elixir

Paulo Igor
January 12, 2024

Neste post vamos falar de Behaviour Driven Development (Desenvolvimento Guiado por Comportamento), também chamado de BDD, que tem como um dos benefícios a interação dos diferentes papéis de um time tais como desenvolvedores, testadores, negócio, através da melhoria da comunicação.

Um pouco sobre BDD

A comunicação é um assunto comum e recorrente nos times de desenvolvimento de software, principalmente nas sessões de retrospectiva, quando os membros do time conversam sobre oportunidades de melhoria. Esse assunto costuma aparecer como ponto positivo, mas bem mais como um ponto de melhoria, por causar problemas nos resultados e gerar retrabalho.

O BDD usa uma linguagem ubíqua (Given-When-Then) que permite que o time se concentre nas expectativas de comportamento do sistema, antes mesmo dos detalhes técnicos da implementação. Essa característica ajuda não apenas na comunicação, mas traz toda a bagagem de benefícios do TDD (Test Driven Development), que ajuda no design emergente, criação de uma arquitetura testável e no feedback contínuo do progresso do desenvolvimento.

BDD no Elixir com Cabbage

Vamos ver como essa técnica funciona em projetos Elixir usando a biblioteca Cabbage, que permite traduzir a linguagem do BDD para testes no ExUnit.

Para configurar o Cabbage em um projeto, basta adicionar a dependência no arquivo mix.exs. Mais detalhes de configuração podem ser vistos na documentação do projeto.

  
    def deps do
      [{:cabbage, "~> 0.3.0"}]
    end
  

As palavras-chave para descrever a Funcionalidade e os Cenários, usando a linguagem Given-When-Then do BDD na biblioteca Cabbage são: Feature, Scenario, Given, When, Then, And. Atualmente a biblioteca não possui tradução para outros idiomas, porém o restante da expressão não precisa ser escrita em inglês, mantendo o benefício da comunicação para todo o time, caso o seu time não tenha o inglês como o idioma principal para comunicação.

Mapeando os Cenários com Given-When-Then

No exemplo a seguir, vamos construir a API para o jogo Papel-Pedra-Tesoura (Jokenpo). Para tal, o time irá discutir a seguinte story:

  
    
Como jogadores de Jokenpo
Nós gostaríamos de informar nossas jogadas
Para que apresentasse a jogada vencedora
    
  

Durante a discussão, o time pensa nos cenários/comportamentos possíveis para entender melhor as expectativas para a feature de “Apresentar o Vencedor da Partida de Jokenpo”. Para representar essas expectativas, vamos defini-las no formato Given-When-Then usando as palavras-chave mencionadas.

  
  
Feature: Apresentar o Vencedor da Partida de Jokenpo

Como jogadores de Jokenpo
Nós gostaríamos de informar nossas jogadas
Para que apresentasse a jogada vencedora

Scenario: Papel ganha da pedra
  Given que existem dois jogadores
  When o jogador1 jogar "papel"
  And o jogador2 jogar "pedra"
  Then o vencedor será "papel"

Scenario: Tesoura ganha do papel
  Given que existem dois jogadores
  When o jogador1 jogar "tesoura"
  And o jogador2 jogar "papel"
  Then o vencedor será "tesoura"

Scenario: Pedra ganha da tesoura
  Given que existem dois jogadores
  When o jogador1 jogar "pedra"
  And o jogador2 jogar "tesoura"
  Then o vencedor será "pedra"

Scenario: Empate
  Given que existem dois jogadores
  When o jogador1 jogar "papel"
  And o jogador2 jogar "papel"
  Then o vencedor será "empate"
  
  

Inicialmente foram discutidos pelo time os primeiros cenários e mapeados usando a linguagem do BDD. Esse mapeamento é facilmente discutido e trabalhado pelos diferentes papéis do time, pois não está escrito em uma linguagem de programação.

Foto: https://www.piqsels.com/en/public-domain-photo-jrnkf

É exatamente aí que o BDD, auxiliado por uma biblioteca como o Cabbage, traz grandes benefícios de produtividade, comunicação e qualidade.

Vamos salvar o arquivo judge.feature na pasta padrão test/features/, usada pelo Cabbage (possível configurar em outra pasta, consultar documentação), no nosso projeto Elixir.

Traduzindo com ExUnit

Agora que nossa feature e seus cenários foram mapeados, precisamos adicionar nosso “tradutor”, que é um simples teste em ExUnit.

Na primeira parte do teste, adicionamos o suporte à Cabbage: use Cabbage.Feature, async: false, file: "judge.feature", especificando qual arquivo esse teste irá utilizar.

Os próximos passos são as traduções da estrutura dos cenários descritos no arquivo judge.feature.

Para entendermos melhor a estrutura do Given-When-Then, vamos ver o objetivo de cada passo.

Given - esse passo é responsável pela pré-condição para a ação, onde realizamos a configuração prévia do cenário para a realização da ação. Nesse passo podemos preparar a carga de dados, criar uma sessão de usuário, ou qualquer outra configuração necessária para o cenário. No nosso exemplo, não temos uma pré-condição, mas poderíamos criar a sessão dos usuários de cada jogador, partindo da premissa que somente jogadores com usuários autenticados pudessem jogar. Para simplificar nosso exemplo, vamos seguir sem uma pré-condição.

  
    defgiven ~r/^que existem dois jogadores/, _, state do
       {:ok, state}
    end
  

When - esse é o passo que a ação é realizada, geralmente representada como uma entrada de dados, uma solicitação, interação, algo que gere uma necessidade de processamento e retorno do sistema. Para o nosso exemplo foram mapeadas as jogadas dos dois jogadores, onde a expressão regular está de acordo com o texto do cenário mapeado no arquivo .feature, definindo partes da expressão que seriam informações usadas para a ação.

  
    defwhen ~r/^o jogador1 jogar "(?\w+)"/, %{jogador1: jogador1}, state do
       {:ok, Map.merge(state, %{jogador1: jogador1})}
    end
    
    defand ~r/^o jogador2 jogar "(?\w+)"/, %{jogador2: jogador2}, state do
       {:ok, Map.merge(state, %{jogador2: jogador2})}
    end
	 

Nesse caso, a jogada de cada jogador é capturada pela parte da expressão regular (?<jogador1>\w+), que significa que qualquer sequência de caracteres (\w+) dentro das “ “ (aspas), será capturada e adicionada na chave jogador1 (?<jogador1>) no Map da função.

Then - aqui é o passo que mapeia a expectativa de comportamento, que é o retorno esperado para a ação que foi executada. Nesse passo utilizamos as funções assert e refute para verificar se o retorno das funções corresponde ao esperado.

  
    defthen ~r/^o vencedor será "(?\w+)"/, %{winner: winner}, state do
      assert {:ok, winner} == Jokenpo.Judge.play(state.jogador1, state.jogador2)
    end
  

Mais uma vez, usamos parte da expressão regular para capturar a expectativa de resultado que o sistema irá retornar (?<winner>\w+). Com isso temos todas as informações para verificar se o cenário é atendido pelo sistema.

Nesse trecho é possível perceber que o retorno esperado está a esquerda da expressão assert {:ok, winner} e a direita temos a integração com o sistema, passando as informações de entrada da ação, que foram capturadas nos passos anteriores Jokenpo.Judge.play(state.jogador1, state.jogador2).

Desenvolvendo guiado pelos Comportamentos (BDD)

Com o mapeamento finalizado, podemos executar nossos testes através do comando mix test e analisar o resultado. Na primeira execução, nada mais é que o teste informando que não temos o module Jokenpo.Judge e a função play definidas. Então vamos precisar criá-las.

Assim começa a magia das técnicas de desenvolvimento guiado (xDD), o próprio feedback de cada execução nos diz o próximo passo da implementação.

Após algumas execuções e desenvolvimento do código guiado pelo retorno de cada cenário mapeado, chegamos à seguinte implementação.

Mas podemos não estar totalmente convencidos que alcançamos o resultado, pois percebemos que os cenários mapeados sempre o jogador1 possui a jogada vitoriosa, não existe nenhum cenário onde o jogador2 coloca a jogada vitoriosa.

Então vamos adicionar novos cenários de testes para nos dar mais segurança que a solução alcançou a expectativa desejada. Com os novos cenários nosso arquivo judge.feature fica assim:

  
Feature: Apresentar o Vencendor da Partida de Jokenpo
 
Como jogadores de Jokenpo
Nós gostaríamos de informar nossas jogadas
Para que apresentasse a jogada vencedora


Scenario: Papel ganha da pedra
  Given que existem dois jogadores
  When o jogador1 jogar "papel"
  And o jogador2 jogar "pedra"
  Then o vencedor será "papel"

Scenario: Papel ganha da pedra
  Given que existem dois jogadores
  When o jogador1 jogar "pedra"
  And o jogador2 jogar "papel"
  Then o vencedor será "papel"

Scenario: Tesoura ganha do papel
  Given que existem dois jogadores
  When o jogador1 jogar "tesoura"
  And o jogador2 jogar "papel"
  Then o vencedor será "tesoura"
  
Scenario: Tesoura ganha do papel
  Given que existem dois jogadores
  When o jogador1 jogar "papel"
  And o jogador2 jogar "tesoura"
  Then o vencedor será "tesoura"

Scenario: Pedra ganha da tesoura
  Given que existem dois jogadores
  When o jogador1 jogar "pedra"
  And o jogador2 jogar "tesoura"
  Then o vencedor será "pedra"

Scenario: Pedra ganha da tesoura
  Given que existem dois jogadores
  When o jogador1 jogar "tesoura"
  And o jogador2 jogar "pedra"
  Then o vencedor será "pedra"

Scenario: Empate
  Given que existem dois jogadores
  When o jogador1 jogar "papel"
  And o jogador2 jogar "papel"
  Then o vencedor será "empate"
  

Adicionamos mais 3 cenários, onde o jogador2 tem a jogada vitoriosa. Agora basta executarmos novamente nossos testes (`mix test`), sem precisar alterar nenhuma linha de código.

Isso mesmo! Acabamos de incluir mais cenários de testes, escrevendo em uma linguagem acessível para todos do time, incluindo os que não possuem conhecimento técnico da linguagem de programação.

O resultado é que a implementação também atendeu aos novos cenários, e agora temos mais segurança para dar os próximos passos no desenvolvimento da solução.

Conclusão

Neste post, aprendemos sobre a técnica do BDD e como utilizá-la em projetos Elixir, desde a discussão dos cenários com todo o time até a automação dos testes através da biblioteca Cabbage.

O código fonte completo usado nesse post está nesse repositório.

Se quiser saber mais sobre as técnicas de desenvolvimento guiado (xDD), tem uma palestra no canal do Youtube da Idopter Labs Kit de Sobrevivência Ágil com xDD, que foi apresentada no Agile Brazil 2021.

Espero que este post ajude você em seus projetos. Caso sua empresa precise de ajuda na construção de aplicações Elixir, entre em contato!