Neste artigo veremos que é possível sim praticar TDD e ter testes automatizados no Bash. Usaremos o BATS como framework de testes.
O objetivo principal desse artigo é ensinar o básico do básico de:
- como usar o BATS
- como praticar Test-Driven Development
Para isso escreveremos um “Hello, World poliglota”.
Durante a leitura pode ser que você ache que estou progredindo de forma demasiadamente lenta para resolver um problema tão simples, e isso é verdade! É intencional. Pois quero mostrar o passo a passo do TDD (e não como escrever hello-world).
Claro que estas ferramentas são bem mais úteis para resolver “problemas reais”. Mas como o foco aqui é ensinar as técnicas de TDD e utilização do BATS, eu não quero adicionar mais complexidade usando um “problema real”.
Ah! E tenho certeza que no caminho você vai acabar aprendendo alguns macetinhos de bash. 😉
O que é BATS?
BATS signfica Bash Automated Testing System. É um framework de testes para bash, que permite que verifiquemos se o nosso programa está se comportando da maneira que queremos.
Estou sendo breve pois já quero partir pra ação. Mas se você quiser, pode obter mais informações sobre o BATS na documentação oficial.
Instalando o BATS
A maioria das distribuições GNU/Linux possuem um pacote chamado bats
que você pode instalar usando o gerenciador de pacotes oficial.
Eu particularmente prefiro instalar via Homebrew, pois é um método que funciona tanto pra GNU/Linux como pra MacOS.
brew install bats-core
Fique a vontade para instalar da maneira que você preferir.
O que é TDD
TDD significa Test-Driven Development, ou seja, desenvolvimento guiado por testes.
Trata-se de uma metodologia onde primeiro escrevemos o teste e deixamos que esse teste guie o desenvolvimento.
O processo do TDD segue um ciclo iterativo geralmente conhecido como “Red-Green-Refactor”.
- Escrevemos um teste definindo uma nova funcionalidade. Nesse primeiro momento a funcionalidade nem existe, portanto o teste falha (fase “Red”)
- Em seguida, escrevemos o mínimo de código necessário pra passar no teste (fase “Green”).
- Por fim, o código é refatorado para melhorar sua estrutura e legibilidade. Fazemos isso com a segurança dos testes garantindo que não estamos quebrando nada (fase “Refactor”)
Eu acho o TDD extremamente valioso pelos seguintes motivos:
- encoraja um design de código mais modular e menos acoplado.
- ajuda a identificar e corrigir bugs mais cedo.
- se bem aplicado, reduz bastante o custo e tempo de manutenção.
- os testes servem como uma documentação viva do sistema, já que descrevem o comportamento esperado do programa.
Só coisa boa, né? Mas, obviamente, todas essas “maravilhas” possuem um preço: aprender TDD é trabalhoso e requer bastante prática.
Esse artigo é exatamente uma maneira de estimular o início dessa jornada de praticante de TDD
Se você se importa com a qualidade do seu trabalho, saiba que esse esforço vale muito a pena!
Agora chega de blablabla! Se você ainda está aqui lendo é por que ao menos têm esperança de que isso é uma coisa útil de se aprender. Então vamos pra prática!
Primeiros passos
Antes de tudo, vamos preparar um diretório onde colocaremos o código do nosso projeto:
mkdir hello-tdd
cd hello-tdd
Vamos também iniciar um repositório git no diretório do nosso projeto:
git init
Você provavelmente já sabe como criar um “Hello, World” em bash. Peço que resista à tentação de escrever o código e siga comigo pra usarmos TDD, onde escreveremos os testes primeiro. Antes mesmo do nosso código principal existir.
Pra não bagunçar o projeto misturando código do nosso programa com o código dos testes automatizados, vamos criar um diretório específico para os testes:
# assumindo que já estamos no 'hello-tdd/'
mkdir test
Criaremos nosso primeiro teste para checar se estamos aptos a executar nosso script. Para isso criamos o arquivo test/hello_test.bats
.
OBSERVAÇÃO: o arquivo tem a extensão .bats
mas o conteúdo é basicamente bash! O BATS não é uma “linguagem” nova que você tem que aprender. A única coisa diferente que você verá num arquivo .bats
é a declaração dos testes nesse formato: @test "nome do teste"
. Todo o resto é apenas bash.
Aqui está o test/hello_test.bats
:
# eu prefiro colocar a descrição em inglês,
# fique a vontade pra colocar em português.
@test "can run the script" {
./hello.sh
}
É isso mesmo! Nosso primeiro teste quer apenas verificar se conseguimos executar nosso programa, por isso está simplesmente chamando ./hello.sh
! Vamos executar esse teste e ver o output:
$ bats test/hello_test.bats
hello_test.bats
✗ can run the script
(in test file test/hello_test.bats, line 2)
`./hello.sh' failed with status 127
/home/meleu/src/hello-tdd/test/hello_test.bats: line 2: ./hello.sh: No such file or directory
1 test, 1 failure
💥 Já começamos com uma falha!
Pois vá se acostumando! O TDD é assim mesmo…
Observe que no meio daquela mensagem temos o motivo da falha: ./hello.sh: No such file or directory
.
O arquivo não existe. Pois então vamos criá-lo:
touch hello.sh
E executar o teste novamente:
$ bats test/hello_test.bats
hello_test.bats
✗ can run the script
(in test file test/hello_test.bats, line 2)
`./hello.sh' failed with status 126
/home/meleu/src/hello-tdd/test/hello_test.bats: line 2: ./hello.sh: Permission denied
1 test, 1 failure
Outra falha. Novamente com uma dica do que devemos fazer: ./hello.sh: Permission denied
.
Se temos um Permission denied
, só nos resta dar permissão de execução pro arquivo e executar o teste novamente:
$ # dando permissão de execução
$ chmod a+x hello.sh
$ # executando o teste
$ bats test/hello_test.bats
hello_test.bats
✓ can run the script
1 test, 0 failures
🥳🎉 Agora sim! Nosso primeiro teste passou!
Tá bom… Isso não é lá grande coisa. Estamos apenas validando que um arquivo tem permissão de execução. Essa parte foi só pra termos uma mini injeção de dopamina ao ver um teste passando.
Organizando os arquivos do projeto
Já que criamos um diretório para os testes, vamos também criar um diretório para o código executável e colocar o arquivo lá:
mkdir src
mv hello.sh src/hello.sh
Dessa forma teremos essa estrutura de diretórios:
$ tree
.
├── src
│ └── hello.sh
└── test
└── hello_test.bats
2 directories, 2 files
Agora vamos executar nosso teste novamente:
$ bats test/hello_test.bats
hello_test.bats
✗ can run the script
(in test file test/hello_test.bats, line 2)
`./hello.sh' failed with status 127
/home/meleu/src/hello-tdd/test/hello_test.bats: line 2: ./hello.sh: No such file or directory
1 test, 1 failure
Ooops! Nosso teste voltou a falhar por não encontrar o arquivo!
Isso está acontecendo pois não estamos especificando o caminho até o arquivo! Vamos resolver isso:
@test "can run the script" {
./src/hello.sh
}
Executando o teste:
$ bats test/hello_test.bats
hello_test.bats
✓ can run the script
1 test, 0 failures
OK. Nosso arquivo foi encontrado e o teste passou. Mas ainda assim ficamos com aquela sensação de que essa solução não parece muito robusta.
Vamos tentar por exemplo entrar no diretório do test/
e executar o teste de lá:
$ cd test/
$ bats hello_test.bats
hello_test.bats
✗ can run the script
(in test file hello_test.bats, line 2)
`./src/hello.sh' failed with status 127
/home/meleu/src/hello-tdd/test/hello_test.bats: line 2: ./src/hello.sh: No such file or directory
1 test, 1 failure
O teste simplesmente não encontrou nosso codigo, só por que mudamos de diretório… Não queremos um teste tão “frágil” assim. Vamos resolver isso criando um setup
pro nosso teste.
Fazendo o setup
Em um arquivo BATS, a função setup
tem um significado especial: ela é uma função que será executada antes de cada teste (se quiser mais detalhes veja a documentação aqui).
Vamos criar uma função de setup
para adicionar o caminho pro nosso executável direto no PATH
. Dessa forma podemos executar o hello.sh
sem nos preocupar em especificar o caminho.
Nosso test/hello_test.bats
então fica assim:
setup() {
PATH="${BATS_TEST_DIRNAME}/../src:${PATH}"
}
@test "can run the script" {
hello.sh
}
Ali no setup
estamos nos aproveitando da variável $BATS_TEST_DIRNAME
, que o BATS já deixa preenchida com o caminho pro diretório do arquivo de teste. A partir desse diretório vamos para ../src
, que é onde está o nosso executável.
Observe que o teste também está sendo feito com uma chamada direta ao hello.sh
, sem especificar o caminho. Nós podemos fazer isso pois o setup
já deixou o PATH
devidamente preparado.
Vamos conferir se isso realmente funciona:
$ bats hello_test.bats
hello_test.bats
✓ can run the script
1 test, 0 failures
Maravilha! Estamos no caminho certo!
O que vimos até agora
- Criamos o diretório
hello-tdd
para começar um novo projeto do zero. - Colocamos nosso arquivo de teste no diretório
test/
. - Nosso arquivo executável ficará em
src/
. - Usamos o
setup
para atualizar oPATH
com o caminho para o executável. - Usamos a variável
$BATS_TEST_DIRNAME
pra pegar o caminho do diretório onde está nosso arquivo de teste. - Nosso teste chama o arquivo executável usando apenas o nome do arquivo (sem precisar especificar o caminho completo)
Só pra lembrar, no momento nosso test/hello_test.bats
está assim:
setup() {
PATH="${BATS_TEST_DIRNAME}/../src:${PATH}"
}
@test "can run the script" {
hello.sh
}
E o nosso src/hello.sh
nada mais é que um arquivo vazio com permissão de execução.
Instalando BATS helpers
O projeto BATS oferece helpers com algumas conveniências que podem nos ajudar bastante na escrita de testes.
No nosso projeto nós queremos verificar se a saída do programa é Hello, World
. Fazemos isso criando asserções sobre o que o programa gera como saída.
Para criar asserções vamos precisar do helper bats-assert
. Vamos usar também o bats-support
para que ele nos forneça mensagens de erro/falha amigáveis, dando mais clareza sobre onde nosso código está quebrando.
Para “instalar” esses helpers no nosso projeto vamos adicioná-los como submódulos:
# bats-assert: responsável pelas asserções
git submodule add \
https://github.com/bats-core/bats-assert.git \
test/test_helper/bats-assert
# bats-support: responsável por mensagems de falha mais amigáveis
git submodule add \
https://github.com/bats-core/bats-support.git \
test/test_helper/bats-support
# bats-core: o bats propriamente dito (útil para usarmos em pipelines)
git submodule add \
https://github.com/bats-core/bats-core.git \
test/bats
Observe que estamos também “instalando” como submódulo o próprio bats-core
. Isso é útil, por exemplo, para rodar testes diretamente numa pipeline de Integração Contínua (mas isso é papo para um outro artigo).
No momento nossa estrutura de diretórios está assim:
$ tree -F -L 2
./
├── src/
│ └── hello.sh*
└── test/
├── bats/ # 👈 arquivos do bats-core aqui
├── hello_test.bats
└── test_helper/ # 👈 bats-support e bats-assert aqui
Implementando funcionalidades com TDD
Nós queremos implementar, via TDD, um programa que ao ser chamado escreva na tela a string Hello, World
.
Primeiro vamos carregar os helpers no nosso setup
, dessa forma:
setup() {
load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'
PATH="${BATS_TEST_DIRNAME}/../src:${PATH}"
}
Como já falamos, para testar tal programa precisamos fazer asserções sobre sua saída. Nosso teste ficará assim:
# ... conteúdo original do test/hello_test.bats
@test "say Hello, World" {
run hello.sh # 👈 note que estamos usando um `run` aqui!
assert_output "Hello, World"
}
Nesse código estamos usando o run
para chamar o nosso programa. Isso nos traz várias conveniências, como por exemplo automaticamente salvar a saída gerada pelo programa para que possamos verificar com o assert_output
.
Pra não quebrar o nosso flow de TDD, eu estou omitindo explicações detalhadas do
run
e doassert_output
. Se tiver dúvidas deixe ali nos comentários.
Vamos executar o teste e ver o resultado:
$ bats test/hello_test.bats
hello_test.bats
✓ can run the script
✗ say Hello, World
(from function `assert_output' in file test/test_helper/bats-assert/src/assert_output.bash, line 194,
in test file test/hello_test.bats, line 16)
`assert_output "Hello, World"' failed
-- output differs --
expected : Hello, World
actual :
--
2 tests, 1 failure
O que é bacana de usar o assert_output
é que ele diz claramente o que era esperado na saída e o que foi realmente impresso:
expected : Hello, World
actual :
Esperamos Hello, World
mas não imprimimos coisa alguma. Isso já era de se esperar, afinal o nosso hello.sh
é apenas um arquivo vazio com permissão de execução.
Vamos resolver isso escrevendo o clássico hello-world em bash no src/hello.sh
:
#!/usr/bin/env bash
echo "Hello, World"
Executemos o teste novamente:
$ bats test/hello_test.bats
hello_test.bats
✓ can run the script
✓ say Hello, World
2 tests, 0 failures
Woohool!!! 🥳🎉 O teste passou!
Controle de versão
Terminamos de implementar uma nova funcionalidade e todos os testes estão passando. Isso é um bom momento pra fazer um commit.
Se fizermos alguma besteira, podemos facilmente voltar para esse commit onde tudo estava funcionando.
git commit --all --message "Hello, World"
Hello, meleu
Agora queremos que o nosso hello-world seja capaz de cumprimentar o nome que passamos como argumento para o programa. E se não passarmos nome algum, queremos continuar cumprimentando o mundo inteiro com Hello, World
.
Lembre-se: escreva o teste primeiro!
# ... conteúdo original do test/hello_test.bats
@test "say hello to people" {
run hello.sh meleu
assert_output "Hello, meleu"
}
Executando:
$ bats test/hello_test.bats
hello_test.bats
✓ can run the script
✓ say Hello, World
✗ say hello to people
(from function `assert_output' in file test/test_helper/bats-assert/src/assert_output.bash, line 194,
in test file test/hello_test.bats, line 19)
`assert_output "Hello, meleu"' failed
-- output differs --
expected : Hello, meleu
actual : Hello, World
--
3 tests, 1 failure
Observação: acostume-se a não ficar irritado vendo testes falharem!
Confie no processo! O Desenvolvimento Guiado por Testes é assim, a gente vai checando as mensagens de falha dos nossos testes e isso vai guiando a nossa próxima ação.
Aqui a mensagem de erro está nos mostrando que esperávamos Hello, meleu
mas obtivemos Hello, World
.
Vamos resolver isso no nosso src/hello.sh
da maneira mais naïve possível:
#!/usr/bin/env bash
echo "Hello, $1"
E vamos conferir se os testes passam:
$ bats test/hello_test.bats
hello_test.bats
✓ can run the script
✗ say Hello, World
(from function `assert_output' in file test/test_helper/bats-assert/src/assert_output.bash, line 194,
in test file test/hello_test.bats, line 14)
`assert_output "Hello, World"' failed
-- output differs --
expected : Hello, World
actual : Hello,
--
✓ say hello to people
3 tests, 1 failure
😳
O nosso novo teste passou mas acabamos quebrando um outro.
Eu gostaria que você refletisse um pouco sobre esse caso. Com um nosso reles hello-world podemos extrapolar para um cenário de “problema real”. Pense em quantas vezes você pegou aquele seu código que está funcionando bem, e adicionou uma nova funcionalidade. Fez um rápido teste manual, ficou satisfeito com o resultado e seguiu adiante. Pouco depois você percebeu que a sua nova funcionalidade quebrou alguma outra parte do programa.
Esse é o tipo de cenário que o TDD não permite que ocorra! Como você tem testes automatizados, se você quebrar algo que antes estava funcionando, sua bateria de testes já vai te avisar.
Quando nos deparamos com essa situação de fazer quebrar um teste que estava passando, a primeira atitude que devemos tomar é desfazer nossa última alteração e pensar numa alteração melhor.
O que eu acredito que devemos fazer aqui é definir um valor default para o caso do usuário não passar nome algum. Portanto o hello.sh
fica assim:
#!/usr/bin/env bash
echo "Hello, ${1:-World}"
Executando os tests:
$ bats test/hello_test.bats
hello_test.bats
✓ can run the script
✓ say Hello, World
✓ say hello to people
3 tests, 0 failures
Que maravilha! Todos os testes passando!
Quando a gente se depara com todos os testes passando, isso imediatamente deve disparar em nossa mente o desejo de refatorar.
Refatoração
Vamos nos aproveitar da segurança dos testes e focar na qualidade do nosso código. O objetivo é deixá-lo mais legível e de mais fácil manutenção.
Óbvio que para um hello-world não tem como ser mais simples do que echo "Hello, World"
, mas vou aproveitar o nosso exemplo para escrever esse código com algumas práticas que eu sempre uso nos meus códigos bash.
Primeiro: todo código deve estar dentro de uma função.
Portanto eu faria o nosso hello.sh
assim:
#!/usr/bin/env bash
hello() {
echo "Hello, ${1:-World}"
}
hello "$@"
Executando os testes:
$ bats test/hello_test.bats
hello_test.bats
✓ can run the script
✓ say Hello, World
✓ say hello to people
3 tests, 0 failures
Maravilha! Fizemos uma mudança e confirmamos que nada quebrou!
A outra prática que eu sempre adoto no meu código bash: todos argumentos devem ser armazenados em uma variável com um nome significativo.
Nosso hello.sh
então fica assim:
#!/usr/bin/env bash
hello() {
local name="${1:-World}"
echo "Hello, ${name}"
}
hello "$@"
Execute os testes novamente e observe que nada quebrou!
Acho que cabe um novo commit aqui:
git commit --all --message "Hello, meleu"
Agora estamos prontos para implementar ainda mais features no nosso hello-world…
Um hello-world poliglota
Uma características dos sistemas Unix-like (o que inclui o GNU/Linux e o MacOS) é que a variável de ambiente $LANG
é utilizada para determinar o idioma utilizado nas mensagens do sistema para o usuário.
Pra começar vamos avaliar o que temos nessa variável. Isso vai variar de como você configurou seu sistema. Se você usa português brasileiro, provavelmente vai ver algo assim:
$ echo $LANG
pt_BR.UTF-8
Se seu sistema está em inglês, talvez veja en_US.UTF-8
. Esse valor depois do ponto .
pode estar diferente.
Na real o que nos interessa aqui é apenas os dois primeiros caracteres. Dali podemos saber qual é o idioma configurado no sistema. Vamos nos aproveitar disso para criar um hello-world poliglota.
Detectaremos o idioma checando a variável $LANG
, e se não reconhecermos o conteúdo da variável, vamos cumprimentar em inglês mesmo, com Hello
.
Começando com o português.
Olá
Como vimos, se nosso sistema está em português, a variável $LANG
será algo tipo pt_BR.UTF-8
.
Uma técnica bem útil de shell em geral (não é nem específico de BATS) é que quando queremos passar um valor para uma variável de ambiente apenas para execução de um único comando, podemos usar a seguinte estratégia:
ENV_VAR=valor meu_commando
Vamos nos aproveitar dessa técnica na hora de escrever nosso teste, que ficará assim:
# ... conteúdo original do test/hello_test.bats
@test "say olá to people, in Portuguese" {
LANG=pt_BR.UTF-8 run hello.sh meleu
assert_output "Olá, meleu"
}
Executando o teste:
$ bats test/hello_test.bats
hello_test.bats
✓ can run the script
✓ say Hello, World
✓ say hello to people
✗ say olá to people
(from function `assert_output' in file test/test_helper/bats-assert/src/assert_output.bash, line 194,
in test file test/hello_test.bats, line 24)
`assert_output "Olá, meleu"' failed
-- output differs --
expected : Olá, meleu
actual : Hello, meleu
--
4 tests, 1 failure
Conforme esperado, todos os testes que já existiam continuam passando. Apenas o novo teste falhou, e ele já nos informa o que está errado: ele espera Olá, meleu
e nosso programa forneceu Hello, meleu
.
Vamos resolver isso com um if
no nosso src/hello.sh
:
#!/usr/bin/env bash
hello() {
local name="${1:-World}"
# comparando com 'pt*' para considerar qualquer valor
# que começa com 'pt' como sendo língua portuguesa.
if [[ "$LANG" == pt* ]]; then
echo "Olá, ${name}"
else
echo "Hello, ${name}"
fi
}
hello "$@"
Executamos o teste:
$ bats test/hello_test.bats
hello_test.bats
✓ can run the script
✓ say Hello, World
✓ say hello to people
✓ say olá to people, in Portuguese
4 tests, 0 failures
Refatorar?
Um teste acabou de passar, e isso me desperta o desejo de refatorar. Eu dou uma olhada no código e decido que ele está nos atendendo muito bem.
Nada de otimização prematura! O código está passando nos testes e está legível o suficiente. Se no futuro ele ficar mais complexo, podemos refatorar, mas desta vez vamos seguir sem mudanças.
Commit
Vamos commitar:
git commit --all --message "Olá, meleu"
Hola
Vamos agora cumprimentar como nuestros hermanos: em espanhol. Aqui na nossa vizinhança (ao redor do Brasil) temos muitos exemplos de países onde provavelmente os usuários terão um $LANG
assim (estou omitindo o .UTF-8
):
- Argentina:
es_AR
- Colômbia:
es_CO
- Paraguai:
es_PY
- Uruguai:
es_UY
Como podemos ver, todos começam com es
, o que significa espanhol.
Portanto vamos escrever nosso teste assim:
# ... conteúdo original do test/hello_test.bats
@test "say Hola to people, in Spanish" {
LANG=es_AR.UTF-8 run hello.sh meleu
assert_output "Hola, meleu"
}
Executamos o teste:
$ bats test/hello_test.bats
hello_test.bats
✓ can run the script
✓ say Hello, World
✓ say hello to people
✓ say olá to people, in Portuguese
✗ say Hola to people, in Spanish
(from function `assert_output' in file test/test_helper/bats-assert/src/assert_output.bash, line 194,
in test file test/hello_test.bats, line 31)
`assert_output "Hola, meleu"' failed
-- output differs --
expected : Hola, meleu
actual : Hello, meleu
--
5 tests, 1 failure
OK, o output não foi o que está sendo esperado pelo teste.
Vamos lá no src/hello.sh
e resolver assim:
#!/usr/bin/env bash
hello() {
local name="${1:-World}"
if [[ "$LANG" == pt* ]]; then
echo "Olá, ${name}"
elif [[ "$LANG" == es* ]]; then
echo "Hola, ${name}"
else
echo "Hello, ${name}"
fi
}
hello "$@"
Executamos os testes:
$ bats test/hello_test.bats
hello_test.bats
✓ can run the script
✓ say Hello, World
✓ say hello to people
✓ say olá to people, in Portuguese
✓ say Hola to people, in Spanish
5 tests, 0 failures
Beleza, tudo passando.
Refatoração
Será que devemos refatorar? Estou olhando para aquela estrutura if-elif
sempre comparando com a $LANG
e percebo que ela está implorando pra virar um case
. Portanto vamos refatorar sim!
O src/hello.sh
vai ficar assim:
#!/usr/bin/env bash
hello() {
local name="${1:-World}"
case "$LANG" in
pt*) echo "Olá, ${name}" ;;
es*) echo "Hola, ${name}" ;;
*) echo "Hello, ${name}" ;;
esac
}
hello "$@"
Execute os testes e observe que tá tudo passando! 🤓
Commit
Vamos commitar e passar para o próximo idioma…
git commit --all --message "Hola, meleu"
Bonjour
A essa altura do campeonato adicionar um novo idioma ficou super simples: basta descobrirmos o “código” do idioma e como dizer “Hello” em tal idioma.
Agora queremos cumprimentar em francês. O código é fr
e o cumprimento é “Bounjour”.
Eu sei que você tá louco pra ir direto lá no hello.sh
e adicionar o caso do francês. Resista a essa tentação! Lembre-se: primeiro o teste!
# ... conteúdo original do test/hello_test.bats
@test "say Bonjour to people, in French" {
LANG=fr_FR.UTF-8 run hello.sh meleu
assert_output "Bonjour, meleu"
}
Execute o teste:
$ bats test/hello_test.bats
hello_test.bats
✓ can run the script
✓ say Hello, World
✓ say hello to people
✓ say olá to people, in Portuguese
✓ say Hola to people, in Spanish
✗ say Bonjour to people, in French
(from function `assert_output' in file test/test_helper/bats-assert/src/assert_output.bash, line 194,
in test file test/hello_test.bats, line 36)
`assert_output "Bonjour, meleu"' failed
-- output differs --
expected : Bonjour, meleu
actual : Hello, meleu
--
6 tests, 1 failure
Agora que vimos que o nosso teste está falhando podemos alterar nosso código para passar no teste.
Nosso src/hello.sh
fica assim:
#!/usr/bin/env bash
hello() {
local name="${1:-World}"
case "$LANG" in
pt*) echo "Olá, ${name}" ;;
es*) echo "Hola, ${name}" ;;
fr*) echo "Bonjour, ${name}" ;;
*) echo "Hello, ${name}" ;;
esac
}
hello "$@"
Executando os testes:
$ bats test/hello_test.bats
hello_test.bats
✓ can run the script
✓ say Hello, World
✓ say hello to people
✓ say olá to people, in Portuguese
✓ say Hola to people, in Spanish
✓ say Bonjour to people, in French
6 tests, 0 failures
Topzera da balada! Todos os testes passando!
Refatoração
Está tudo certinho, os testes estão passando e talz… Mas aquele case
está começando a me incomodar…
Eu acho que uma função hello-world deveria ser mais simples de ler do que ter essa pequena maçaroca de case
ali.
💡 Tive uma ideia: vamos criar uma função chamada greeting()
que será responsável por imprimir o cumprimento no idioma correto!
Assim nosso hello-world vai voltar a ser tão simples quanto um echo "$(greeting), ${name}"
Portanto o src/hello.sh
fica assim:
#!/usr/bin/env bash
hello() {
local name="${1:-World}"
echo "$(greeting), ${name}"
}
greeting() {
case "$LANG" in
pt*) echo "Olá" ;;
es*) echo "Hola" ;;
fr*) echo "Bonjour" ;;
*) echo "Hello" ;;
esac
}
hello "$@"
Vamos executar os testes:
$ bats test/hello_test.bats
hello_test.bats
✓ can run the script
✓ say Hello, World
✓ say hello to people
✓ say olá to people, in Portuguese
✓ say Hola to people, in Spanish
✓ say Bonjour to people, in French
6 tests, 0 failures
Que delicinha! Tudo passando!
Perceba que eu não precisei ir na linha de comando ficar testando cada um dos idiomas pra ver se algo quebrou. Bastou executar a bateria de testes pronto! Sinta o conforto e a gostosura disso! 🥰
Commit
Vamos pra mais um commitzinho:
git commit --all --message "Hola, meleu"
Hallo, Ciao, Konnichiwa
Você já entendeu o espírito da coisa, né? Então fica aí como um pequeno exercício pra você treinar esse ciclo:
- Escreva um teste para um novo idioma
- Execute o teste e observe a mensagem de falha
- Adicione o novo cumprimento lá no
case
- Certifique-se que o teste passou
- Se encontrar uma maneira melhor de organizar o código, refatore.
E se quiser refatorar mais seguindo adiante com esse over-engineering do nosso hello-world, manda brasa. Você pode, por exemplo, tentar usar um array associativo com os possíveis cumprimentos…
Nesse processo você vai ver que reconfortante que é ter testes automatizados e não precisar testar “na mão”.
Lembre-se, durante a refatoração o flow é esse:
- faz uma alteração no código
- já roda os testes em seguida
- se quebrar algum teste
- desfazer a alteração
- escolher uma melhor e voltar pro passo 1
Recapitulando
Vamos listar os principais pontos sobre cada tema abordado no artigo.
BATS
- Extensão do arquivo:
.bats
- Função
setup
é executada antes de cada teste. - Básico de um
setup
:- carrega
bats-support
- carrega
bats-assert
- define um
$PATH
pra chamar o nosso programa facilmente
- carrega
setup() {
load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'
PATH="${BATS_TEST_DIRNAME}/../src:${PATH}"
}
- Formato básico de um teste:
@test "descrição significativa do teste" {
# chame o programa com run
run my_program
# valide a saída do programa com assert_output
assert_ouput "saída esperada"
}
TDD
- Escreva o teste antes de ter o código que será testado.
- Você PRECISA ver seu teste falhando
- para que saibamos que temos testes relevantes;
- notar que ele produz descrições de falha que são fáceis de entender e dão dicas do que devemos fazer.
- Escreva a menor quantidade possível de código para fazer seu teste passar.
- Por fim refatore, com a segurança dos seus testes automatizados.
Palavras finais
Claro que um hello-world é extremamente trivial comparado com problemas da “vida real”. O objetivo aqui foi apenas dar uma introduzida no TDD e como usar o BATS pra isso. Escolhi um problema simples exatamente para que pudéssemos focar nestes temas.
Espero que artigo tenha deixado você atiçado para se aprofundar no tema
Dar uma lida no README do bats-assert pode ser bem legal para você ter uma noção de outras asserções que você pode usar.
Outra coisa importante de se ter em mente: programas em bash geralmente manipulam arquivos. Portanto uma lida no README do bats-file também será muito útil.
Deixe aí nos comentários se você gostaria de ver mais artigos nesse estilo por aqui.
Referências
- Learn Go with Tests - livro que ensina Golang com TDD. Foi de onde peguei a inspiração para usar esse “hello-world poliglota”.
- BATS Tutorial - o tutorial oficial do BATS.