Testing Unitario Laboratorio de Testing y Aseguramiento de la Calidad del Software
Introducción Testing ad hoc Automatización de testing Testing unitario Unidad y Suite de test GoogleTest Fixture e independencia Test parametrizados por datos
Testing ad hoc (no sistemático) Todo programador está habituado al testing. En muchos casos, la forma en la que hacemos testing es ad hoc, es decir, no sistemática. Ej: supongamos que queremos testear la siguiente función: #include <stdio.h> int main(int argc, char **argv) { int a,b,res; } if (argc!= 3) printf("uso: max <int> <int>\n\n"); else { a = atoi(argv[1]); b = atoi(argv[2]); if (a < b) res = b; else res = a; printf("resultado: %i\n\n", res); }
Testing ad hoc (cont.) En este caso, podemos testear la aplicación directamente desde la línea de comandos: $ gcc max.c -o max $ $./max 2 3 Resultado: 3 $./max 123546 94746294 Resultado: 94746294 $./max -454-3 Resultado: -3 $./max 24 6 29 Uso: max <int> <int> $
Testing ad hoc (cont.) Ej: En caso que lo que tengamos no sea una aplicación, sino una funcionalidad interna: int max(int x, int y) { if (x < y) return y; else return x; } el testing ad hoc se vuelve más difícil: tenemos que programar un arnés para la rutina (i.e., un main que la invoque).
Dificultades del Testing ad hoc Es simple para testear aplicaciones, pero no almacena los tests para pruebas futuras. Requiere la construcción manual de arneses para la prueba de funcionalidades internas (no directamente invocables desde la interfaz de la aplicación). Requiere la inspección humana de la salida de los tests: se decide manualmente si el test pasó o no.
Automatización de testing Cuando el testing se vuelve complejo y caro como el testing es opcional, hay una tendencia a abandonarlo; o se lo intenta simplificar a través de su automatización. El costo de realizar y mantener un testing automatizado debe ser menor que el costo ahorrado por la reparación de defectos encontrados tardíamente.
Economía del Testing Esfuerzo en testing Incremento del esfuerzo Esfuerzo reducido Esfuerzo en desarrollo Esfuerzo inicial Esfuerzo ahorrado
Economía del Testing Esfuerzo en testing Incremento del esfuerzo Esfuerzo creciente Esfuerzo en desarrollo Esfuerzo inicial Esfuerzo ahorrado
Automatización de testing: objetivos Aumentar la calidad: utilizando tests como especificaciones; previniendo la reintroducción de defectos (en lugar de repararlos); localizando los defectos (ubicación y causa). Comprender el SUT: utilizando los tests como documentación. Reducir (y no introducir) riesgos: utilizando los tests como red de seguridad ; sin introducir cambios en el SUT.
Automatización de testing: objetivos Ser sencillos de ejecutar: completamente automáticos. self-checking. repetibles. Ser sencillos de escribir y mantener: simplicidad. expresividad. separación de incumbencias. Requerir mantenimiento mínimo: robustez.
Automatización de testing: problemas Muchas herramientas ofrecen automatizar test que interactuan con el SUT a través de su interfaz de usuario (metáfora record and playback) Estos test acarrean los siguientes problemas Sensibilidad al comportamiento Sensibilidad a la interfaz Sensibilidad a los datos Sensibilidad al contexto
Pruebas unitarias Testing unitario Unidad y Suite de test GoogleTest Fixture e independecia Test parametrizados por datos
Testing unitario Es una metodología para testear unidades individuales de código (SUT: Software under test), preferentemente de forma aislada de sus dependencias (DOC: Depend-on component). Las unidades pueden ser de distinta granularidad: porción de código, método, clase, librería, etc. Normalmente las pruebas son creadas por los mismos programadores durante el proceso de desarrollo. Normalmente se utiliza uno o mas frameworks para su sistematización y automatización.
Testing unitario (cont.) Normalmente las unidades son las partes mas pequeñas de un sistema: funciones o procedimientos, clases, etc. pero la granularidad es variable. Test unitario 1 ejercita alp unidad 1 Test unitario 2 ejercita Test unitario 3 ejercita unidad 2 usa Test unitario 4 ejercita componente 2 usa Test unitario 5 ejercita aplicación
Testing unitario: sistematización y automatización Una librería (framework) de apoyo al testing unitario ayuda a sistematizar y automatizar parte de las tareas manuales asociadas al testing: Define la estructura básica de un test: Inicialización Ejercitación Verificación Demolición Permite almacenar los tests como scripts. Permite definir la salida esperada como parte del script.
Testing unitario: sistematización y automatización Una librería (framework) de apoyo al testing unitario ayuda a sistematizar y automatizar parte de las tareas manuales asociadas al testing: Permite organizar tests en suites: Algunas librerías encuentran todos los test definidos y construyen la suite automáticamente. Otras requieren una construcción explícita. Las suites suelen organizarse jerárquicamente.
Testing unitario: sistematización y automatización Una librería (framework) de apoyo al testing unitario ayuda a sistematizar y automatizar parte de las tareas manuales asociadas al testing: Ofrece entornos para la ejecución de tests y suites: El test runner permite ejecutar automaticamente todos los test o subconjunto de ellos, de manera independiente. El test runner permite repetir un test con diferentes datos de entrada.
Testing unitario: sistematización y automatización Una librería (framework) de apoyo al testing unitario ayuda a sistematizar y automatizar parte de las tareas manuales asociadas al testing: Reporta información detallada sobre las pruebas: El test runner genera un reporte estadístico de la ejecución de las suites, El resultado puede incluir datos de benchmarking y cobertura.
Testing unitario con Google Test Google Test Framework es una librería de apoyo al testing unitario para C/C++ que incluye: descubrimiento automático de tests, amplio conjunto de aserciones (y aserciones definidas por el usuario), test negativos, fallas fatales y no fatales, test paramétricos, etc.
Estructura de un test arrange: se preparan los datos para alimentar al programa. #include <limits.h> #include "staticroutines.h" #include "gtest/gtest.h" TEST(MaxTest, Positive) { int a = 19; int b = 4; act: se ejecuta el programa con los datos construidos. int res = max(a,b); } EXPECT_TRUE(res == a); assert: se evalúa si los resultados obtenidos se corresponden con lo esperado.
Ejecución de tests Google Test necesita un runner explícito, que detecta automáticamente todos los test, y puede ser común a todos los test de un proyecto. se inicializa el framework int main(int argc, char **argv) { ::testing::initgoogletest(&argc, argv); } return RUN_ALL_TESTS(); se ejecutan todos los test
Demo Max
Algunas aserciones ASSERT_TRUE(<expr>): verifica que <expr> evalúe a true, parando la ejecución en caso de falla. ASSERT_FALSE(<expr>): verifica que <expr> evalúe a false, parando la ejecución en caso de falla. EXPECT_TRUE(<expr>): verifica que <expr> evalúe a true. EXPECT_FALSE(<expr>): verifica que <expr> evalúe a false. ASSERT_EQ(<expected>, <actual>): verifica que <actual> y <expected> evalúen al mismo valor. Sobre punteros verifica que sean la misma dirección y no que tengan el mismo valor! NE, LT, LE, GT, GE, para verificar desigualdad, menor, menor igual, mayor, y mayor igual respectivamente.
Algunas aserciones (cont.) ASSERT_STREQ(<expected>, <actual>): verifica que los strings <actual> y <expected> tengan el mismo contenido. STRNE: verifica que tengan diferente contenido. ASSERT_STRCASEEQ(<expected>, <actual>): verifica que los strings <actual> y <expected> tengan el mismo contenido, ignorando mayúsculas/minúsculas. STRCASENE: verifica que tengan diferente contenido ignorando mayúsculas/minúsculas. ASSERT_PREDn(<pred>, <val1>,..., <valn>): verifica que la función <pred> (de n parámetros) aplicada a <val1>,...,<valn> devuelva true.
Algunas aserciones (cont. 2) ASSERT_FLOAT_EQ(<expected>, <actual>): verifica los valores float <expected> y <actual> sean casi iguales. DOUBLE: para doubles ASSERT_NEAR(<val1>, <val2>, <error>): verifica que la diferencia entre <val1> y <val2> no supere el valor <error>. ASSERT_THROW(<prog>, <exception>): verifica que el programa <prog> lance la expeción <exception>. ASSERT_ANY_THROW(<prog>,): verifica que el programa <prog> lance cualquier excepción. ASSERT_NO_THROW(<prog>,): verifica que el programa <prog> no lance ninguna excepción. También tienen sus versiones EXPECT
Test negativos en Google Test Los tests negativos son aquellos en los que probamos que el SUT falla al ejecutar cierta porción de código bajo ciertas entradas. Con Google Test, podemos capturar los tests negativos con aserciones especiales: TEST(DeathTest, SegmentationFault) { ASSERT_DEATH({ int *n = NULL; int x = *n;}, "Segmentation fault:.*"); } TEST(DeathTest, NormalExit) { EXPECT_EXIT(NormalExit(), ::testing::exitedwithcode(0), "Success"); } TEST(DeathTest, SigKill) { EXPECT_EXIT(KillMyself(), ::testing::killedbysignal(sigkill), "Sending..."); }
Ejercicio Utilizando la clase Point: 1.Implementá el método equals. 2.Construí una suite de test para la clase, incluyendo al menos un test por método. 3. Descubriste algún bug? Corregilo.
Suites de test Cuando el SUT está compuesto por varias clases, o simplemente varios métodos de una clase, resulta claro que debemos organizar los tests. Los tests se organizan en test suites: una test suite es simplemente un conjunto de tests. Con Google Test, varios test lógicamente relacionados se puede agrupar en bajo un mismo nombre de Test Case. Una Suite (Fixture) es un conjunto de test bajo un mismo nombre de Test Case, definidos en una misma clase.
Suites de test y datos compartidos En muchos casos, los tests de una suite comparten los datos que manipulan. En estos contextos, suele ser útil organizar los tests definiendo procesos comunes de inicialización (o arrangement, o setup) para todos los tests. Similarmente, se pueden definir procesos comunes de destrucción (o teardown), que se ejecuten luego de cada test.
Suites de test y datos compartidos: un ejemplo Consideremos un ejemplo de testing de una clase Minefield, que representa el estado de un campo minado en el juego Buscaminas : class Mine { #include bool mined; "Mine.h" bool marked; class bool Minefield opened; { public: Mine field[8][8];! Mine(); public: void void setmined(bool open(int x, ismined); int y); bool void ismined(); close(int x, int y); void void setmarked(bool mark(int x, ismarked); int y); bool void ismarked(); unmark(int x, int y); void void setopened(bool putmine(int isopened); x, int y); bool void isopened(); removemine(int x, int y);! int minedneighbours(int x, int y);! }; }; Para poder testear minedneighbours, debemos crear el minefield, y ubicar minas en lugares específicos.
Suites de test y datos compartido: x un ejemplo x El proceso setup xen el siguiente ejemplo crea el escenario x x adecuado x para la ejecución de tests de minedneighbours. Se ejecuta antes de cada xtest. #include "Minefield.h" #include "gtest/gtest.h" class MinefieldTest : public ::testing::test {! protected:! Minefield testingfield;!! void SetUp() {!! testingfield.putmine(0, 0);!! testingfield.putmine(3, 4);!! testingfield.putmine(4, 3);!! testingfield.putmine(2, 2); TEST_F(MinefieldTest, NoOfMinedNeighbours01) { int number = testingfield.minedneighbours(1,!! 0); testingfield.putmine(0, 7); ASSERT_EQ(1, number);!! testingfield.putmine(7, 7); };!! testingfield.putmine(5, 1);!! testingfield.putmine(4, 7); TEST_F(MinefieldTest, NoOfMinedNeighbours02) {! } }; int number = testingfield.minedneighbours(7, }; 7); ASSERT_EQ(0, number); x
Sobre setup y teardown Es importante que no haya dependencias entre tests diferentes. No hay ninguna garantía sobre el orden en el que se ejecutan los tests GoogleTest construye un fixture fresco para cada test. testingfield = new Minefield(); testingfield.setup(); Las rutinas SetUp y TearDown ayudan NoOfMinedNeighbours01; a setear el contexto entre diferentes tests. testingfield.teardown(); free(testingfield); El método SetUp se ejecuta antes de cada test de la suite. testingfield.setup(); testingfield = new Minefield(); NoOfMinedNeighbours02; El método TearDown se ejecuta después de cada test testingfield.teardown(); de la suite. free(testingfield);
Independencia entre Tests GoogleTest asegura la independencia entre test (aunque class MineTest : public ::testing::test { se debe tener cuidado! con datos persistentes). protected: static void SetUpTestCase() { mine = new Mine(); } class MineTest : public ::testing::test {! static Mine* mine; class MineTest : protected: public ::testing::test }; {! Mine *mine; protected: }; Mine* MineTest::mine = NULL; Mine mine; }; TEST_F(MineTest, TEST_F(MineTest, Marked) { Marked) { mine = new Mine(); mine->setmarked(true); TEST_F(MineTest, mine->setmarked(true); Marked) { ASSERT_TRUE(mine->isMarked() &&!mine->isopened()); mine.setmarked(true); ASSERT_TRUE(mine->isMarked() }; &&!mine->isopened()); ASSERT_TRUE(mine.isMarked() }; &&!mine.isopened()); }; TEST_F(MineTest, Opened) { TEST_F(MineTest, Opened) mine->setopened(true); { TEST_F(MineTest, mine->setopened(true); Opened) { ASSERT_TRUE(!mine->isMarked() && mine->isopened()); mine.setopened(true); ASSERT_TRUE(!mine->isMarked() }; && mine->isopened()); ASSERT_TRUE(!mine.isMarked() }; && mine.isopened()); };
Demo Minefield
Ejercicio Utilizando las clases Minefield y Minefield_Test: 1.Agregá a Minefield_Test más casos de prueba para testear todos los métodos de Minefield. 2.Implementá el método minedneighbours en la clase Minefield y extendé el test correspondiente.
Tests parametrizados por los datos que manipulan Ya hemos visto varios casos en los cuales varios tests poseen exactamente la misma estructura, pero difieren en los datos que se utilizan para realizar el arrange del test. Para estos casos, Google Test ofrece una forma conveniente de organizar los tests, separando su estructura de los datos que manipulan. La clave es: definir un test como paramétrico. definir un generador de parámetros para la suite, que se usará para instanciar los datos para los tests.
Test parametrizados: un ejemplo Consideremos nuevamente el método max. El siguiente test parametrizado lo prueba con varios datos diferentes: using namespace std; using ::testing::testwithparam; using ::testing::values; Indica que la suite está formada por tests parametrizados. typedef ::tr1::tuple<int, int> mytuple; class MaxTest : public TestWithParam< mytuple > { }; TEST_P(MaxTest, Test) { mytuple row; int maxval, minval; } row = GetParam(); maxval = ::tr1::get<0>(row); minval = ::tr1::get<1>(row); ASSERT_EQ(maxVal, max(minval,maxval)); INSTANTIATE_TEST_CASE_P(MaxOnLeft, MaxTest, Values( mytuple (2,1), mytuple (45,-2), mytuple (-12,-90) )); Test paramétrico (TEST_P) Datos de los test
Test parametrizados Generador Descripción El runner genera una el conjunto instancia de valores de cada en el rango test por comenzando cada dato: en Range(begin, MaxOnLeft/MaxTest.Test/0 end, step) begin, terminando para (2,1) en end (no incluido) de a step incrementos. MaxOnLeft/MaxTest.Test/1 para (42,2) Values(v1,v2,...,vn) MaxOnLeft/MaxTest.Test/2 el conjunto para de valores (-12,-90) {v1, v2,..., vn}. Puede además haber más de una generador de datos (entonces ValuesIn(container) se genera el conjunto una instancia de valores por del contenedor cada test, container. por cada dato de cada generador). GoogleTest Bool define una el conjunto serie de de valores funciones {true, false}. para definir generadores de datos: Combine(g1,g2,...,gn) el producto cartesiano (tuple< >) de g1, g2,..., gn.
Demo Max Parametrizado Largest Parametrizado
Testing unitario: beneficios El testing unitario sistematizado: Facilita los cambios: los test permiten comprobar que la unidad continúa funcionando correctamente a pesar de cualquier tipo de cambio (refactoring o nuevas funcionalidades); su sistematización facilita el testing de regresión. Simplifica la integración: los test reducen la incertidumbre sobre las componentes individuales, facilitando la integración y su verificación, ya sea bottom-up o top-down (con la integración de mocks)
Testing unitario: beneficios El testing unitario sistematizado: Sirve como documentación: los test especifican las partes criticas del comportamiento de la unidad, ya sea su uso apropiado o inapropiado; sirven como ejemplo y descripción siempre actualizada de la API. Contribuye al diseño: los test sirven como ejemplo de uso y dan feedback temprano; fuerzan a enforcarse en el qué y en el cómo, encontrando defectos de diseño y aportando a su replanteamiento.
Testing unitario: principios Primero escribir los test. Diseñar pensando en la testeabilidad. Utilizar la puerta principal. Documentar los test fixtures. No modificar el SUT. Mantener la independencia de los test. Aislar el SUT. Minimizar el solapamiento. Minimizar el código inestable. Mantener la lógica de testing fuera del código de producción. Verificar sólo una condición por test. Probar diferentes incumbencias de forma separada.
Ejercicio Considerando los métodos definidos en staticroutines: 1.Extendé el test de largest para incluir más casos. 2.Construí una suite paramétrica para testear bubblesort. 3.Si encontrás bugs, corregilos.
Referencias Google Test: code.google.com/p/googletest/