2010-02-26 14 views

Respuesta

19

Las pruebas unitarias solo requieren "planos de corte" o límites en los que se pueden realizar pruebas. Es bastante sencillo probar las funciones C que no llaman a otras funciones o que solo llaman a otras funciones que también se prueban. Algunos ejemplos de esto son funciones que realizan cálculos u operaciones lógicas, y son de naturaleza funcional. Funcional en el sentido de que la misma entrada siempre da como resultado la misma salida. Probar estas funciones puede tener un gran beneficio, a pesar de que es una pequeña parte de lo que normalmente se considera como pruebas unitarias.

Pruebas más sofisticadas, como el uso de simulaciones o resguardos también es posible, pero no es tan fácil como en los lenguajes más dinámicos, o incluso en los lenguajes orientados a objetos como C++. Una forma de abordar esto es usar #defines. Un ejemplo de esto es este artículo, Unit testing OpenGL applications, que muestra cómo simular llamadas OpenGL. Esto le permite probar que se realizan secuencias válidas de llamadas OpenGL.

Otra opción es aprovechar los símbolos débiles.Por ejemplo, todas las funciones API de MPI son símbolos débiles, por lo que si define el mismo símbolo en su propia aplicación, su implementación anula la implementación débil en la biblioteca. Si los símbolos en la biblioteca no fueran débiles, obtendría errores de símbolos duplicados en el tiempo del enlace. A continuación, puede implementar lo que en realidad es una simulación de toda la API de MPI C, lo que le permite garantizar que las llamadas coincidan adecuadamente y que no haya llamadas adicionales que puedan causar interbloqueos. También es posible cargar los símbolos débiles de la biblioteca usando dlopen() y dlsym(), y pasar la llamada si es necesario. MPI realmente proporciona los símbolos de PMPI, que son fuertes, por lo que no es necesario usar dlopen() y sus amigos.

Puede obtener muchos de los beneficios de las pruebas unitarias para C. Es un poco más difícil, y es posible que no sea posible obtener el mismo nivel de cobertura que podría esperarse de algo escrito en Ruby o Java, pero definitivamente vale la pena obra.

+3

La mejor respuesta hasta el momento. Incluso se muestra cómo manejar la necesidad práctica de la Inyección de Dependencia en C, que es la técnica principal para aislar el alcance del código para lograr una verdadera prueba unitaria. – kirakun

11

En el nivel más básico, las pruebas unitarias son simplemente bits de código que ejecutan otros bits de código y le dicen si funcionó como se esperaba.

Simplemente podría hacer una nueva aplicación de consola, con una función main(), que ejecutó una serie de funciones de prueba. Cada prueba llamaría a una función en su aplicación y devolvería un 0 para el éxito u otro valor para la falla.

Te daría un código de ejemplo, pero estoy muy oxidado con C. Estoy seguro de que hay algunos marcos que también facilitarían esto.

+4

Si todavía está buscando un marco compruebe esta lista de frameworks de pruebas unitarias c disponibles: http : //en.wikipedia.org/wiki/List_of_unit_testing_frameworks#C – tobsen

+0

ah-ha excelente lista. Hay incluso más frameworks de lo que pensaba. –

1

Uso assert. Aunque no es realmente un marco.

+4

afirmar no es bueno para las pruebas, porque su programa de prueba terminará en la primera falla, lo que limita severamente las pruebas automatizadas y los informes de prueba. –

+0

Assert se puede usar como parte de las pruebas unitarias o como parte de garantizar que sus funciones se llamen correctamente, pero no son "Unit Testing" –

3

La forma más simple de hacer una prueba unitaria es construir un código de controlador simple que se vincule con el otro código, y llamar a cada función en cada caso ... y afirmar los valores de los resultados de las funciones y construir poco a poco ... así es como lo hago de todos modos

 
int main(int argc, char **argv){ 

    // call some function 
    int x = foo(); 

    assert(x > 1); 

    // and so on.... 

} 

Espero que esto ayude, saludos, Tom.

4

No hay nada intrínsecamente orientado a objetos para probar pequeñas piezas de código de forma aislada. En los lenguajes de procedimiento, prueba funciones y colecciones de los mismos.

Si está desesperado, y tendría que estar desesperado, junté un pequeño preprocesador C y un marco basado en gmake. Comenzó como un juguete, y nunca creció realmente, pero yo tengo lo usé para desarrollar y probar un par de proyectos de tamaño medio (más de 10,000 líneas).

Dave's Unit Test es mínimamente intrusivo pero puede hacer algunas pruebas que originalmente pensé que no sería posible para un marco basado en preprocesador (puede exigir que un cierto tramo de código arroje un fallo de segmentación bajo ciertas condiciones, y lo probará para ti).

También es un ejemplo de por qué hacer un uso intensivo del preprocesador es difícil para hacer de forma segura.

1

Con C debe ir más allá de simplemente implementar un marco sobre el código existente.

Una cosa que siempre he hecho es crear un módulo de prueba (con un principal) desde el que pueda realizar pequeñas pruebas para probar el código. Esto le permite hacer incrementos muy pequeños entre el código y los ciclos de prueba.

La mayor preocupación es escribir su código para poder probarlo. Concéntrese en funciones pequeñas e independientes que no dependen de variables compartidas o estado. Intente escribir de una manera "Funcional" (sin estado), esto será más fácil de probar. Si tiene una dependencia que no siempre puede estar allí o es lenta (como una base de datos), es posible que tenga que escribir una capa "simulada" completa que pueda sustituirse por su base de datos durante las pruebas. todavía se aplican

Los objetivos principales de la unidad de prueba: garantizar el código bajo prueba siempre se restablece a un estado dado, prueba constantemente, etc ...

Cuando escribí el código en C (de vuelta antes de Windows) que tenía un lote archivo que mostraría un editor, luego cuando termine de editar y salir, compilará, vinculará, ejecutará pruebas y luego mostrará el editor con los resultados de compilación, los resultados de las pruebas y el código en diferentes ventanas. Después de mi descanso (de un minuto a varias horas, dependiendo de lo que se estaba compilando), pude revisar los resultados y volver directamente a la edición. Estoy seguro de que este proceso podría mejorarse estos días :)

6

Puede usar libtap que proporciona una serie de funciones que pueden proporcionar diagnósticos cuando falla una prueba. Un ejemplo de su uso:

#include <mystuff.h> 
#include <tap.h> 

int main() { 
    plan(3); 
    ok(foo(), "foo returns 1"); 
    is(bar(), "bar", "bar returns the string bar"); 
    cmp_ok(baz(), ">", foo(), "baz returns a higher number than foo"); 
    done_testing; 
} 

Sus similares para aprovechar las bibliotecas en otros idiomas.

3

Aquí hay un ejemplo de cómo implementaría múltiples pruebas en un solo programa de prueba para una función dada que podría llamar a una función de biblioteca.

Supóngase que se desea probar el módulo siguiente:

#include <stdlib.h> 

int my_div(int x, int y) 
{ 
    if (y==0) exit(2); 
    return x/y; 
} 

A continuación, crear el siguiente programa de prueba:

#include <stdio.h> 
#include <stdlib.h> 
#include <unistd.h> 
#include <setjmp.h> 

// redefine assert to set a boolean flag 
#ifdef assert 
#undef assert 
#endif 
#define assert(x) (rslt = rslt && (x)) 

// the function to test 
int my_div(int x, int y); 

// main result return code used by redefined assert 
static int rslt; 

// variables controling stub functions 
static int expected_code; 
static int should_exit; 
static jmp_buf jump_env; 

// test suite main variables 
static int done; 
static int num_tests; 
static int tests_passed; 

// utility function 
void TestStart(char *name) 
{ 
    num_tests++; 
    rslt = 1; 
    printf("-- Testing %s ... ",name); 
} 

// utility function 
void TestEnd() 
{ 
    if (rslt) tests_passed++; 
    printf("%s\n", rslt ? "success" : "fail"); 
} 

// stub function 
void exit(int code) 
{ 
    if (!done) 
    { 
     assert(should_exit==1); 
     assert(expected_code==code); 
     longjmp(jump_env, 1); 
    } 
    else 
    { 
     _exit(code); 
    } 
} 

// test case 
void test_normal() 
{ 
    int jmp_rval; 
    int r; 

    TestStart("test_normal"); 
    should_exit = 0; 
    if (!(jmp_rval=setjmp(jump_env))) 
    { 
     r = my_div(12,3); 
    } 

    assert(jmp_rval==0); 
    assert(r==4); 
    TestEnd(); 
} 

// test case 
void test_div0() 
{ 
    int jmp_rval; 
    int r; 

    TestStart("test_div0"); 
    should_exit = 1; 
    expected_code = 2; 
    if (!(jmp_rval=setjmp(jump_env))) 
    { 
     r = my_div(2,0); 
    } 

    assert(jmp_rval==1); 
    TestEnd(); 
} 

int main() 
{ 
    num_tests = 0; 
    tests_passed = 0; 
    done = 0; 
    test_normal(); 
    test_div0(); 
    printf("Total tests passed: %d\n", tests_passed); 
    done = 1; 
    return !(tests_passed == num_tests); 
} 

Al redefinir assert para actualizar una variable booleana, puede continuar en caso de una la aserción falla y ejecuta múltiples pruebas, haciendo un seguimiento de cuántas tuvieron éxito y cuántas fallaron.

Al inicio de cada prueba, establezca rslt (las variables utilizadas por la macro assert) en 1 y establezca las variables que controlan las funciones de código auxiliar.Si se llama a uno de sus stubs más de una vez, puede configurar matrices de variables de control para que los stubs puedan verificar diferentes condiciones en diferentes llamadas.

Dado que muchas funciones de la biblioteca son símbolos débiles, se pueden redefinir en su programa de prueba para que se llamen. Antes de llamar a la función para probar, puede establecer un número de variables de estado para controlar el comportamiento de la función de stub y verificar las condiciones en los parámetros de la función.

En los casos en que no pueda redefinir de esa manera, asigne un nombre diferente a la función de código auxiliar y redefina el símbolo en el código para probar. Por ejemplo, si desea unir fopen pero encuentra que no es un símbolo débil, defina su stub como my_fopen y compile el archivo para probar con -Dfopen=my_fopen.

En este caso particular, la función a probar puede llamar al exit. Esto es complicado, ya que exit no puede regresar a la función que se está probando. Este es uno de los raros momentos en los que tiene sentido usar setjmp y longjmp. Utiliza setjmp antes de ingresar a la función para probar, luego, en el exit topado, llama al longjmp para regresar directamente a su caso de prueba.

También tenga en cuenta que el exit redefinido tiene una variable especial que verifica si realmente desea salir del programa y llama al _exit para hacerlo. Si no lo hace, es posible que su programa de prueba no se cierre limpiamente.

Este conjunto de pruebas también cuenta el número de pruebas intentadas y fallidas y devuelve 0 si se pasaron todas las pruebas y 1 en caso contrario. De esta forma, make puede verificar si fallan las pruebas y actuar en consecuencia.

El código de prueba de salida voluntad por encima de lo siguiente:

-- Testing test_normal ... success 
-- Testing test_div0 ... success 
Total tests passed: 2 

Y el código de retorno será 0.