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/.hque 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
.coriginais, 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.
| Fase | Etapa de Aniche | O que você faz |
|---|---|---|
| 0 | — | Detecta ambiente → Caminho A (Zephyr), B (ESP-IDF) ou C (outro) |
| 1 | 1 Entender | Requisito, entradas, saídas de cada função fornecida |
| 2 | 2 Explorar código | Mapeia dependências para isolar com fakes/mocks |
| 3 | 3 Partições | Catálogo numerado de partições (a etapa mais importante) |
| 4 | 4 Fronteiras | Catálogo numerado de fronteiras (ponto on/off) |
| 5 | 5 Projetar casos | Combina partições/fronteiras, reduz combinações |
| 6 | 6 Automatizar | Escreve a suíte no framework do caminho, com rastreabilidade |
| 7 | — | Executa as ferramentas: execução + cobertura |
| 8 | 7 Aumentar | Itera 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.
-
Zephyr? → Caminho A. Sinais:
west.yml/west.yaml;CMakeLists.txtcomfind_package(Zephyr ...);prj.conf;testcase.yaml/sample.yaml;tests/;ztest/ZTEST_SUITE. Ferramentas: twister + ztest + FFF + coverage. Leiareferences/zephyr-path.md. -
ESP-IDF? → Caminho B. Sinais:
CMakeLists.txtcomidf_component_register(;sdkconfig/sdkconfig.defaults; uso de$ENV{IDF_PATH}/idf.py; um componentemain/. Ferramentas: Unity nativo do ESP-IDF (build host, alvolinux) + 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. Leiareferences/espidf-path.md. -
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:
- 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).
- 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:
- Comportamento documentado, ignorado com um motivo — o assert do que a documentação promete, ignorado com o motivo:
ztest_test_skip()(Caminho A) ouTEST_IGNORE_MESSAGE("BUG: ...")(Caminho B). Comentário acima:/* Covers: Pn — BUG: <docs say X; code does Y> */. - 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) ousource .venv/bin/activate. O venv frequentemente é um link simbólico: verifique comtest -e .venv/bin/activateouls -laL(que seguem o link), nunca apenas comls -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, ouwest topdir/.west/config) e, para o host,ZEPHYR_TOOLCHAIN_VARIANT=host. Sewestnão estiver noPATHmas 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.shquando existir) para o build host; caso contrário, compile comgccdiretamente (vejaespidf-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(vejaespidf-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, ougcov/gcovrsobre 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]