CommunityEscrita e Ediçãogithub.com

Teste Unitário Embarcado (baseado em especificação)

Gera uma suíte de testes unitários para arquivos .c/.h de firmware embarcado, seguindo a abordagem de teste baseado em especificação de Maurício Aniche (partições → fronteiras → casos → cobertura). Detecta automaticamente o ambiente: Zephyr usa twister + ztest + FFF no native_sim; ESP-IDF usa Unity nativo em build host + gcovr; outros seguem o framework de teste existente do projeto (Unity, CMock, Ceedling, GoogleTest…) ou para e pergunta se não houver nenhum.

O que é Teste Unitário Embarcado (baseado em especificação)?

Esta skill automatiza a geração de suítes de teste unitário para firmware embarcado, seguindo rigorosamente o método de teste baseado em especificação de Maurício Aniche (do livro Effective Software Testing). O processo começa com a detecção automática do ambiente de desenvolvimento: se o projeto for Zephyr, utiliza twister + ztest + FFF no simulador native_sim; se for ESP-IDF, utiliza o Unity nativo em build host com gcovr; para outros ambientes, segue o framework de teste unitário já existente no projeto (Unity, CMock, Ceedling, GoogleTest, etc.) ou para e pergunta qual framework usar. A skill executa as 7 etapas de Aniche (entendimento, exploração, partições, fronteiras, design de casos, automação e aumento) em 9 fases práticas, garantindo rastreabilidade total entre partições/fronteiras e casos de teste através de comentários no código. O resultado é uma suíte de testes executável que cobre as funções fornecidas, com meta de cobertura padrão de 95% de função, 95% de linha e 90% de branch, iterando até atingir o alvo. A skill é ativada automaticamente quando o usuário pede para gerar testes unitários para arquivos .c/.h, sem necessidade de mencionar a palavra "skill" ou o nome do framework.

Funciona com✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/PedroGilo12/claude-skills/tree/HEAD/plugins/firmware-testing/skills/embedded-unit-testing

Installed? Explore more Escrita e Edição skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

O que Teste Unitário Embarcado (baseado em especificação) faz?

Gera uma suíte de teste unitário para um conjunto de arquivos .c/.h fornecidos pelo usuário, usando as 7 etapas do teste baseado em especificação de Maurício Aniche (Effective Software Testing, cap. 2). O julgamento que faz disso uma skill: particionar bem o domínio de cada função (a etapa mais valiosa), derivar fronteiras, projetar a menor suíte que ainda pega bugs, automatizá-la no framework correto do projeto e iterar até que a cobertura atinja o alvo.

Leia references/specification-based-testing.md antes da Fase 3 — é a calibragem de como pensar sobre partições e fronteiras, e as pragmáticas que evitam uma suíte inchada ou frágil.

Invariantes de escopo (leia primeiro)

  • Saída em inglês. Tudo que esta skill produz é em inglês, mesmo quando o usuário escreve em outro idioma: código de teste, nomes de teste, comentários, catálogo, anotações Covers:, mensagens de skip/ignore, a declaração de caminho e o resumo final.
  • Apenas teste unitário. Nunca gere testes de integração, e2e, sistema, HIL ou Robot Framework. A granularidade é a função/módulo isolado, com dependências simuladas (fakes/mocks).
  • Apenas os arquivos fornecidos. A suíte é para o conjunto .c/.h que o usuário passou. Não escaneie o projeto em busca de outros alvos nem teste módulos vizinhos por conta própria.
  • Nunca modifique os arquivos alvo — nem temporariamente, nem "para testar e reverter". A suíte compila os arquivos .c originais, sem cópia no diretório de teste. Para alcançar algo difícil, use a API pública, fakes das dependências ou um binário de teste separado.
  • Autônomo. Gere, execute e reporte sem pedir confirmação para executar. Há duas exceções onde você para e pergunta: (a) Caminho C (nem Zephyr nem ESP-IDF) onde não é possível identificar nenhum framework de teste unitário (Fase 0); (b) a cada 2 iterações de cobertura sem atingir o alvo (Fase 8).
  • Nenhum relatório escrito por você. Você não escreve um relatório de resultados. Execute as ferramentas e aponte para os artefatos que elas geram (HTML de cobertura, saída do runner). O "resultado" é o que a ferramenta emite, não sua prosa.
  • Meta de cobertura: função ≥ 95%, linha ≥ 95%, branch ≥ 90% (95/95/90) por padrão. Uma vez atingida a meta, pare de iterar e liste as linhas/branches restantes não cobertos. Vá para 100/100/100 apenas se o usuário pedir. Abaixo da meta apenas quando o que falta é comprovadamente inatingível (ruído de log/assert, limitação de testabilidade do módulo), com cada linha/branch restante justificada.

O fluxo (7 etapas de Aniche → 9 fases)

As 7 etapas não são um pipeline rígido — são iterativas (a Fase 3 frequentemente revela algo que te envia de volta à Fase 1). As fases abaixo são a espinha dorsal.

FaseEtapa de AnicheO que você faz
0—Detecta ambiente → Caminho A (Zephyr), B (ESP-IDF) ou C (outro)
11 EntenderRequisito, entradas, saídas de cada função fornecida
22 Explorar códigoMapeia dependências para isolar com fakes/mocks
33 PartiçõesCatálogo numerado de partições (a etapa mais importante)
44 FronteirasCatálogo numerado de fronteiras (ponto on/off)
55 Projetar casosCombina partições/fronteiras, reduz combinações
66 AutomatizarEscreve a suíte no framework do caminho, com rastreabilidade
7—Executa as ferramentas: execução + cobertura
87 AumentarItera sobre lacunas (cobertura + itens do catálogo sem teste)

Fase 0 — Detecção de ambiente

Identifique o ambiente do projeto que contém os arquivos fornecidos, nesta ordem, e escolha o caminho. Robot Framework, HIL, pytest de integração NÃO são frameworks de teste unitário — eles nunca contam em nenhum caminho.

  1. Zephyr? → Caminho A. Sinais: west.yml/west.yaml; CMakeLists.txt com find_package(Zephyr ...); prj.conf; testcase.yaml/sample.yaml; tests/; ztest/ZTEST_SUITE. Ferramentas: twister + ztest + FFF + coverage. Leia references/zephyr-path.md.

  2. ESP-IDF? → Caminho B. Sinais: CMakeLists.txt com idf_component_register(; sdkconfig/sdkconfig.defaults; uso de $ENV{IDF_PATH}/idf.py; um componente main/. Ferramentas: Unity nativo do ESP-IDF (build host, alvo linux) + gcov/gcovr. Use Unity diretamente — não pare para perguntar. Exceção: se o projeto já usa consistentemente outro framework de teste unitário (Ceedling, GoogleTest…), siga esse. Leia references/espidf-path.md.

  3. Outro (nem Zephyr nem ESP-IDF) → Caminho C. Inspecione a estrutura de teste unitário existente (Unity, CMock, Ceedling, GoogleTest, personalizado) e siga o padrão do projeto fielmente. Se nenhum framework de teste unitário for encontrado, PARE e pergunte ao usuário qual usar — nunca assuma um padrão silencioso no Caminho C. Leia references/non-zephyr-path.md.

Indique no texto da resposta (não apenas no seu raciocínio), antes de prosseguir, o caminho escolhido e os sinais concretos que o justificam, com arquivo e o que você encontrou — ex.: "Caminho A (Zephyr): zephyr/west.yml, find_package(Zephyr) em tests/.../CMakeLists.txt, suítes ZTEST_SUITE em tests/lib/."


Fase 1 — Entender requisito, entradas e saídas

Para cada função que a suíte cobrirá, extraia da leitura do .c/.h:

  • a regra de negócio: o que ela faz e o que ela não deve fazer (cantos incluídos);
  • as entradas: tipos, faixa/domínio, restrições entre variáveis;
  • as saídas: como as entradas se tornam o resultado (e efeitos colaterais observáveis: saída escrita, erro retornado, movimento emitido/não emitido).

Parte disso é explícito; parte são regras de domínio implícitas (saturação, wrap, unidades, sinal, folga) que você infere. Essas regras implícitas são onde os bugs se escondem.

Fase 2 — Explore o código

Leia o .c/.h inteiro e construa o mapa de dependências a isolar: HAL, drivers, kernel/RTOS, E/S, tempo, chamadas para outros módulos. Cada dependência vira um fake/mock na Fase 6. Identifique também como a lógica real é alcançada (API pública direta, ou funções static via handle/vtable — chame-as através da API real, nunca invente uma assinatura).

Fase 3 — Identifique as partições (a etapa mais importante)

Aplique as 3 lentes de references/specification-based-testing.md para cada função: cada entrada isoladamente, entradas em combinação e classes de saída. Considere NULL/vazio e as regras de domínio implícitas. Se a função tiver estado, também particione pelo estado do módulo/contexto.

Produza, por função, um catálogo nomeado e numerado com IDs estáveis dentro da suíte:

  • Partições: <func>/P1, P2, …
  • Fronteiras (preenchidas na Fase 4): <func>/B1, B2, …

Cada item recebe uma breve descrição. Este catálogo é o contrato de rastreabilidade da suíte (materializado na Fase 6) e a fonte da verdade para lacunas (Fase 8). Uma Fase 3 bem feita deve cobrir quase tudo na primeira passagem de cobertura.

Fase 4 — Analise as fronteiras

Para cada fronteira entre partições vizinhas, adicione ao catálogo o ponto on (o valor na condição) e o ponto off (o vizinho mais próximo na partição oposta); igualdades dão um on e dois pontos off. Descarte uma fronteira já coberta por outro caso — não duplique. "Partição ou fronteira? Não importa": o que conta é que o caso existe.

Fase 5 — Projete os casos de teste

Combine as partições entre categorias de entrada, mas reduza a explosão combinatória: teste o excepcional (NULL/vazio) uma vez e não o combine com tudo; use o domínio para cortar. Use uma única semente de entrada, variando apenas o mínimo por caso (facilita depuração). Quando em dúvida, a entrada mais simples. Para entradas que não participam do comportamento, valores razoáveis/realísticos. Se um caso tiver o mesmo esqueleto que vários outros, planeje-o como parametrizado. Liste os casos e, para cada um, os IDs do catálogo que ele exercita.

Fase 6 — Automatize (com rastreabilidade obrigatória)

Escreva a suíte no framework do caminho detectado (sintaxe, isolamento e detalhes de scaffolding em references/zephyr-path.md, references/espidf-path.md ou references/non-zephyr-path.md).

O catálogo das Fases 3/4 se torna comentários na suíte, em dois níveis:

  1. Cabeçalho por função — um bloco no topo da seção de teste daquela função, listando o catálogo completo (cada partição/fronteira com seu ID e breve descrição).
  2. Anotação por teste — cada teste referencia qual(is) item(ns) ele exercita, ex.: /* Covers: P3 + B1 */.

Regras obrigatórias (isso é o que torna a rastreabilidade valiosa):

  • Nenhum teste órfão: todo teste referencia ≥ 1 item do catálogo.
  • Nenhum item órfão: toda partição/fronteira é exercitada por ≥ 1 teste. Um item sem teste é sinalizado como lacuna (alimenta a Fase 8).
  • IDs estáveis e idênticos entre cabeçalho e anotações — para que um grep "P4" liste o cabeçalho e todos os testes que tocam naquela partição.
  • Um teste cobrindo uma combinação lista todos os IDs (/* Covers: P2 + P5 */).
  • Covers: diz a verdade: cada ID anotado corresponde ao que o caso realmente exercita (entrada, estado e assert), não apenas a um item que existe no catálogo. Antes da Fase 7, releia as anotações em relação a cada corpo de teste e corrija as que não correspondem.
  • Nos Caminhos B e C, o mesmo esquema, adaptado à sintaxe do framework (comentário acima de cada TEST(...)/TEST_CASE(...)/TEST_F(...)).

Forma canônica (Caminho A, ztest):

/*
 * Catálogo — motor_ctrl_transform  (Fase 3 · teste baseado em especificação)
 * Partições:
 *   P1  p_axis_values == NULL        -> rejeitado, saída não escrita
 *   P2  axis_count válido (1..N)      -> transforma
 *   P3  valor positivo               -> passos positivos
 *   P4  valor negativo               -> sinal preservado
 *   P5  valor zero                   -> passos zero
 * Fronteiras:
 *   B1  axis_count == N (máx)        -> aceito
 *   B2  axis_count == N+1            -> rejeitado
 */

/* Covers: P2 + P3 */
ZTEST(motor_ctrl_suite, test_transform_positive_gives_positive_steps) { /* ... */ }

/* Covers: P1 */
ZTEST(motor_ctrl_suite, test_transform_null_input_is_rejected) { /* ... */ }

Nota: no Zephyr recente, o runner exige que nomes de teste comecem com test_ (ZTEST(suite, test_...)) — sem esse prefixo o twister não coleta o caso.

Defeito encontrado: código diverge da documentação

Quando o código contradiz o cabeçalho, o comentário da função ou o documento de contrato da API (ex.: retorna um código de erro diferente do documentado, não restaura estado em caso de falha), não deixe a suíte vermelha e não use ZTEST_EXPECT_FAIL. Escreva dois testes:

  1. Comportamento documentado, ignorado com um motivo — o assert do que a documentação promete, ignorado com o motivo: ztest_test_skip() (Caminho A) ou TEST_IGNORE_MESSAGE("BUG: ...") (Caminho B). Comentário acima: /* Covers: Pn — BUG: <docs say X; code does Y> */.
  2. Caracterização — fixa o comportamento atual, para que qualquer mudança (incluindo a correção) apareça. Comentário: /* Covers: Pn — characterization of the BUG above */.

A suíte termina verde, o bug aparece em toda execução (como skip/ignore) e o binário entra nas medições. Sintaxe nos arquivos de caminho. Liste os defeitos no resumo final.

Fase 7 — Execução + cobertura

Antes de executar, ative o ambiente do projeto. A ferramenta de teste raramente está no PATH global — geralmente vive em um virtualenv do repositório. Não conclua "não é possível executar" antes de procurar e ativar o ambiente (isso se aplica a todos os caminhos):

  • Procure por um venv na raiz do firmware (.venv/, venv/, env/) e use seu binário (.venv/bin/west, .venv/bin/python) ou source .venv/bin/activate. O venv frequentemente é um link simbólico: verifique com test -e .venv/bin/activate ou ls -laL (que seguem o link), nunca apenas com ls -d/find -type d. Diga o que encontrou: o caminho do venv usado, ou "nenhum venv em <diretórios verificados>".
  • Zephyr: defina ZEPHYR_BASE (ex.: deps/zephyr, ou west topdir/.west/config) e, para o host, ZEPHYR_TOOLCHAIN_VARIANT=host. Se west não estiver no PATH mas a árvore Zephyr existir, execute o twister diretamente: python3 $ZEPHYR_BASE/scripts/twister ....
  • ESP-IDF: certifique-se de que o ambiente IDF está configurado (. $IDF_PATH/export.sh quando existir) para o build host; caso contrário, compile com gcc diretamente (veja espidf-path.md).

Somente após esgotar essa busca é honesto relatar que a execução não pôde ser realizada aqui.

Economia de contexto (builds de firmware são verbosos e cada turno relê tudo):

  • Redirecione build, twister e gcovr para um arquivo (> build.log 2>&1) e mostre apenas o resumo (tail -n 30 build.log, grep -E 'error|FAIL|PASS|lines|branches' build.log). Abra o log completo apenas quando o resumo não explicar a falha.
  • Escreva cada arquivo de teste inteiro de uma só vez; corrija com edições direcionadas.
  • Agrupe leituras e verificações independentes no mesmo turno; não releia arquivos que você já leu.

Execute a ferramenta do caminho e aponte para os artefatos que ela gera (não escreva um relatório):

  • Caminho A (Zephyr): west twister --coverage -p native_sim -T <caminho_suíte>. Artefatos: twister-out/coverage/index.html, twister-out/coverage.json, twister-out/twister.xml, twister-out/twister.json.
  • Caminho B (ESP-IDF): build host com Unity + gcov/gcovr (veja espidf-path.md); aponte para o relatório de cobertura (coverage.html/.xml) e a saída do teste.
  • Caminho C (outro): runner próprio do projeto + cobertura (ex.: ceedling gcov, ou gcov/gcovr sobre o build de teste). Mantenha e aponte para os artefatos emitidos.

Detalhes e solução de problemas no arquivo de cada caminho.

Fase 8 — Iteração de cobertura

Compare a cobertura reportada pelas ferramentas com a meta 95/95/90 (ou a que o usuário pediu). Abaixo da meta, toda linha/branch atingível sem teste é uma lacuna; uma vez atingida a meta, pare e liste o que permanece descoberto. Para cada lacuna, adicione casos usando ambas as fontes: a cobertura numérica (linhas/branches vermelhas) e

...

[truncado para tradução]

Habilidades Relacionadas