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.c testa calculadora.c
  • Nomenclatura consistente: test_<modulo>_<funcionalidade>
  • Fixtures centralizadas: Crie test_helpers.h com 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