Essa é a continuação da parte 1 da série "Testes em Python".
Na série de hoje iremos ver como realizar testes de comportamento resgatando alguns conceitos visto na primeira parte.
Pré-requisito
- Conhecimento básico de Python
- Conceito básico sobre testes e o porque eles são importantes (Veja a parte 1 da série)
Roteiro
- O que são testes de comportamento
- O que é BDD
- Behave
- Exemplo
- Considerações adicionais
- Conclusão
1. O que são testes de comportamento
1.1 Motivação
Ao receber uma demanda de uma implementação de funcionalidade, normalmente são os próprios desenvolvedores que implementam e realizam os casos de testes para validar se tudo funciona como o esperado.
No entanto, essas implementações são escritas de uma maneira muito técnica, de modo que somente quem possui conhecimento de certas tecnologias possa entender, não permitindo uma clareza de comunicação com profissionais não-técnicos, que geralmente são os que demandam a funcionalidade.
1.2 Testes de comportamento
Testes de comportamento surgiu a partir da necessidade de validar cenários por meio de uma comunicação mais transparente. Deste modo, conseguimos validar qual deve ser o comportamento do nosso código e realizar assertivas em cima desses cenários.
Esses testes permitem:
- Entendimento comum por meio do escopo do negócio (regras de negócio)
- Comunicação clara por meio de linguagem comum entre todos os participantes envolvidos
- Melhora na documentação sobre os cenários da aplicação
- Ajuda em um melhor acordo comum da "definição de pronto" do sistema
2. O que é o BDD
BDD é uma sigla que significa Behavior-Driven Development, do português desenvolvimento orientado ao comportamento. Ele foi introduzido por Dan North em resposta ao tradicional TDD¹
TDD¹ é uma sigla que significa Test-Driven Development, do português desenvolvimento orientado a testes. Resgatando o que foi visto na parte 1 da série, seria escrever os cenários de testes antes de implementar o nosso código propriamente dito. Assim, estaríamos garantindo que toda a nossa aplicação estaria coberta por testes. Veja mais em https://www.eusoudev.com.br/tdd/
Isso significa que vamos primeiro escrever cenários com comportamentos em linguagem natural e depois escrever nosso código orientado a isso. Veremos que esses comportamentos serão fluxos do usuário.
O BDD segue algumas filosofias, dentre elas pode se destacar a entrega de valor ao negócio. Assim, diferentes personas envolvidas na elaboração de um projeto como desenvolvedores, QA, participantes do negócio e outras pessoas não técnicas podem garantir que o valor está sendo entregue a partir de uma documentação clara e simplificada.
Quando todos possuem uma visão clara dos requerimentos para um programa funcionar, todos ficam mais alinhados com o valor que será entregue, pois a aplicação estará coberta por esses cenários orientados ao comportamento. Isso inclusive pode levar com que todos descubram cenários não pensados anteriormente.
3. Behave
Behave é uma biblioteca em Python que iremos utilizar para fazer nossos testes orientados ao comportamento. Ela utiliza componentes do cucumber, que é uma ferramenta para rodar testes escritos em linguagem natural.
Veremos como utilizar na prática por meio de um exemplo.
4. Exemplo
4.1 O escopo do exemplo
Vamos realizar um pequeno sistema de criação de conta de um banco simplificado. Nele, podemos além de criar uma conta, depositar e sacar dinheiro.
Vamos considerar:
- Nome do cliente
- Número da conta
- Senha
- Saldo
As ações de depositar e sacar dinheiro só poderão ser feitas se o cliente estiver autenticado.
Nota: Repare que esse fluxo é bem simplificado e vários outros fatores estão sendo desconsiderados dessa abstração para que possamos nos concentrar no BDD.
4.2 Preparando o ambiente
Vamos criar um diretório para conter o nosso código, vamos chamar a pasta de simple_bank_account.
Antes de começar a implementação, vamos escrever o comportamento esperado. Para isso vamos instalar o behave.
OBS.: Sugiro utilizar um ambiente virtual para instalar as dependências, pois irá possibilitar isolar o nosso código de outros recursos e projetos que temos no nosso computador. Recomendo o tutorial do Django Girls para isso.
Quando estiver no ambiente virtual, rode o comando para instalar o behave
pip install behave
Agora vamos implementar nossa estrutura base de arquivos. Teremos nossa estrutura raiz que terá o projeto e o behave sempre procura os arquivos dentro de um diretório chamado features (irá conter as funcionalidades do sistema) e as steps (local onde iremos realizar as assertivas para garantir que as funcionalidades estão funcionando).
No final vamos ter algo parecido com essa estrutura:
Feita a criação dos diretório, chegamos a essa estrutura
Vamos agora implementar nossa primeira funcionalidade
4.3 Escrevendo a estória do usuário (features)
Vamos escrever o nosso primeiro fluxo do usuário.
Esses fluxos são arquivos terminados em .feature e são interpretados pelo behave a partir de uma linguagem chamada Gherkin.
Essa linguagem permite interpretar a linguagem escrita de maneira natural. Diversos idiomas são suportados e para o nosso exemplo vamos ver o uso desse documento escrito em português.
Fluxo de criação da conta
Dentro da pasta features vamos criar um arquivo chamado create_account.feature e nele iremos escrever a funcionalidade de criação de conta.
Seguindo a nomenclatura da documentação, mas escrevendo em português, temos o seguinte fluxo
# language: pt
Funcionalidade: criar conta
"""
Como cliente, eu quero que seja possível criar uma nova conta no banco para possibilitar depositar e sacar dinheiro
"""
Cenário: cliente solicita criação de conta
Quando a conta do cliente for criada
Então o saldo de sua conta deve estar zerado
4.3.1 create_account.feature
Repare que aqui temos algumas palavras chaves como Funcionalidade, Cenário, Quando e Então.
- Funcionalidade: descreve o comportamento esperado de uma funcionalidade da aplicação.
- Cenário: descreve uma parte do comportamento esperado da funcionalidade. Faz referência com cada cenário de teste que vimos com unittest.
- Quando: descreve uma ação que acontece na nossa aplicação
- Então: o que deve ser retornado quando a ação for realizada
Repare que esse documento poderá ser escrito por analistas de negócios, analista de qualidade e profissionais não técnicos justamente por estar em linguagem natural.
Outras palavras chaves podem ser utilizadas e o mapeamento completo pode ser visto na documentação do cucumber
Vamos rodar no terminal o comando behave dentro da raiz do nosso projeto e ver o que acontece
behave
Repare que temos alguns avisos nos alertando que não temos nenhuma definição de step implementada. Vamos implementar essas steps.
4.4 Escrevendo as steps (implementando as assertivas)
Aqui é onde tudo será validado. Steps são arquivos em python (.py) que irá implementar as palavras chaves utilizadas no arquivo de feature.
Dentro da pasta features/steps vamos criar uma step que valide a feature de criação de conta. Para isso, vamos criar o arquivo create_account.py criando uma estrutura desse jeito
Repare que no arquivo 4.3.1 create_account.feature utilizamos algumas palavras chave. Essas palavras chaves serão utilizadas com base em um decorator implementado pela própria biblioteca do behave. Vamos importar as palavras mapeadas
from behave import when, then
@when("a conta do cliente for criada")
def create_account(context):
pass
@then("o saldo de sua conta deve estar zerado")
def validate_create_account(context):
pass
4.4.1 create_account.py
Repare que nesse arquivo python utilizamos as palavras chaves when e then que respectivamente significam o quando e então que utilizamos no nosso arquivo 4.3.1 create_account.feature. E o parâmetro que é passado pra esse decorator (@when e @then) é justamente o texto que escrevemos na nossa feature.
Vamos rodar o behave novamente
Veremos que tudo passou, mas na verdade não estamos validando nada ainda. Vamos começar a rascunhar nosso código e importar no arquivo de step.
4.5 Interagindo com o código e o teste
Iremos criar uma estrutura de classe simples para conseguir importar no nosso arquivo de teste. Vamos criar fora da pasta de features
class Client:
def __init__(self):
pass
class SimpleBankAccount(Client):
def __init__(self):
super().__init__()
4.5.1 simple_bank_account.py
Considerando os requisitos levantados no escopo do exemplo, vamos passar para criação da conta o nome do cliente, a numeração da conta (sim, nesse exemplo fictício o cliente especifica isso) e a senha. Logo depois, validar se o saldo da conta é zero.
from behave import when, then
from simple_bank_account import SimpleBankAccount
@when("a conta do cliente for criada")
def create_account(context):
account = SimpleBankAccount(client_name="Shizuku Tsukishima", number=1, password=123456)
context.result = account.balance
@then("o saldo de sua conta deve estar zerado")
def validate_create_account(context):
assert context.result == 0
4.5.2 create_account.py
Na primeira step @when("a conta do cliente for criada") instanciamos nossa classe SimpleBankAccount passando os dados necessários que gostaríamos.
Logo em seguida, salvamos dentro do contexto da nossa step, o resultado que queremos validar através de um comportamento dinâmico que o behave implementa. Isso é importante para compartilhar valores com outras steps.
Já na step @then("o saldo de sua conta deve estar zerado") pegamos o valor salvo dentro do contexto e validamos se é igual a zero com o assert
Vamos rodar o behave novamente
Veremos que começou a aparecer alguns erros, o primeiro ele é um TypeError pois o nosso código não tem ainda nada implementado, somente a base. Vamos voltar no arquivo 4.5.1 simple_bank_account.py e inicializar a classe com esses argumentos.
class Client:
def __init__(self, client_name: str):
self.name = client_name
class SimpleBankAccount(Client):
def __init__(self, client_name: str, number: int, password: int):
super().__init__(client_name=client_name)
self.number = number
self.password = password
4.5.3 simple_bank_account.py
Rodando novamente o behave vamos perceber que não existe também o atributo balance que irá conter o saldo da conta. Isso acontece pois no 4.5.2 create_account.py estamos acessando account.balance e esse atributo realmente não existe em nossa classe. Vamos adicioná-lo
class Client:
def __init__(self, client_name: str):
self.name = client_name
class SimpleBankAccount(Client):
def __init__(self, client_name: str, number: int, password: int):
super().__init__(client_name=client_name)
self.number = number
self.password = password
self.balance = 0
Rodando novamente o behave veremos que a funcionalidade criar conta deve funcionar
E repare que dessa vez ele passou pois realmente estava testando nosso cenário. Se trocássemos o assert da nossa step para
assert context.result != 0
por exemplo, veremos que os testes não irão mais passar
É bom fazer essas validações com outros tipos de valores para garantir que o teste realmente está sendo implementado.
4.6 Funcionalidade de depositar e sacar
Para esse artigo não ficar muito grande, não iremos ver nesse artigo como ficaram as outras funcionalidades. No entanto, o código poderá ser visto aqui.
No final, teremos um retorno que descreve exatamente o fluxo de interação do usuário com o nosso sistema.
5. Considerações adicionais
Aqui, listo alguns tópicos que são interessantes levar em consideração:
- Para conseguir fazer um bom levantamento nos arquivos de funcionalidade, é uma boa prática implementar uma cultura de Discovery para envolver diferentes profissionais para discutir a cerca do negócio e da implementação.
- É importante seguir o fluxo: escrever a funcionalidade > implementar os steps > implementar o código para programar orientado ao comportamento. E, ao longo desse processo, rodar o behave para ver os testes pendentes, falhando e passando. Mas nunca se esqueça de refatorar o código. Nesse código de exemplo mesmo poderíamos ter desacoplado algumas coisas como a parte de autenticação (pode ser visto no código completo do github) e se ao rodar os testes eles passarem, ainda estaríamos garantindo que os comportamentos permanecem os mesmos.
- Utilize hooks para evitar steps repetidas nos testes de comportamento
- Aprender sobre as regras de negócio é tão importante quanto aprender sobre tecnologia. Lembre-se que nosso papel como profissional da área de tecnologia é resolver problemas de um certo domínio de negócio. Entender como essas regras funcionam são importantes para ajudar a pensar em cenários e corner cases nas validações.
6. Conclusão
O BDD reforça o pensamento "Outside-in" (de fora para dentro) e é guiado a entrega de valor ao negócio. Os colaboradores então trabalham em conjunto no levantamento dos cenários que serão entregues e que automaticamente estão relacionados com a cobertura de testes do código em questão.
Usar ou não BDD? Depende. Se for necessária interações entre diferentes personas (analista de negócios, desenvolvedores, testadores, stackholder) pode ser interessante principalmente pela escrita de um documento em linguagem natural.
De toda forma é interessante saber que existe essa outra maneira de implementar testes e que tendo um documento comum que será utilizado tanto para validar os cenários do sistema tanto como definições dos critérios de aceitação entre os analistas de negócio é algo bem legal :)
A parte mais difícil de construir um sistema é decidir precisamente o que construir
– Fred Brooks, O Mítico Homem-Mês: Ensaios Sobre Engenharia de Software