Testes Bash
Frameworks e padrões de teste para scripts shell, com foco em shunit2 e shellspec.
Seleção de Framework
| Framework | Pontos Fortes | Melhor Para |
|---|---|---|
| shunit2 | Estilo xUnit, simples, portátil | Testes unitários, teste de funções |
| shellspec | Estilo BDD, moderno, extenso | Especificações de comportamento, projetos completos |
shunit2
Instalação
# Baixar diretamente
curl -L https://github.com/kward/shunit2/raw/master/shunit2 -o shunit2
# Ou via gerenciador de pacotes
apt install shunit2
brew install shunit2
Estrutura Básica de Teste
#!/usr/bin/env bash
# Incluir o script sendo testado
source ./my_script.sh
# Funções de teste começam com test
test_addition() {
result=$(add 2 3)
assertEquals "2 + 3 deve ser igual a 5" "5" "$result"
}
test_string_output() {
result=$(greet "World")
assertEquals "Hello, World" "$result"
}
test_file_creation() {
create_temp_file
assertTrue "O arquivo temporário deve existir" "[ -f /tmp/testfile ]"
}
test_exit_code() {
validate_input "valid"
assertEquals "Deve retornar 0 para entrada válida" 0 $?
}
# Setup e teardown
oneTimeSetUp() {
# Executado uma vez antes de todos os testes
export TEST_DIR=$(mktemp -d)
}
oneTimeTearDown() {
# Executado uma vez após todos os testes
rm -rf "$TEST_DIR"
}
setUp() {
# Executado antes de cada teste
cd "$TEST_DIR"
}
tearDown() {
# Executado após cada teste
rm -f "$TEST_DIR"/*
}
# Carregar shunit2
source shunit2
Asserções do shunit2
# Igualdade
assertEquals [mensagem] esperado atual
assertNotEquals [mensagem] inesperado atual
# Nulo/Vazio
assertNull [mensagem] valor
assertNotNull [mensagem] valor
# Booleano/Status
assertTrue [mensagem] condição
assertFalse [mensagem] condição
# Mesma referência (comparação de strings)
assertSame [mensagem] esperado atual
assertNotSame [mensagem] inesperado atual
# Contém (em string)
assertContains [mensagem] contêiner conteúdo
# Status de saída
assertEquals 0 $?
Testando Funções Isoladamente
#!/usr/bin/env bash
# test_my_functions.sh
source ./my_functions.sh
test_calculate_sum_empty() {
result=$(calculate_sum)
assertEquals "Soma vazia deve ser 0" "0" "$result"
}
test_calculate_sum_single() {
result=$(calculate_sum 5)
assertEquals "5" "$result"
}
test_calculate_sum_multiple() {
result=$(calculate_sum 1 2 3 4 5)
assertEquals "15" "$result"
}
test_validate_email_valid() {
validate_email "[email protected]"
assertTrue "Email válido deve passar" $?
}
test_validate_email_invalid() {
validate_email "not-an-email"
assertFalse "Email inválido deve falhar" $?
}
source shunit2
shellspec
Instalação
# Via curl
curl -fsSL https://git.io/shellspec | sh
# Via Homebrew
brew install shellspec
# Via gerenciadores de pacotes
apt install shellspec
Estrutura de Diretórios
project/
├── lib/
│ └── functions.sh
├── spec/
│ ├── spec_helper.sh
│ ├── functions_spec.sh
│ └── support/
│ └── fixtures/
└── .shellspec
Configuração (.shellspec)
--require spec_helper
--format documentation
--color
Estrutura Básica de Especificação
Matchers do shellspec
# Matchers de saída
The output deve eq "correspondência exata"
The output deve include "parcial"
The output deve start with "prefixo"
The output deve end with "sufixo"
The output deve match pattern "*glob*"
The output deve be blank
# Matchers de status
The status deve be success # saída 0
The status deve be failure # saída não zero
The status deve eq 1 # código específico
# Matchers de variável
The variable VAR deve eq "valor"
The variable VAR deve be defined
The variable VAR deve be undefined
# Matchers de arquivo
The file "caminho" deve be exist
The file "caminho" deve be file
The file "caminho" deve be directory
The file "caminho" deve be readable
# Matchers de caminho
The path "arquivo.txt" deve be exist
Mocking no shellspec
Describe 'função de deploy'
# Mock de comando externo
curl() {
echo "resposta mockada"
return 0
}
It 'chama a API endpoint'
When call deploy "servidor"
The output deve include "resposta mockada"
End
End
Describe 'operações de arquivo'
# Mock com sobrescrita de função
Mock rm
echo "rm chamado com: $*"
End
It 'tenta remover o arquivo'
When call cleanup_temp
The output deve include "rm chamado com"
End
End
Spec Helper
# spec/spec_helper.sh
# Carregar funções comuns
spec_helper_precheck() {
minimum_version "0.28.0"
}
spec_helper_loaded() {
# Configurar ambiente de teste
export TEST_MODE=true
}
spec_helper_configure() {
# Importar funções do projeto
import 'lib/functions.sh'
}
Padrões de Teste
Testando Códigos de Saída
# shunit2
test_success_exit() {
run_command "entrada_válida"
assertEquals 0 $?
}
test_error_exit() {
run_command "entrada_inválida"
assertEquals 1 $?
}
# shellspec
It 'sai com 0 em caso de sucesso'
When call run_command "entrada_válida"
The status deve eq 0
End
Testando stdout e stderr
# shunit2
test_stdout() {
result=$(my_function 2>/dev/null)
assertEquals "saída esperada" "$result"
}
test_stderr() {
error=$(my_function 2>&1 >/dev/null)
assertContains "$error" "mensagem de erro"
}
# shellspec
It 'saída para stdout'
When call my_function
The stdout deve eq "saída esperada"
End
It 'saída de erro para stderr'
When call my_function
The stderr deve include "mensagem de erro"
End
Testando com Fixtures
# Configurar fixtures de teste
setUp() {
TEST_DIR=$(mktemp -d)
cat > "$TEST_DIR/config.json" <<EOF
{
"key": "value"
}
EOF
}
tearDown() {
rm -rf "$TEST_DIR"
}
test_config_parsing() {
result=$(parse_config "$TEST_DIR/config.json")
assertEquals "value" "$result"
}
Testando Funções Interativas
# Fornecer entrada via heredoc
test_interactive() {
result=$(my_prompt <<EOF
sim
EOF
)
assertEquals "confirmado" "$result"
}
# Ou usar printf
test_with_input() {
result=$(printf 'sim\n' | my_prompt)
assertEquals "confirmado" "$result"
}
Executando Testes
# shunit2
./test_script.sh
# shellspec
shellspec # Executar todas as especificações
shellspec spec/file_spec.sh # Executar especificação específica
shellspec --format tap # Saída TAP
shellspec --jobs 4 # Execução paralela
Melhores Práticas
- Uma asserção por teste quando prático
- Nomes de teste descritivos explicando o que é testado
- Isolar testes - sem dependências entre testes
- Testar casos de borda - entrada vazia, caracteres especiais, dados grandes
- Limpar recursos no tearDown
- Mockar comandos externos - não teste curl, teste sua lógica
- Testar códigos de saída não apenas a saída