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
• 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); }
}
En este caso, podemos testear la aplicación directamente
desde la línea de comandos:
Testing “ad hoc” (cont.)
$ 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> $
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).
• 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
Economía del Testing
Esfuerzo en desarrollo Esfuerzo en testing Esfuerzo inicial Incrementodel esfuerzo Esfuerzo
reducido
Esfuerzo ahorrado
Economía del Testing
Esfuerzo en desarrollo Esfuerzo en testing Esfuerzo inicial Incrementodel esfuerzo Esfuerzo
creciente
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”;
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:
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
Pruebas unitarias
Testing unitario | Unidad y Suite de test |
GoogleTest | Fixture e independecia |
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
• Normalmente las unidades son las partes mas pequeñas
de un sistema: funciones o procedimientos, clases, etc. • pero la granularidad es variable.
Testing unitario (cont.)
alp aplicación ejercita Test unitario 1 Test unitario 2 Test unitario 3 Test unitario 4 Test unitario 5 componente 2 unidad 2 unidad 1 ejercita ejercita ejercita ejercita usa usa
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
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: sistematización y
automatización
• 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.
#include <limits.h> #include "staticRoutines.h" #include "gtest/gtest.h" TEST(MaxTest, Positive) { int a = 19; int b = 4;
int res = max(a,b);
EXPECT_TRUE(res == a); }
act: se ejecuta el programa con los datos construidos.
assert: se evalúa si los resultados obtenidos se corresponden con lo esperado.
Estructura de un test
arrange: se preparan los datos para alimentar al programa.
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS();
}
se inicializa el framework
se ejecutan todos los test
Ejecución de tests
• Google Test necesita un runner explícito,
• que detecta automáticamente todos los test, y
Demo
• 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
• 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.
• 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.
• 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.
• 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.
• Consideremos un ejemplo de testing de una clase
Minefield, que representa el estado de un campo minado en el juego “Buscaminas”: class Mine { bool mined; bool marked; bool opened; public: Mine();
void setMined(bool isMined); bool isMined();
void setMarked(bool isMarked); bool isMarked();
void setOpened(bool isOpened); bool isOpened();
! };
• Para poder testear minedNeighbours, debemos crear el
minefield, y ubicar minas en lugares específicos.
Suites de test y datos compartidos:
un ejemplo
#include "Mine.h" class Minefield { Mine field[8][8]; ! public:void open(int x, int y); void close(int x, int y); void mark(int x, int y); void unmark(int x, int y); void putMine(int x, int y); void removeMine(int x, int y);
int minedNeighbours(int x, int y);! };
• El proceso setUp en el siguiente ejemplo crea el escenario
adecuado para la ejecución de tests de minedNeighbours.
• Se ejecuta antes de cada test.
#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); ! ! testingField.putMine(0, 7); ! ! testingField.putMine(7, 7); ! ! testingField.putMine(5, 1); ! ! testingField.putMine(4, 7); ! } };
Suites de test y datos compartido:
un ejemplo
x x xx
x x
x
x
TEST_F(MinefieldTest, NoOfMinedNeighbours01) {
int number = testingField.minedNeighbours(1, 0); ASSERT_EQ(1, number);
};
TEST_F(MinefieldTest, NoOfMinedNeighbours02) {
int number = testingField.minedNeighbours(7, 7); ASSERT_EQ(0, number);
• 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.
• Las rutinas SetUp y TearDown ayudan a setear el contexto
entre diferentes tests.
• El método SetUp se ejecuta antes de cada test de la suite.
• El método TearDown se ejecuta después de cada test de la suite.
Sobre setUp y tearDown
testingfield = new Minefield(); testingfield.SetUp();
NoOfMinedNeighbours01; testingfield.TearDown(); free(testingfield);
testingfield = new Minefield(); testingfield.SetUp();
NoOfMinedNeighbours02; testingfield.TearDown(); free(testingfield);
• GoogleTest asegura la independencia entre test (aunque se debe tener cuidado con datos persistentes).
Independencia entre Tests
class MineTest : public ::testing::Test { !
protected: Mine mine; };
TEST_F(MineTest, Marked) { mine.setMarked(true);
ASSERT_TRUE(mine.isMarked() && !mine.isOpened()); };
TEST_F(MineTest, Opened) { mine.setOpened(true);
ASSERT_TRUE(!mine.isMarked() && mine.isOpened()); };
✓
class MineTest : public ::testing::Test { !
protected: Mine *mine; };
TEST_F(MineTest, Marked) { mine = new Mine();
mine->setMarked(true);
ASSERT_TRUE(mine->isMarked() && !mine->isOpened()); };
TEST_F(MineTest, Opened) { mine->setOpened(true);
ASSERT_TRUE(!mine->isMarked() && mine->isOpened()); };
✗
class MineTest : public ::testing::Test { !
protected:
static void SetUpTestCase() { mine = new Mine();
}
static Mine* mine; };
Mine* MineTest::mine = NULL;
TEST_F(MineTest, Marked) { mine->setMarked(true);
ASSERT_TRUE(mine->isMarked() && !mine->isOpened()); };
TEST_F(MineTest, Opened) { mine->setOpened(true);
ASSERT_TRUE(!mine->isMarked() && mine->isOpened()); };
Demo
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
• 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.
Tests “parametrizados” por los datos
que manipulan
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;
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) ));
Indica que la suite está formada por tests parametrizados.
Test paramétrico (TEST_P)
Datos de los test
Test parametrizados: un ejemplo
Test parametrizados
• El runner genera una instancia de cada test por cada dato: • MaxOnLeft/MaxTest.Test/0 para (2,1)
• MaxOnLeft/MaxTest.Test/1 para (42,2)
• MaxOnLeft/MaxTest.Test/2 para (-12,-90)
• Puede además haber más de una generador de datos
(entonces se genera una instancia por cada test, por cada dato de cada generador).
• GoogleTest define una serie de funciones para definir
generadores de datos:
Generador Descripción
Range(begin, end, step) el conjunto de valores en el rango comenzando en begin, terminando en end (no incluido) de a step
incrementos.
Values(v1,v2,...,vn) el conjunto de valores {v1, v2, ..., vn}.
ValuesIn(container) el conjunto de valores del contenedor container.
Bool el conjunto de valores {true, false}.
Demo
• Max 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
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.
•