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
mallocque 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.