Mocking em C: técnicas e ferramentas

1. Por que Mocking em C? Contexto e Desafios

1.1. Dependências complexas em sistemas embarcados e legados

Testar código C no mundo real significa lidar com hardware, sistemas operacionais, bibliotecas de terceiros e décadas de código legado. Uma função que chama read_sensor() ou send_uart() não pode ser testada unitariamente sem o hardware presente. O mocking resolve esse problema substituindo dependências reais por versões controladas, permitindo verificar comportamento sem efeitos colaterais.

1.2. Limitações do C para isolamento de testes

Diferente de linguagens com reflexão (Java, C#), C não permite inspecionar ou modificar dinamicamente chamadas de função. Não há carregamento tardio nativo, e funções são resolvidas em tempo de linkagem. As técnicas de mocking em C dependem de truques do pré-processador, ponteiros para função e substituição em nível de linker.

1.3. Diferença entre stub, mock e fake em C

  • Stub: Retorna valores fixos pré-definidos. Exemplo: int get_temperature() { return 25; }
  • Mock: Verifica interações — quantas vezes foi chamada, com quais argumentos. Exemplo: verificar se send_command(0x42) foi chamada exatamente uma vez.
  • Fake: Implementação funcional simplificada. Exemplo: um malloc que aloca de um pool estático em vez do heap real.

2. Técnicas Manuais de Mocking (Sem Ferramentas)

2.1. Mocking com ponteiros de função e structs de vtable

// hal.h
typedef struct {
    int (*init)(void);
    int (*read)(uint8_t addr);
} HAL_UART;

// test_hal_mock.c
static int mock_init_called = 0;
static int mock_init_retval = 0;

int mock_init(void) {
    mock_init_called++;
    return mock_init_retval;
}

void test_uart_init_calls_mock(void) {
    HAL_UART uart = { .init = mock_init };
    mock_init_retval = 0;
    int result = uart.init();
    assert(result == 0);
    assert(mock_init_called == 1);
}

2.2. Uso de #define e macros para substituição em tempo de compilação

// production.c
#include "sensor.h"
int read_temperature(void) {
    return sensor_read(REG_TEMP);
}

// test_sensor.c
#define sensor_read(reg) mock_sensor_read(reg)
#include "sensor.h"

int mock_sensor_read_called = 0;
int mock_sensor_read(uint8_t reg) {
    mock_sensor_read_called++;
    return 42; // temperatura fictícia
}

2.3. Mocking com variáveis globais e controle de estado

// mock_flags.h
extern int uart_send_called;
extern uint8_t uart_send_last_byte;
extern int uart_send_retval;

// mock_flags.c
int uart_send_called = 0;
uint8_t uart_send_last_byte = 0;
int uart_send_retval = 0;

int uart_send(uint8_t byte) {
    uart_send_called++;
    uart_send_last_byte = byte;
    return uart_send_retval;
}

3. CMock: Geração Automática de Mocks a partir de Headers

3.1. Instalação e integração com Ceedling (Ruby + Unity)

CMock faz parte do ecossistema Ceedling. Instalação simples com Ruby:

gem install ceedling
ceedling new meu_projeto
cd meu_projeto

3.2. Configuração de mock no project.yml

# project.yml
:cmock:
  :mock_prefix: mock_
  :when_no_prototypes: :warn
  :enforce_strict_order: true
  :plugins:
    - expect
    - return_thru_ptr

3.3. Expectativas, retornos e callbacks

// test/test_controller.c
#include "unity.h"
#include "mock_sensor.h"

void test_controller_reads_sensor_once(void) {
    sensor_read_ExpectAndReturn(REG_TEMP, 25);
    int result = controller_get_temperature();
    TEST_ASSERT_EQUAL(25, result);
}

4. Mocking com Falso Hardware e Periféricos (Embedded C)

4.1. Criação de mocks para HAL (Hardware Abstraction Layer)

// mock_hal.h
typedef struct {
    uint32_t DR;   // Data Register
    uint32_t SR;   // Status Register
    uint32_t CR1;  // Control Register 1
} USART_TypeDef;

extern USART_TypeDef USART1;

// mock_hal.c
USART_TypeDef USART1 = {0};

void HAL_UART_Transmit(USART_TypeDef *huart, uint8_t *data, uint16_t size, uint32_t timeout) {
    // apenas copia para buffer interno
    memcpy(mock_tx_buffer, data, size);
    mock_tx_size = size;
}

4.2. Simulação de registradores, interrupções e timers

// mock_timer.c
static uint32_t mock_tick = 0;

uint32_t HAL_GetTick(void) {
    return mock_tick;
}

void mock_timer_advance(uint32_t ms) {
    mock_tick += ms;
}

// test: verifica timeout após 1000ms
void test_timeout_after_1s(void) {
    mock_tick = 0;
    start_operation();
    mock_timer_advance(1000);
    TEST_ASSERT_TRUE(is_timed_out());
}

4.3. Uso de weak symbols e linkage overriding com GCC

// production.c (compilado normalmente)
__attribute__((weak)) int read_adc(void) {
    return adc_read_hardware();
}

// test.c (substitui o weak symbol)
int read_adc(void) {
    return 512; // valor mockado
}

5. Ferramentas Alternativas: FFF (Fake Function Framework)

5.1. Sintaxe enxuta: FFF_FAKES_LIST, RESET_FAKE, FakeValueType

#include "fff.h"

FAKE_VALUE_FUNC(int, sensor_read, uint8_t);

void test_sensor_read_returns_expected(void) {
    sensor_read_fake.return_val = 25;
    int result = sensor_read(REG_TEMP);
    TEST_ASSERT_EQUAL(25, result);
}

5.2. Verificação de ordem de chamadas e argumentos

FAKE_VOID_FUNC(uart_send, uint8_t);

void test_uart_send_called_with_correct_byte(void) {
    uart_send(0x42);
    TEST_ASSERT_EQUAL(1, uart_send_fake.call_count);
    TEST_ASSERT_EQUAL(0x42, uart_send_fake.arg0_val);
}

5.3. Comparação prática: FFF vs CMock

Característica FFF CMock
Dependências Apenas fff.h Ruby + Ceedling
Geração automática Manual A partir de headers
Verificação de ordem Manual Automática
Tamanho do mock ~50 linhas ~200+ linhas
Ideal para Projetos pequenos/médios Projetos grandes com muitos mocks

6. Mocking de Dependências Dinâmicas (dlopen, callbacks)

6.1. Mocking de funções de bibliotecas compartilhadas com LD_PRELOAD

// mock_malloc.c
#define _GNU_SOURCE
#include <dlfcn.h>

void *malloc(size_t size) {
    static void *(*real_malloc)(size_t) = NULL;
    if (!real_malloc) real_malloc = dlsym(RTLD_NEXT, "malloc");

    mock_malloc_count++;
    if (mock_malloc_fail) return NULL;
    return real_malloc(size);
}

Compile como biblioteca compartilhada e use:

gcc -shared -fPIC -o libmock_malloc.so mock_malloc.c -ldl
LD_PRELOAD=./libmock_malloc.so ./test_suite

6.2. Substituição de callbacks com wrappers em tempo de teste

// production.h
typedef void (*event_callback_t)(int event_id);
void register_callback(event_callback_t cb);

// test.c
static int last_event = -1;
void mock_event_handler(int event_id) {
    last_event = event_id;
}

void test_event_triggers_callback(void) {
    register_callback(mock_event_handler);
    trigger_event(42);
    TEST_ASSERT_EQUAL(42, last_event);
}

6.3. Cuidados com thread-safety e estado global em mocks

// mock_state.c
#include <pthread.h>

static pthread_mutex_t mock_lock = PTHREAD_MUTEX_INITIALIZER;
static int mock_call_count = 0;

void mock_increment(void) {
    pthread_mutex_lock(&mock_lock);
    mock_call_count++;
    pthread_mutex_unlock(&mock_lock);
}

7. Boas Práticas e Armadilhas Comuns

7.1. Manter mocks minimalistas e focados no comportamento esperado

Mocks devem simular apenas o necessário para o teste. Evite lógica complexa dentro do mock — isso cria outro código para depurar.

// RUIM: mock com lógica condicional complexa
int mock_read(uint8_t reg) {
    if (reg == 0x10) return 25;
    else if (reg == 0x20) return 100;
    else return 0;
}

// BOM: mock simples controlado pelo teste
int mock_read(uint8_t reg) {
    return mock_read_return;
}

7.2. Evitar acoplamento excessivo entre mocks e testes

Mocks não devem conhecer detalhes internos da implementação. Teste comportamento observável, não chamadas internas arbitrárias.

7.3. Limpeza de estado (ResetMock) e isolamento entre casos de teste

void setUp(void) {
    sensor_read_fake.return_val = 0;
    sensor_read_fake.call_count = 0;
    // ou usar RESET_FAKE(sensor_read) no FFF
}

void tearDown(void) {
    // limpar estado global se necessário
}

8. Integração com Pipeline de CI e Cobertura

8.1. Execução de testes com mocks em ambientes headless

# .github/workflows/test.yml
name: C Tests
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install dependencies
        run: sudo apt-get install -y ruby gcc lcov
      - name: Run Ceedling tests
        run: |
          gem install ceedling
          ceedling test:all

8.2. Medição de cobertura de código com mocks

# Adicione ao project.yml
:gcov:
  :utilities:
    - gcov
  :report_root: build/artifacts/gcov

# Execute
ceedling gcov:all
lcov --directory . --capture --output-file coverage.info
genhtml coverage.info --output-directory coverage_report

8.3. Exemplo prático: Ceedling + CMock + GitHub Actions

# project.yml completo
:project:
  :build_root: build
:paths:
  :test: test
  :source: src
:cmock:
  :mock_prefix: mock_
  :plugins:
    - expect
    - return_thru_ptr
:gcov:
  :utilities:
    - gcov

O pipeline executará todos os testes com mocks, gerará relatório de cobertura e publicará como artefato.

Referências

  • CMock Documentation — Documentação oficial do gerador de mocks automático para C, com exemplos de uso e plugins.
  • FFF (Fake Function Framework) — Repositório oficial da biblioteca leve de mocking para C, com exemplos de sintaxe e integração.
  • Ceedling Project Site — Guia completo do sistema de build e testes unitários para C, incluindo integração com CMock e Unity.
  • LD_PRELOAD Trick: Mocking System Calls in C — Documentação oficial do Linux sobre LD_PRELOAD, técnica usada para substituir funções de bibliotecas compartilhadas.
  • Embedded C Testing with Mocks — Artigo técnico da Memfault sobre práticas de mocking em sistemas embarcados, com exemplos reais de HAL e periféricos.
  • Gcov Documentation — Documentação oficial do GCC sobre gcov, ferramenta de cobertura de código usada em conjunto com mocks.
  • Unity Test Framework — Framework de testes unitários para C, frequentemente usado com CMock e Ceedling.