Testing em C: Unity, CMock e Ceedling
1. Introdução ao Teste Unitário em C
Testar código em C apresenta desafios únicos quando comparado a linguagens modernas. A ausência de orientação a objetos, o gerenciamento manual de memória e a forte dependência de hardware tornam a criação de testes unitários uma tarefa complexa. Diferentemente de linguagens como Java ou Python, onde frameworks de teste são onipresentes, o ecossistema C historicamente carecia de ferramentas padronizadas para esse fim.
Unity, CMock e Ceedling formam um trio poderoso que preenche essa lacuna. Unity é um framework de assertions leve e portátil, ideal para ambientes embarcados. CMock gera automaticamente funções mock a partir de cabeçalhos C, permitindo isolar unidades de código. Ceedling automatiza todo o ciclo de testes, integrando Unity e CMock em um fluxo contínuo.
Os benefícios são claros: detecção precoce de bugs, refatoração segura de código legado e conformidade com padrões como MISRA C, que exige rastreabilidade e cobertura de testes em sistemas críticos.
2. Unity: Framework de Assertions Leve
Unity é um framework minimalista, escrito em C puro, que fornece macros para verificação de resultados esperados. Sua instalação é simples: basta copiar os arquivos unity.c e unity.h para o projeto.
Principais Macros
#include "unity.h"
void test_soma(void) {
TEST_ASSERT_EQUAL(5, soma(2, 3));
TEST_ASSERT_TRUE(soma(0, 0) == 0);
TEST_ASSERT_STRING_EQUAL("ok", status_code(200));
}
Estrutura de um Teste
Todo teste em Unity segue um padrão com setUp(), tearDown() e funções test_*:
#include "unity.h"
#include "calculadora.h"
void setUp(void) {
// inicializações antes de cada teste
}
void tearDown(void) {
// limpeza após cada teste
}
void test_calcula_media(void) {
float valores[] = {10.0, 20.0, 30.0};
TEST_ASSERT_EQUAL_FLOAT(20.0, calcular_media(valores, 3));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_calcula_media);
return UNITY_END();
}
Exemplo Prático: Função Matemática
// src/matematica.h
int fatorial(int n);
// src/matematica.c
int fatorial(int n) {
if (n <= 1) return 1;
return n * fatorial(n - 1);
}
// test/test_matematica.c
#include "unity.h"
#include "matematica.h"
void test_fatorial_zero(void) {
TEST_ASSERT_EQUAL(1, fatorial(0));
}
void test_fatorial_cinco(void) {
TEST_ASSERT_EQUAL(120, fatorial(5));
}
void test_fatorial_negativo(void) {
TEST_ASSERT_EQUAL(1, fatorial(-3));
}
3. CMock: Geração Automática de Mocks
CMock permite substituir funções reais por simuladas durante os testes. Isso é essencial quando o código depende de hardware, sensores ou bibliotecas externas.
Configuração Básica
Para mockar uma função, declare-a em um cabeçalho e configure o CMock no project.yml:
:cmock:
:mock_path: build/mocks
:includes:
- "sensor.h"
Uso de Expect e Return
#include "mock_sensor.h"
void test_leitura_sensor(void) {
// Configura expectativa: função ler_temperatura será chamada
// e deve retornar 25.5
ler_temperatura_ExpectAndReturn(25.5);
float temp = processar_sensor();
TEST_ASSERT_EQUAL_FLOAT(25.5, temp);
}
Exemplo: Mock de Sensor em Sistema Embarcado
// src/sensor.h
float ler_temperatura(void);
int inicializar_sensor(void);
// src/controlador.c
#include "sensor.h"
float obter_media_temperatura(int amostras) {
float soma = 0;
for (int i = 0; i < amostras; i++) {
soma += ler_temperatura();
}
return soma / amostras;
}
// test/test_controlador.c
#include "unity.h"
#include "mock_sensor.h"
#include "controlador.h"
void test_media_temperatura(void) {
ler_temperatura_ExpectAndReturn(30.0);
ler_temperatura_ExpectAndReturn(40.0);
ler_temperatura_ExpectAndReturn(50.0);
float media = obter_media_temperatura(3);
TEST_ASSERT_EQUAL_FLOAT(40.0, media);
}
4. Ceedling: Automação do Ciclo de Testes
Ceedling orquestra Unity e CMock, gerenciando compilação, execução e relatórios. Um projeto típico possui esta estrutura:
projeto/
├── project.yml
├── src/
│ ├── main.c
│ └── calculadora.c
├── test/
│ └── test_calculadora.c
└── build/
└── ... (gerado automaticamente)
Comandos Principais
# Executar todos os testes
ceedling test:all
# Executar testes específicos
ceedling test:pattern test_calculadora
# Limpar build
ceedling clobber
Exemplo: Projeto Completo
project.yml:
:project:
:build_root: build
:paths:
:source:
- src
:test:
- test
:plugins:
:load_paths:
- vendor/ceedling/plugins
:enabled:
- stdout_pretty_tests_report
- module_generator
Após criar os arquivos src/calculadora.c e test/test_calculadora.c, execute:
ceedling test:all
Saída esperada:
TEST OUTPUT
-----------
test_calculadora.c: 3 tests, 0 failures, 0 ignored
5. Técnicas Avançadas de Teste com Unity e CMock
Teste de Dependências de Hardware
Para testar código que acessa registradores, use mocks com parâmetros por ponteiro:
void escrever_registro(uint32_t endereco, uint32_t valor);
void test_inicializacao_gpio(void) {
escrever_registro_Expect(0x40020C00, 0x55555555);
inicializar_gpio();
}
Verificação de Buffers com TEST_ASSERT_EQUAL_MEMORY
uint8_t buffer_esperado[4] = {0x01, 0x02, 0x03, 0x04};
uint8_t buffer_recebido[4];
processar_pacote(buffer_recebido);
TEST_ASSERT_EQUAL_MEMORY(buffer_esperado, buffer_recebido, sizeof(buffer_esperado));
Mock de Funções com Callbacks
void registrar_callback(void (*cb)(int));
void meu_callback(int valor) {
// implementação real
}
void test_registro_callback(void) {
registrar_callback_Expect(meu_callback);
configurar_sistema();
}
6. Integração com Ferramentas de Qualidade e CI
Cobertura de Código com gcov/lcov
Adicione ao project.yml:
:plugins:
:enabled:
- gcov
Execute:
ceedling gcov:all
ceedling utils:gcov
Pipeline CI com GitHub Actions
name: Testes C
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Instalar dependências
run: sudo apt-get install ruby gcc
- name: Executar testes
run: |
gem install ceedling
ceedling test:all
Verificação com Valgrind
ceedling test:all
valgrind --leak-check=full build/test/out/test_calculadora
7. Melhores Práticas e Padrões para Testes em C
- Um arquivo de teste por módulo:
test_calculadora.ctestacalculadora.c - Nomenclatura consistente:
test_<modulo>_<funcionalidade> - Fixtures centralizadas: Crie
test_helpers.hcom dados de teste reutilizáveis - Teste de código legado: Use mocks para isolar funções sem refatorar
Exemplo de fixture:
// test/helpers/test_data.h
#define TEST_BUFFER_SIZE 256
extern uint8_t buffer_teste[TEST_BUFFER_SIZE];
8. Limitações e Alternativas no Ecossistema C
Unity/CMock/Ceedling não são ideais para projetos muito pequenos (overhead desnecessário) ou muito grandes (performance pode ser limitada). Alternativas incluem:
- CuTest: Framework minimalista, sem mocks
- Check: Framework com suporte a forks para testes mais seguros
- Google Test: Via wrapper C++, oferece rica API de assertions
Para migração, comece adicionando Ceedling a um subconjunto de módulos, mockando dependências externas gradualmente. Em sistemas safety-critical, respeite MISRA C configurando flags de compilação e evitando alocações dinâmicas nos testes.
Referências
- Documentação oficial do Unity — Guia completo do framework de assertions, incluindo todas as macros e exemplos de uso
- Documentação oficial do CMock — Referência para geração de mocks, configuração de expectativas e tratamento de parâmetros complexos
- Documentação oficial do Ceedling — Manual de automação de testes, incluindo configuração de projetos e plugins
- Tutorial: Testes Unitários em C para Sistemas Embarcados — Artigo prático da Embarcados sobre implementação de testes com Unity em microcontroladores
- MISRA C:2012 Guidelines for the Use of the C Language in Critical Systems — Padrão oficial MISRA C, relevante para conformidade em testes de sistemas críticos
- Valgrind User Manual — Documentação oficial do Valgrind para detecção de vazamentos de memória em testes C
- GitHub Actions Documentation: Building and testing C — Guia oficial para integração de testes C em pipelines CI com GitHub Actions