Testing scripts Bash com Bats ou Shunit2
1. Por que testar scripts Bash? Motivação e desafios
1.1. Fragilidade de scripts sem testes: erros silenciosos e efeitos colaterais
Scripts Bash frequentemente executam operações críticas como manipulação de arquivos, backups ou deploys. Sem testes, um simples erro de digitação pode deletar arquivos importantes ou corromper dados. Erros silenciosos ocorrem quando comandos falham mas o script continua executando, ignorando o código de retorno ($?). Efeitos colaterais em variáveis globais ou no sistema de arquivos podem causar comportamentos imprevisíveis.
1.2. Dificuldades específicas: variáveis de ambiente, comandos externos, $?
Testar Bash apresenta desafios únicos:
- Dependência de variáveis de ambiente que podem não estar definidas
- Comandos externos (curl, rm, sed) que alteram o sistema real
- Códigos de retorno que precisam ser verificados após cada operação
- Subshells e pipes que criam escopos de variáveis diferentes
1.3. Benefícios: confiabilidade em automações, CI/CD e manutenção colaborativa
Scripts testados oferecem confiança em pipelines de CI/CD, facilitam a colaboração em equipe e previnem regressões. Um conjunto de testes bem escrito documenta o comportamento esperado e acelera a manutenção.
2. Panorama das ferramentas: Bats vs Shunit2
2.1. Bats (Bash Automated Testing System): sintaxe simplificada e integração TAP
Bats usa uma sintaxe limpa com @test e suporte nativo ao formato TAP (Test Anything Protocol). Suas asserções como assert_success e assert_output tornam os testes legíveis e fáceis de escrever.
2.2. Shunit2: inspirado em JUnit, mais verboso e com asserções explícitas
Shunit2 segue o estilo xUnit com funções testSomething() e asserções como assertEquals. É mais verboso mas oferece controle granular sobre cada verificação.
2.3. Critérios de escolha: complexidade do script, time de desenvolvimento, portabilidade
Bats é ideal para equipes que preferem sintaxe moderna e integração TAP. Shunit2 funciona melhor em ambientes que já usam padrões xUnit ou necessitam de portabilidade máxima (um único arquivo .sh).
3. Configuração e primeiros passos com Bats
3.1. Instalação e estrutura básica
# Instalação via apt (Ubuntu/Debian)
sudo apt install bats
# Via brew (macOS)
brew install bats-core
# Estrutura de arquivos
projeto/
├── script.sh
└── test/
└── script.bats
3.2. Sintaxe fundamental
#!/usr/bin/env bats
# test/validacao.bats
setup() {
source ../script.sh
}
@test "valida_email retorna sucesso para email valido" {
run valida_email "usuario@exemplo.com"
assert_success
assert_output "Email valido"
}
@test "valida_email retorna erro para email invalido" {
run valida_email "invalido"
assert_failure
assert_output --partial "invalido"
}
3.3. Exemplo prático: testando função de validação
# script.sh
valida_email() {
local email="$1"
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "Email valido"
return 0
else
echo "Email invalido: $email"
return 1
fi
}
# test/valida_email.bats
@test "rejeita email sem arroba" {
run valida_email "usuariogmail.com"
assert_failure
assert_output --partial "invalido"
}
@test "aceita email com subdominio" {
run valida_email "user@sub.dominio.com.br"
assert_success
}
4. Configuração e primeiros passos com Shunit2
4.1. Instalação e estrutura
# Download do único script
curl -L https://raw.githubusercontent.com/kward/shunit2/master/shunit2 -o shunit2
chmod +x shunit2
# Estrutura
projeto/
├── backup.sh
└── test/
├── test_backup.sh
└── shunit2
4.2. Asserções principais
#!/bin/bash
# test/test_backup.sh
source ../backup.sh
source ./shunit2
testCriaBackupComSucesso() {
local dir_teste=$(mktemp -d)
export BACKUP_DIR="$dir_teste/backup"
export ORIGEM="$dir_teste/origem"
mkdir -p "$ORIGEM"
echo "conteudo" > "$ORIGEM/arquivo.txt"
cria_backup "$ORIGEM"
assertTrue "Backup deve existir" "[ -d '$BACKUP_DIR' ]"
assertContains "$(ls $BACKUP_DIR)" "arquivo.txt"
rm -rf "$dir_teste"
}
testFalhaQuandoOrigemInexistente() {
export BACKUP_DIR="/tmp/test_backup"
local resultado=$(cria_backup "/caminho/inexistente" 2>&1)
assertEquals 1 $?
assertContains "$resultado" "Erro"
}
# Executar testes
. ./shunit2
4.3. Exemplo prático: testando script de backup
# backup.sh
cria_backup() {
local origem="$1"
if [ ! -d "$origem" ]; then
echo "Erro: diretorio $origem nao existe" >&2
return 1
fi
if [ -z "$BACKUP_DIR" ]; then
echo "Erro: BACKUP_DIR nao definido" >&2
return 1
fi
mkdir -p "$BACKUP_DIR"
cp -r "$origem"/* "$BACKUP_DIR/"
echo "Backup concluido em $BACKUP_DIR"
}
5. Técnicas avançadas de teste
5.1. Mocking de comandos externos
# Com Bats
mock_curl() {
echo "resposta mockada"
}
@test "usa mock para curl" {
function curl() { mock_curl "$@"; }
export -f curl
run meu_script_que_usa_curl
assert_success
}
5.2. Isolamento de ambiente
setup() {
export TEST_DIR=$(mktemp -d)
cd "$TEST_DIR"
}
teardown() {
rm -rf "$TEST_DIR"
}
@test "trabalha em diretorio temporario" {
touch "arquivo_teste.txt"
run processa_arquivos
[ -f "resultado.txt" ]
}
5.3. Teste de saída e código de retorno
@test "verifica pipe e codigo de retorno" {
run bash -c 'echo "dado" | grep "dado"'
assert_success
assert_output "dado"
run bash -c 'echo "dado" | grep "nao_existe"'
assert_failure
}
6. Organização e boas práticas
6.1. Estrutura de diretórios
projeto/
├── src/
│ └── utils.sh
├── test/
│ ├── bats/
│ │ └── test_utils.bats
│ ├── fixtures/
│ │ ├── entrada_valida.txt
│ │ └── config_padrao.conf
│ └── helpers/
│ └── mock_comandos.sh
└── Makefile
6.2. Setup e teardown
setup() {
export VAR_AMBIENTE="teste"
DIR_TRABALHO=$(mktemp -d)
}
teardown() {
unset VAR_AMBIENTE
rm -rf "$DIR_TRABALHO"
}
6.3. Nomenclatura e documentação
@test "CT001: validacao de email - formato padrao" {
# Cenário: email com formato usuario@dominio.com
# Resultado esperado: sucesso
run valida_email "teste@exemplo.com"
assert_success
}
7. Integração com CI/CD e linting
7.1. GitHub Actions
name: Testes Bash
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Instalar dependencias
run: sudo apt install bats shellcheck
- name: Executar lint
run: shellcheck src/*.sh
- name: Executar testes
run: bats test/
7.2. Combinando com ShellCheck
# Makefile
.PHONY: test lint
lint:
shellcheck src/*.sh
test: lint
bats test/
7.3. Geração de relatórios
# Gerar relatório TAP
bats --formatter tap test/ > resultados.tap
# Gerar JUnit XML (com bats-extra)
bats --formatter junit test/ > resultados.xml
8. Erros comuns e depuração de testes
8.1. Falsos positivos por estado global não limpo
# ERRADO: variável persiste entre testes
setup() {
export CONTADOR=0
}
@test "incrementa contador" {
CONTADOR=$((CONTADOR + 1))
[ "$CONTADOR" -eq 1 ]
}
# CORRETO: reinicializar no setup
setup() {
export CONTADOR=0
}
8.2. Problemas de permissão e caminhos relativos
# Sempre usar caminhos absolutos ou relativos ao diretório do teste
setup() {
cd "$(dirname "$BATS_TEST_FILENAME")"
source "../src/script.sh"
}
8.3. Debug com logs detalhados
# Ativar debug
export BATS_TRAP_DEBUG=1
# Logs manuais
@test "debug detalhado" {
echo "# DEBUG: variavel TEMP=$TEMP" >&3
run meu_comando
echo "# DEBUG: saida=$output" >&3
}
Referências
- Bats-core Documentation — Documentação oficial do Bats com guia de instalação, sintaxe e exemplos avançados
- Shunit2 GitHub Repository — Repositório oficial do Shunit2 com documentação, exemplos e guia de contribuição
- ShellCheck - Shell Script Analysis Tool — Ferramenta de linting para Bash que complementa os testes automatizados
- Testing Bash Scripts with Bats - DevOps University — Tutorial prático abordando instalação, mocking e integração CI/CD
- Bash Automated Testing System - Tutorial by Opensource.com — Artigo introdutório com exemplos de testes para scripts do mundo real
- Shunit2 Testing Framework - Linux Journal — Guia completo sobre uso do Shunit2 em projetos Bash
- Mocking in Bash Tests - Software Engineering Stack Exchange — Discussão técnica sobre técnicas de mocking para testes de shell script