Escribir código en C que sea fácil de testear requiere romper la tendencia natural de mezclar la lógica de negocio con los efectos secundarios. Un efecto secundario es cualquier operación que interactúe con el mundo exterior: leer un registro de hardware, escribir en un archivo con fopen, llamar a printf o consultar la hora con time. Cuando una función está “acoplada” a estos mecanismos, la lógica se vuelve imposible de aislar; para probar un simple algoritmo de control, terminarías necesitando el hardware real o archivos físicos en el disco.
La solución técnica es el desacoplamiento mediante la inyección de dependencias. En lugar de que una función busque sus dependencias (como abrir un archivo por su nombre), estas le son entregadas como parámetros. Esto se logra de dos formas principales: pasando punteros a objetos de datos (como FILE* o estructuras de configuración) o, lo más potente en C, pasando punteros a función agrupados en una struct. Esto actúa como una “interfaz” manual. Si tu lógica depende de una estructura de funciones, en producción le pasarás las implementaciones reales y, en tus tests, le pasarás implementaciones simuladas o mocks que simplemente guardan los resultados en memoria para que puedas verificarlos.
Esta técnica es esencial cuando desarrollas firmware, sistemas embebidos o cualquier software que interactúe con el sistema operativo. Si no aplicas este patrón, tus tests dejarán de ser deterministas: fallarán no porque tu lógica sea incorrecta, sino porque el archivo que intentabas leer está bloqueado o porque el sensor no respondió a tiempo.
#define _POSIX_C_SOURCE 200809L // Necesario para fmemopen [POSIX]
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
/* Interfaz para el hardware (Inyección de dependencias mediante struct) */
typedef struct {
int (*leer_temperatura)(void);
void (*activar_ventilador)(bool estado);
} HardwareInterface;
/* Lógica de negocio: puramente dependiente de sus interfaces */
void controlar_clima(HardwareInterface *hw, float umbral, FILE *log) {
int temp = hw->leer_temperatura();
if (log) {
fprintf(log, "Temperatura actual: %d\n", temp);
}
if (temp > (int)umbral) {
hw->activar_ventilador(true);
if (log) fprintf(log, "Estado: Ventilador ACTIVADO\n");
} else {
hw->activar_ventilador(false);
if (log) fprintf(log, "Estado: Ventilador DESACTIVADO\n");
}
}
/* --- IMPLEMENTACIONES PARA TEST (Mocks) --- */
static int temp_mock = 0;
static bool ventilador_estado_mock = false;
int mock_leer_temp(void) {
return temp_mock;
}
void mock_activar_ventilador(bool estado) {
ventilador_estado_mock = estado;
}
/* --- TEST SUITE --- */
void test_control_clima_encendido() {
// Preparamos el escenario
temp_mock = 35;
ventilador_estado_mock = false;
HardwareInterface hw = {
.leer_temperatura = mock_leer_temp,
.activar_ventilador = mock_activar_ventilador
};
// Usamos fmemopen [POSIX] para capturar la salida en un buffer de memoria
// en lugar de escribir en un archivo físico o en stdout.
char buffer[256];
FILE *mem_log = fmemopen(buffer, sizeof(buffer), "w+");
assert(mem_log != NULL);
// Ejecución
controlar_clima(&hw, 25.0f, mem_log);
// Verificación de efectos secundarios en el hardware
assert(ventilador_estado_mock == true);
// Verificación de logs en memoria
rewind(mem_log); // Volver al inicio para leer
char linea[64];
fgets(linea, sizeof(linea), mem_log);
assert(strcmp(linea, "Temperatura actual: 35\n") == 0);
fclose(mem_log);
printf("Test 'encendido' PASADO\n");
}
void test_control_clima_apagado() {
temp_mock = 20;
ventilador_estado_mock = false;
HardwareInterface hw = {
.leer_temperatura = mock_leer_temp,
.activar_ventilador = mock_activar_ventilador
};
// Probamos con un log nulo para asegurar que la lógica no depende de él
controlar_clima(&hw, 25.0f, NULL);
assert(ventilador_estado_mock == false);
printf("Test 'apagado' PASADO\n");
}
int main(void) {
test_control_clima_encendido();
test_control_clima_apagado();
printf("Todos los tests completados con éxito.\n");
return 0;
}
Desglose del ejemplo
El núcleo del código es la función controlar_clima. Fíjate que no tiene ninguna llamada a funciones de sistema como read_sensor_register() o fopen(). En su lugar, recibe un puntero a HardwareInterface. Esto significa que la función no tiene idea de cómo se lee la temperatura, solo sabe que puede llamar a hw->leer_temperatura().
En el test test_control_clima_encendido, hemos implementado un “Mock”. Hemos creado variables estáticas (temp_mock, ventilador_estado_mock) que actúan como el hardware simulado. Al asignar mock_leer_temp a la estructura hw, podemos controlar exactamente qué valor recibirá la lógica de negocio. Esto permite simular escenarios extremos (como temperaturas de 100°C o sensores que fallan) de forma determinista y segura.
Para la parte de registro (logging), en lugar de escribir en un archivo real, utilizamos fmemopen [POSIX]. Esta función crea un FILE* que apunta a un buffer de memoria. Esto es extremadamente eficiente para tests porque evita la latencia de disco y el desorden de archivos temporales, permitiéndonos usar rewind y fgets para verificar que los mensajes de log sean exactamente los que esperamos.
El error frecuente
Un error común es intentar usar directivas de preprocesamiento como #ifdef TESTING para cambiar el comportamiento. Por ejemplo:
void log_error(const char *msg) {
#ifdef TESTING
// Simulación barata, pero difícil de verificar
printf("[MOCK] %s\n", msg);
#else
FILE *f = fopen("error.log", "a");
if (f) {
fprintf(f, "%s\n", msg);
fclose(f);
}
#endif
}
Esto es un mal diseño por dos razones:
1. Complejidad de compilación: Tienes que compilar el binario de nuevo para cada entorno de prueba.
2. Falta de introspección: En el bloque de test, el error solo se imprime en consola. No tienes una forma programática de verificar si la función log_error fue llamada con el mensaje correcto, a menos que captures stdout, lo cual es engorroso y propenso a fallos.
Si inyectas el puntero FILE* (como hicimos en el ejemplo) o usas una interfaz de logging, puedes verificar el contenido del log de forma automática y elegante mediante memoria.
N° 119