0Pricing
DevOps Bootcamp · Aula

Dispositivos de teste, ambientes temporários e cobertura

Crie dispositivos de teste isolados e meça quais ramificações dos scripts seus testes realmente exercitam.

Dispositivos de teste, ambientes temporários e cobertura é uma aula grátis de DevOps Bootcamp no CoddyKit. Esta é a aula 3 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de DevOps Bootcamp, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de DevOps Bootcamp inclui 4 aulas no total.

Por Que Fixtures e Isolamento São Importantes

Ao testar scripts Bash, o maior risco são os efeitos colaterais: seus testes podem modificar acidentalmente arquivos, bancos de dados ou o estado real do sistema. Um teste que passa na sua máquina, mas corrompe dados de produção, é pior do que não ter teste algum.

A solução são as fixtures de teste — ambientes controlados e descartáveis que reproduzem condições reais sem tocar em nada real. Boas fixtures oferecem:

  • Reprodutibilidade — os testes produzem o mesmo resultado em todas as execuções
  • Isolamento — os testes não interferem uns nos outros nem no sistema hospedeiro
  • Segurança — operações destrutivas afetam apenas dados descartáveis
  • Velocidade — nenhuma chamada de rede nem E/S pesada, a menos que seja absolutamente necessário

Nos testes de Bash, as fixtures normalmente são diretórios temporários preenchidos com arquivos conhecidos, executáveis simulados colocados no início de $PATH e variáveis de ambiente limitadas ao processo do teste.

Criando e Limpando Diretórios Temporários

O padrão usual para um diretório temporário por teste usa mktemp -d, que cria um diretório exclusivo em /tmp e exibe seu caminho. Você armazena esse caminho e registra um trap para removê-lo automaticamente quando o shell terminar — mesmo em caso de falha.

Este padrão de duas linhas deve aparecer em every arquivo de teste que interaja com o sistema de arquivos:

#!/usr/bin/env bash
set -euo pipefail

# Create an isolated temp directory
TMPDIR=$(mktemp -d)
# Always clean up, even if the script exits early or errors out
trap 'rm -rf "$TMPDIR"' EXIT

echo "Working in: $TMPDIR"

# Simulate creating fixture files
mkdir -p "$TMPDIR/project/{src,tests,logs}"
echo 'version=1.2.3' > "$TMPDIR/project/.env"
echo 'Hello fixture' > "$TMPDIR/project/src/main.sh"

ls -R "$TMPDIR/project"
echo 'Temp dir will be removed automatically on exit'

Estruturando uma Árvore de Diretórios de Fixture

Uma fixture bem estruturada reproduz a organização de diretórios que o script em teste realmente espera. Pense nela como uma pequena raiz de projeto falsa. A função de configuração da fixture cria essa estrutura antes de cada teste, e a limpeza a remove.

Práticas importantes:

  • Use uma função setup() que sua estrutura de testes chame antes de cada teste
  • Use uma função teardown() ou cleanup() executada após cada teste, mesmo em caso de falha
  • Mantenha os arquivos da fixture mínimos — apenas o que o script realmente lê
  • Dê nomes descritivos aos arquivos da fixture para facilitar o diagnóstico de falhas
#!/usr/bin/env bash
# fixture_helpers.bash — source this from your test files

FIXTURE_ROOT=''

setup_fixture() {
  FIXTURE_ROOT=$(mktemp -d)
  # Build the directory tree the deploy script expects
  mkdir -p "$FIXTURE_ROOT"/{dist,config,logs}
  echo '{"version":"2.0"}' > "$FIXTURE_ROOT/config/app.json"
  echo 'console.log("app")' > "$FIXTURE_ROOT/dist/index.js"
  touch "$FIXTURE_ROOT/logs/.gitkeep"
  export FIXTURE_ROOT
  echo "[setup] Fixture ready at $FIXTURE_ROOT"
}

teardown_fixture() {
  if [[ -n "$FIXTURE_ROOT" && -d "$FIXTURE_ROOT" ]]; then
    rm -rf "$FIXTURE_ROOT"
    echo '[teardown] Fixture removed'
  fi
}

# Self-test
setup_fixture
ls "$FIXTURE_ROOT"
teardown_fixture

Simulando Executáveis com um PATH Falso

Muitos scripts Bash chamam ferramentas externas, como curl, aws, docker ou git. Nos testes, você não quer acessar serviços reais; por isso, substitui essas ferramentas por executáveis falsos.

A técnica é simples:

  1. Crie um diretório temporário bin/ dentro da sua fixture
  2. Escreva nele pequenos scripts de shell com os mesmos nomes das ferramentas reais
  3. Coloque esse diretório no início de $PATH antes de chamar o script em teste

Como $PATH é pesquisado da esquerda para a direita, o executável falso vence. O binário real nunca é chamado.

#!/usr/bin/env bash
set -euo pipefail

FIXTURE=$(mktemp -d)
trap 'rm -rf "$FIXTURE"' EXIT

# Create a fake 'curl' that records calls and returns canned data
FAKE_BIN="$FIXTURE/bin"
mkdir -p "$FAKE_BIN"

cat > "$FAKE_BIN/curl" << 'SCRIPT'
#!/usr/bin/env bash
# Log every argument for later inspection
echo "curl $*" >> "$FIXTURE_BIN_LOG"
# Return a canned HTTP 200 response body
echo '{"status":"ok","id":42}'
SCRIPT
chmod +x "$FAKE_BIN/curl"

# Export the log path so the fake can find it
export FIXTURE_BIN_LOG="$FIXTURE/curl_calls.log"

# Prepend fake bin directory to PATH
export PATH="$FAKE_BIN:$PATH"

# Now any call to 'curl' hits our fake
curl -s https://api.example.com/health
curl -X POST https://api.example.com/deploy

echo '--- Recorded curl calls ---'
cat "$FIXTURE_BIN_LOG"

Capturando e Verificando a Saída de Comandos

Uma fixture só é útil se você puder verificar o que seu script fez. Os padrões usuais são:

  • Capture stdout/stderr em variáveis com $() ou substituição de processos
  • Inspecione os arquivos de registro gravados pelos binários falsos
  • Verifique explicitamente os códigos de saída com $? ou lógica condicional
  • Confirme se determinados arquivos foram criados, modificados ou permaneceram intactos

Escrever pequenos auxiliares de verificação, focados e específicos, torna seus testes mais legíveis e fornece mensagens precisas quando algo dá errado.

#!/usr/bin/env bash
set -euo pipefail

# Minimal assertion helpers
assert_eq() {
  local desc="$1" expected="$2" actual="$3"
  if [[ "$expected" == "$actual" ]]; then
    echo "PASS: $desc"
  else
    echo "FAIL: $desc"
    echo "  expected: $expected"
    echo "  actual:   $actual"
    return 1
  fi
}

assert_file_exists() {
  local desc="$1" file="$2"
  if [[ -f "$file" ]]; then
    echo "PASS: $desc"
  else
    echo "FAIL: $desc — file not found: $file"
    return 1
  fi
}

assert_contains() {
  local desc="$1" needle="$2" haystack="$3"
  if [[ "$haystack" == *"$needle"* ]]; then
    echo "PASS: $desc"
  else
    echo "FAIL: $desc — '$needle' not found in output"
    return 1
  fi
}

# Demo usage
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
echo 'hello world' > "$TMPDIR/greeting.txt"
OUT=$(cat "$TMPDIR/greeting.txt")
assert_eq   'file content matches'     'hello world' "$OUT"
assert_file_exists 'greeting file created' "$TMPDIR/greeting.txt"
assert_contains    'output has hello'     'hello'       "$OUT"

Limitando o Escopo de Variáveis de Ambiente nos Testes

Os scripts costumam ler variáveis de ambiente, como $HOME, $CONFIG_PATH ou $DATABASE_URL. Nos testes, você precisa substituí-las sem poluir o ambiente real.

A abordagem mais segura é executar o script em teste em um subprocesso contendo apenas as variáveis que você definir explicitamente. O comando env permite reduzir o ambiente e readicionar somente o necessário:

  • env -i VAR=val ./script.sh — ambiente completamente limpo
  • (export VAR=val; ./script.sh) — o subprocesso herda o ambiente do processo pai e acrescenta suas substituições

Usar subprocessos também significa que, se o script alterar $IFS, $PWD ou outro estado global, essas alterações nunca escaparão de volta para o executor dos testes.

#!/usr/bin/env bash
set -euo pipefail

FIXTURE=$(mktemp -d)
trap 'rm -rf "$FIXTURE"' EXIT

# Write a tiny script under test that reads env vars
cat > "$FIXTURE/deploy.sh" << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
ENV_NAME=${DEPLOY_ENV:-unknown}
CFG=${CONFIG_DIR:-/etc/app}
echo "Deploying to $ENV_NAME using config from $CFG"
SCRIPT
chmod +x "$FIXTURE/deploy.sh"

echo '--- Test 1: staging env ---'
# Run in a clean subshell with explicit vars
(
  export DEPLOY_ENV=staging
  export CONFIG_DIR="$FIXTURE/config"
  "$FIXTURE/deploy.sh"
)

echo '--- Test 2: production env ---'
(
  export DEPLOY_ENV=production
  export CONFIG_DIR=/etc/prod-config
  "$FIXTURE/deploy.sh"
)

echo '--- Test 3: defaults ---'
# No env vars set — script should use its own defaults
env -i PATH="$PATH" "$FIXTURE/deploy.sh"

Introdução ao kcov para Cobertura de Código Bash

Cobertura responde à pergunta: quais linhas (e ramificações) do meu script os testes realmente executaram? Um número alto de cobertura não garante correção, mas um número baixo revela caminhos não testados que provavelmente contêm erros.

A principal ferramenta para cobertura de código Bash é o kcov. Ela funciona instrumentando o script no nível do sistema operacional usando PTRACE (Linux) ou dtrace (macOS), portanto não exige alterações no código-fonte. Ela produz um relatório HTML mostrando linhas vermelhas (não cobertas) e verdes (cobertas).

Uso básico:

  • kcov --include-path=./src coverage-out/ ./src/myscript.sh
  • Abra coverage-out/index.html em um navegador para inspecionar os resultados
  • Na integração contínua, analise coverage-out/myscript.sh/coverage.json para obter uma porcentagem legível por máquina

Observação: o kcov precisa ser instalado separadamente (brew install kcov no macOS, apt install kcov no Ubuntu 20.04+).

Executando o kcov em um Script Real

Aqui está um exemplo completo que mostra um script pronto para implantação, um teste que o exercita e a chamada do kcov que mede a cobertura. Observe que o diretório de saída é específico de cada teste, para que você possa combinar os resultados de várias execuções de teste posteriormente.

#!/usr/bin/env bash
# This demo shows the *structure* of a kcov workflow.
# It will not run kcov itself (not guaranteed to be installed),
# but the script under test and test runner are fully runnable.
set -euo pipefail

FIXTURE=$(mktemp -d)
trap 'rm -rf "$FIXTURE"' EXIT

# 1. Script under test
cat > "$FIXTURE/process.sh" << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
INPUT="$1"
if [[ ! -f "$INPUT" ]]; then
  echo "ERROR: file not found" >&2
  exit 1
fi
LINE_COUNT=$(wc -l < "$INPUT")
if (( LINE_COUNT == 0 )); then
  echo "WARNING: file is empty"
else
  echo "Processed $LINE_COUNT lines"
fi
SCRIPT
chmod +x "$FIXTURE/process.sh"

# 2. Happy path test (exercises line 6 and 11)
echo -e 'one\ntwo\nthree' > "$FIXTURE/data.txt"
OUT=$("$FIXTURE/process.sh" "$FIXTURE/data.txt")
echo "Happy path: $OUT"

# 3. Empty file test (exercises line 9)
touch "$FIXTURE/empty.txt"
OUT=$("$FIXTURE/process.sh" "$FIXTURE/empty.txt")
echo "Empty file: $OUT"

# 4. Missing file test (exercises line 5-6)
if ! OUT=$("$FIXTURE/process.sh" "$FIXTURE/missing.txt" 2>&1); then
  echo "Missing file (expected error): $OUT"
fi

# To measure coverage, wrap each call with kcov:
# kcov --include-path="$FIXTURE" "$FIXTURE/cov/happy" "$FIXTURE/process.sh" ...
# kcov --merge "$FIXTURE/cov/all" "$FIXTURE/cov/happy" "$FIXTURE/cov/empty"

Combinando a Cobertura de Várias Execuções de Teste

Um único teste raramente cobre todas as ramificações. Você executa vários testes — cada um em seu próprio diretório de saída de cobertura — e depois os combina. A opção --merge do kcov combina várias execuções em um único relatório unificado.

O padrão típico em um pipeline de integração contínua:

  • Execute o teste A → saída em cov/test_a/
  • Execute o teste B → saída em cov/test_b/
  • Combine → kcov --merge cov/all/ cov/test_a/ cov/test_b/
  • Analise cov/all/<script>/coverage.json para obter a porcentagem final

Você também pode impor um limite mínimo e fazer a compilação da integração contínua falhar se a cobertura ficar abaixo dele:

#!/usr/bin/env bash
# extract_coverage.sh — parse kcov JSON and fail below threshold
set -euo pipefail

MIN_COVERAGE=80   # percent
COV_JSON="${1:-coverage-out/myscript.sh/coverage.json}"

if [[ ! -f "$COV_JSON" ]]; then
  echo "ERROR: coverage JSON not found at $COV_JSON" >&2
  exit 1
fi

# kcov JSON contains a key like: "percent_covered": "87.50"
PERCENT=$(grep -oP '"percent_covered":\s*"\K[0-9.]+' "$COV_JSON")
PERCENT_INT=${PERCENT%%.*}   # truncate decimal

echo "Coverage: ${PERCENT}%  (minimum: ${MIN_COVERAGE}%)"

if (( PERCENT_INT < MIN_COVERAGE )); then
  echo "FAIL: coverage ${PERCENT}% is below threshold ${MIN_COVERAGE}%" >&2
  exit 1
fi

echo "PASS: coverage threshold met"

Cobertura de Ramificações versus Cobertura de Linhas

Há duas métricas principais de cobertura que você encontrará:

  • Cobertura de linhas — esta linha foi executada? É fácil manipulá-la: um único teste pode passar por muitas linhas e não cobrir caminhos condicionais importantes.
  • Cobertura de ramificações — cada ramificação de todo if, case e &&/|| foi percorrida? É um indicador muito mais forte. Exige testes tanto para o lado verdadeiro quanto para o falso de cada decisão.

O kcov informa ambas. A ideia principal é: 100% de cobertura de linhas não implica 100% de cobertura de ramificações. Considere este script — um único teste com um arquivo não vazio cobrirá todas as linhas, mas a ramificação do arquivo vazio (linha 9 abaixo) nunca será alcançada:

#!/usr/bin/env bash
# Illustrates line vs branch coverage gap
set -euo pipefail

check_file() {
  local f="$1"
  if [[ -f "$f" ]]; then        # branch A (true) OR branch B (false)
    local lines
    lines=$(wc -l < "$f")
    if (( lines > 0 )); then    # branch C (true) OR branch D (false)
      echo "File has $lines lines"
    else
      echo "File is empty"     # branch D — unreached if only tested with non-empty file
    fi
  else
    echo "File missing"        # branch B — unreached if only tested with existing file
  fi
}

# Only one test: covers lines 5-10 (4 of 6 branches)
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
echo 'data' > "$TMPDIR/sample.txt"
check_file "$TMPDIR/sample.txt"

# To reach 100% branch coverage you also need:
# check_file "/nonexistent/path"
# check_file "$TMPDIR/empty.txt" (after: touch "$TMPDIR/empty.txt")

Integrando fixtures e cobertura na CI

Reunindo tudo: uma pipeline de CI robusta para projetos Bash combina a configuração de fixtures, a execução de testes com kcov, a mesclagem e uma verificação de limite mínimo em um único script. Esse script se torna o ponto de entrada da CI — um comando para executar tudo.

Princípios de design do executor de testes da CI:

  • Cada caso de teste chama setup_fixture e registra teardown_fixture por meio de trap
  • O script em teste é invocado com um $PATH falso e variáveis de ambiente com escopo restrito
  • kcov envolve cada invocação e grava os resultados em um subdiretório numerado
  • Após todos os testes, kcov mescla os resultados e o script de limite mínimo controla a compilação
  • O executor da CI termina com código diferente de zero se qualquer teste ou a verificação de cobertura falhar
#!/usr/bin/env bash
# ci_runner.sh — full fixture + coverage pipeline entry point
set -euo pipefail

SRC="./src/deploy.sh"
COV_ROOT="$(mktemp -d)/coverage"
trap 'rm -rf "$COV_ROOT"' EXIT
mkdir -p "$COV_ROOT"

PASS=0
FAIL=0
RUN_NUM=0

run_test() {
  local name="$1" test_fn="$2"
  local fixture
  fixture=$(mktemp -d)
  local cov_out="$COV_ROOT/run_$((++RUN_NUM))"

  if (
    trap 'rm -rf "$fixture"' EXIT
    export FIXTURE="$fixture"
    # Fake bin directory shadowing real tools
    mkdir -p "$fixture/bin"
    export PATH="$fixture/bin:$PATH"
    "$test_fn" "$fixture"
  ); then
    echo "PASS: $name"
    (( PASS++ )) || true
  else
    echo "FAIL: $name"
    (( FAIL++ )) || true
  fi
}

# Example test function
test_happy_path() {
  local fx="$1"
  mkdir -p "$fx/dist"
  echo 'app.js' > "$fx/dist/index.js"
  # Would normally run: kcov "$cov_out" "$SRC" --env=staging "$fx"
  echo "[test] happy path executed in $fx"
}

run_test 'happy_path' test_happy_path

echo "Results: $PASS passed, $FAIL failed"
(( FAIL == 0 ))

Verificação de conhecimento: técnica do PATH falso

Teste sua compreensão da técnica do PATH falso usada em fixtures de testes Bash.

Recapitulação: fixtures, ambientes temporários e cobertura

Nesta lição, foram apresentadas todas as ferramentas necessárias para testes Bash confiáveis e isolados:

  • Diretórios temporários — mktemp -d junto com um trap ... EXIT garante a limpeza automática, independentemente de como o teste termina.
  • Estrutura de fixture — um par setup_fixture / teardown_fixture preenche e destrói uma árvore de diretórios mínima que reproduz as entradas reais dos scripts.
  • PATH falso — coloque executáveis fictícios em $FIXTURE/bin/ e anteponha esse caminho a $PATH para interceptar chamadas a curl, aws, docker ou qualquer ferramenta externa sem tocar nos binários do sistema.
  • Escopo do ambiente — execute o script em teste em um subshell (() ou env -i) para que as variáveis alteradas nunca vazem de volta para o executor de testes.
  • Asserções — pequenas funções auxiliares (assert_eq, assert_file_exists, assert_contains) produzem uma saída clara de aprovação/reprovação e mensagens de erro significativas.
  • Cobertura com kcov — envolve a execução do script sem alterações no código-fonte e produz relatórios HTML e JSON de cobertura de linhas e ramificações.
  • Mesclagem e limite mínimo — combine várias execuções do kcov com --merge, analise o JSON e faça a CI falhar se a cobertura ficar abaixo do mínimo definido.
  • Cobertura de ramificações versus linhas — priorize sempre a cobertura de ramificações; somente a cobertura de linhas pode não detectar caminhos condicionais inteiros e transmitir uma falsa sensação de confiança.

Com essas técnicas, seus testes Bash se tornam tão rigorosos quanto os testes de qualquer linguagem compilada.

Perguntas Frequentes

A aula “Dispositivos de teste, ambientes temporários e cobertura” é grátis?

Sim — o texto completo de “Dispositivos de teste, ambientes temporários e cobertura” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de DevOps Bootcamp, atualize para CoddyKit PRO. O curso de DevOps Bootcamp inclui 4 aulas no total.

O que vou aprender em “Dispositivos de teste, ambientes temporários e cobertura”?

Crie dispositivos de teste isolados e meça quais ramificações dos scripts seus testes realmente exercitam. Você pratica DevOps Bootcamp com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar DevOps Bootcamp?

Nenhuma experiência prévia é necessária. DevOps Bootcamp no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 3 de 4.

Quanto tempo leva a aula “Dispositivos de teste, ambientes temporários e cobertura”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de DevOps Bootcamp?

Sim. Cada aula de DevOps Bootcamp inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. Teste unitário de funções com Bats-core
  2. Simulação de comandos e criação de substitutos para ferramentas externas
  3. Dispositivos de teste, ambientes temporários e cobertura
  4. Execução de testes de Shell em pipelines de integração contínua
← Voltar para DevOps Bootcamp