Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Testing y Verificación Básica

Pruebas unitarias, aserciones con assert y cobertura de ramas

Universidad Nacional de Río Negro

Introducción

El testing o prueba de software es una práctica de ingeniería fundamental que consiste en verificar que el código funciona como se espera bajo diferentes condiciones. En programación estructurada, no basta con que el código “parezca” funcionar; es necesario someterlo a pruebas sistemáticas que validen su comportamiento y su estado ante múltiples escenarios de entrada.

¿Para qué probar?

Las pruebas cumplen propósitos didácticos y técnicos fundamentales:


Desarrollo

Anatomía de una Prueba Unitaria (AAA)

Las pruebas unitarias (unit tests) verifican de forma aislada el comportamiento de componentes de software individuales (normalmente funciones específicas).

Una prueba unitaria robusta se estructura sistemáticamente siguiendo el patrón AAA (Arrange-Act-Assert):

  1. Arrange (Preparar): Configura los datos de entrada, las condiciones iniciales y el resultado esperado.

  2. Act (Actuar): Invoca la función bajo prueba con los parámetros preparados y captura el valor de retorno.

  3. Assert (Verificar): Comprueba que el resultado obtenido coincide exactamente con el valor esperado utilizando aserciones.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <assert.h>

// Declaración de la función a probar
int calcular_suma(int a, int b);

void test_suma_valores_positivos(void) {
    // 1. Arrange: Preparar datos
    int a = 5;
    int b = 3;
    int esperado = 8;
    
    // 2. Act: Invocar la función
    int resultado = calcular_suma(a, b);
    
    // 3. Assert: Verificar
    assert(resultado == esperado);
}

Categorías de Casos de Prueba

Al diseñar una suite de pruebas, se deben contemplar obligatoriamente las siguientes categorías de escenarios:

Aserciones con assert y Cobertura de Ramas

La herramienta básica para la verificación estática y dinámica en desarrollo C es la macro assert, declarada en el archivo de cabecera <assert.h>.

Funcionamiento de la macro assert

La macro evalúa una expresión lógica. Si es verdadera, el programa continúa normalmente; si es falsa, la macro aborta inmediatamente la ejecución del programa mostrando por la salida de error estándar (stderr) la expresión lógica fallida, el nombre del archivo fuente y la línea física del código.

1
2
3
4
5
6
7
#include <assert.h>

void procesar_indice(int *arreglo, int idx) {
    // Aserción de desarrollo para verificar precondiciones físicas
    assert(arreglo != NULL);
    assert(idx >= 0);
}

Cobertura de Ramas (Branch Coverage)

La cobertura de código indica qué caminos lógicos de nuestras funciones fueron ejecutados durante las pruebas. El estándar riguroso para programación estructurada es lograr el 100% de cobertura de ramas (branch coverage), garantizando que se prueben tanto las condiciones verdaderas como las falsas de cada bifurcación del flujo de control.

1
2
3
4
5
6
7
8
9
10
int valor_absoluto(int x) {
    if (x < 0) { // Bifurcación en rama 1 y rama 2
        return -x;
    }
    return x;
}

// Para lograr 100% de cobertura de ramas debemos invocar:
assert(valor_absoluto(-5) == 5); // Cubre la rama verdadera (x < 0)
assert(valor_absoluto(5) == 5);  // Cubre la rama falsa (x >= 0)

Ejercicios de Autoevaluación

Anatomía de Pruebas Unitarias

Solution to Exercise 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <assert.h>
#include <string.h>

void test_invertir_cadena_palabra_simple(void) {
    // 1. Arrange: Preparar el estado y datos de entrada
    char entrada[] = "hola";
    const char *esperado = "aloh";

    // 2. Act: Ejecutar la función bajo prueba
    invertir_cadena(entrada);

    // 3. Assert: Verificar el resultado
    assert(strcmp(entrada, esperado) == 0);
}
Solution to Exercise 2
  • Casos límite (Boundary cases):

    1. n == 1: El arreglo posee un único elemento, y coincide con el buscado (arr[0] == elem).

    2. n == 1: El arreglo posee un único elemento, y no coincide con el buscado (arr[0] != elem).

    3. elem se encuentra exactamente en la primera posición del arreglo (arr[0] == elem).

    4. elem se encuentra exactamente en la última posición del arreglo (arr[n-1] == elem).

  • Caso de esquina (Corner case):

    • n == 0 (búsqueda en un arreglo vacío). En este caso, la función debe abortar o retornar error de forma segura en lugar de desreferenciar.

Aserciones y Cobertura de Ramas

Solution to Exercise 3

La macro assert(condición) se desactiva y remueve físicamente del código compilado cuando se define la constante #define NDEBUG.

Si escribimos assert(malloc(100) != NULL);, en la compilación de producción la llamada entera a malloc será eliminada del ejecutable. Por ende, la memoria nunca se reservará y el programa experimentará fallos silenciosos por desreferenciación de punteros no inicializados. Las asignaciones de recursos deben validarse con condicionales if tradicionales.

Solution to Exercise 4

Para lograr el 100% de cobertura de ramas debemos forzar a que cada sentencia condicional tome caminos verdaderos y falsos

1
2
3
4
5
6
7
8
#include <assert.h>

void test_es_bisiesto_ramas(void) {
    assert(es_bisiesto(2000) == true);  // Evalúa VERDADERO la rama 1 (divisible por 400)
    assert(es_bisiesto(1900) == false); // Evalúa FALSO la rama 1 y VERDADERO la rama 2 (divisible  por 100)
    assert(es_bisiesto(2024) == true);  // Evalúa FALSAS rama 1 y 2, y VERDADERO la rama 3 divisible por 4)
    assert(es_bisiesto(2023) == false); // Evalúa FALSAS todas las ramas (camino por defecto final)
}

Glosario

Prueba unitaria (Unit Test)
Método de verificación que examina de forma aislada el comportamiento de bloques individuales de código fuente (habitualmente funciones).
AAA (Arrange-Act-Assert)
Patrón estructurado para redactar pruebas unitarias en tres pasos: Preparar (Arrange), Actuar (Act) y Verificar (Assert).
Caso límite (Boundary Case)
Caso de prueba diseñado para evaluar valores de entrada que están en los límites o extremos del rango de validez.
Caso de esquina (Corner Case)
Caso de prueba excepcional donde se combinan en simultáneo múltiples condiciones o factores límite del sistema.
Cobertura de ramas (Branch Coverage)
Métrica que mide el porcentaje de bifurcaciones condicionales que han sido evaluadas en caminos verdaderos y falsos durante la ejecución de las pruebas.

Síntesis y Resumen

En esta unidad cubriste los fundamentos prácticos del testeo de software en C:


References y Lecturas Complementarias