¿Debería codificar sus datos en todas las pruebas unitarias?

33

La mayoría de los tutoriales / ejemplos de pruebas unitarias que existen suelen incluir la definición de los datos que se probarán para cada prueba individual. Supongo que esto es parte de la teoría de "todo debe ser probado de forma aislada".

Sin embargo, descubrí que cuando se trata de aplicaciones de varios niveles con una gran cantidad de DI , el código requerido para configurar cada prueba se queda muy largo. En cambio, he creado una serie de clases testbase que ahora puedo heredar, que tiene muchos andamios de prueba preconstruidos.

Como parte de esto, también estoy creando conjuntos de datos falsos que representan la base de datos de una aplicación en ejecución, aunque generalmente con solo una o dos filas en cada "tabla".

¿Es una práctica aceptada predefinir, si no todos, la mayoría de los datos de prueba en todas las pruebas unitarias?

Actualizar

De los comentarios a continuación, parece que estoy haciendo más integración que pruebas unitarias.

Mi proyecto actual es ASP.NET MVC, que utiliza la Unidad de trabajo sobre Entity Framework Code First y Moq para las pruebas. Me he burlado de la UoW y los repositorios, pero estoy usando las clases de lógica de negocios reales y probando las acciones del controlador. Las pruebas a menudo verifican que la UoW se haya comprometido, por ejemplo:

[TestClass]
public class SetupControllerTests : SetupControllerTestBase {
  [TestMethod]
  public void UserInvite_ExistingUser_DoesntInsertNewUser() {
    // Arrange
    var model = new Mandy.App.Models.Setup.UserInvite() {
      Email = userData.First().Email
    };

    // Act
    setupController.UserInvite(model);

    // Assert
    mockUserSet.Verify(m => m.Add(It.IsAny<UserProfile>()), Times.Never);
    mockUnitOfWork.Verify(m => m.Commit(), Times.Once);
  }
}

SetupControllerTestBaseestá construyendo el UoW simulado e instanciando el userLogic.

Muchas de las pruebas requieren tener un usuario o producto existente en la base de datos, por lo que he completado previamente lo que devuelve el UoW simulado, en este ejemplo userData, que es solo un IList<User>registro de usuario único.

mattdwen
fuente
44
El problema con los tutoriales / ejemplos es que deben ser simples, pero no puede mostrar la solución a un problema complejo en un ejemplo simple. Deben ir acompañados de "estudios de caso" que describan cómo se utiliza la herramienta en proyectos reales de tamaño razonable, pero rara vez lo son.
Jan Hudec
Tal vez podría agregar algunos pequeños ejemplos de código que no le satisfacen totalmente.
Luc Franken
Si necesita mucho código de configuración para ejecutar una prueba, corre el riesgo de ejecutar una prueba funcional. Si la prueba falla cuando cambia el código, pero no hay nada de malo en el código. Definitivamente es una prueba funcional.
Reactgular
El libro "Patrones de prueba xUnit" presenta un argumento sólido para accesorios y ayudantes reutilizables. El código de prueba debe ser tan fácil de mantener como cualquier otro código.
Chuck Krutsinger el
Este artículo puede ser útil: yegor256.com/2015/05/25/unit-test-scaffolding.html
yegor256

Respuestas:

25

En última instancia, desea escribir el menor código posible para obtener el mayor resultado posible. Tener mucho del mismo código en múltiples pruebas a) tiende a dar como resultado una codificación de copiar y pegar yb) significa que si cambia la firma de un método, puede terminar teniendo que arreglar muchas pruebas rotas.

Utilizo el enfoque de tener clases TestHelper estándar que me brindan muchos de los tipos de datos que uso habitualmente, para poder crear conjuntos de entidades estándar o clases DTO para que mis pruebas consulten y sepan exactamente qué obtendré cada vez. Entonces puedo llamar TestHelper.GetFooRange( 0, 100 )para obtener un rango de 100 objetos Foo con todas sus clases / campos dependientes establecidos.

Particularmente donde hay relaciones complejas configuradas en un sistema de tipo ORM que necesitan estar presentes para que las cosas se ejecuten correctamente, pero no son necesariamente significativas para esta prueba que puede ahorrar mucho tiempo.

En situaciones en las que estoy probando cerca del nivel de datos, a veces creo una versión de prueba de mi clase de repositorio que se puede consultar de manera similar (una vez más, esto es en un entorno de tipo ORM, y no sería relevante para un base de datos real), porque burlarse de las respuestas exactas a las consultas es mucho trabajo y, a menudo, solo proporciona beneficios menores.

Hay algunas cosas a tener en cuenta, aunque en las pruebas unitarias:

  • Asegúrese de que sus simulacros sean simulacros . Las clases que realizan operaciones alrededor de la clase que se está probando deben ser objetos simulados si está realizando pruebas unitarias. Sus clases de tipo DTO / entidad pueden ser reales, pero si las clases realizan operaciones, debe burlarse de ellas; de lo contrario, cuando el código de soporte cambie y sus pruebas comiencen a fallar, tendrá que buscar mucho más tiempo para descubrir qué cambio En realidad causó el problema.
  • Asegúrate de probar tus clases . A veces, si uno mira a través de un conjunto de pruebas unitarias, se hace evidente que la mitad de las pruebas realmente están probando el marco de simulación más que el código real que se supone que deben probar.
  • No reutilice los objetos simulados / de apoyo Esto es un problema: cuando uno comienza a tratar de ser inteligente con las pruebas de unidad de soporte de código, es realmente fácil crear inadvertidamente objetos que persisten entre las pruebas, lo que puede tener efectos impredecibles. Por ejemplo, ayer tuve una prueba que pasó cuando se ejecutó sola, pasó cuando se ejecutaron todas las pruebas en la clase, pero falló cuando se ejecutó todo el conjunto de pruebas. Resultó que había un objeto estático furtivo en un asistente de prueba que, cuando lo creé, definitivamente nunca habría causado un problema. Solo recuerde: al comienzo de la prueba, todo se crea, al final de la prueba todo se destruye.
glenatron
fuente
10

Lo que sea que haga que la intención de su prueba sea más legible.

Como regla general:

Si los datos son parte de la prueba (p. Ej., No deben imprimir filas con un estado de 7), codifíquelos en la prueba para que quede claro lo que el autor pretendía que sucediera.

Si los datos son solo de relleno para asegurarse de que tienen algo con lo que trabajar (por ejemplo, no deberían marcar el registro como completo si el servicio de procesamiento arroja una excepción), entonces tenga un método BuildDummyData o una clase de prueba que mantenga los datos irrelevantes fuera de la prueba .

Pero tenga en cuenta que me cuesta pensar en un buen ejemplo de esto último. Si tiene muchos de estos en un dispositivo de prueba de unidad, probablemente tenga un problema diferente que resolver ... tal vez el método bajo prueba es demasiado complejo.

pdr
fuente
+1 estoy de acuerdo. Esto huele a lo que está probando es estar estrechamente acoplado para pruebas unitarias.
Reactgular
5

Diferentes métodos de prueba

Primero defina lo que está haciendo: Prueba de unidad o prueba de integración . El número de capas es irrelevante para las pruebas unitarias, ya que solo es probable que pruebe una clase. El resto te burlas. Para las pruebas de integración es inevitable que pruebe varias capas. Si tiene buenas pruebas unitarias, el truco es hacer que las pruebas de integración no sean demasiado complejas.

Si sus pruebas unitarias son buenas, no tiene que repetir las pruebas con todos los detalles al hacer las pruebas de integración.

Los términos que utilizamos dependen de la plataforma, pero puede encontrarlos en casi todas las plataformas de prueba / desarrollo:

Aplicación de ejemplo

Dependiendo de la tecnología que use, los nombres pueden diferir, pero lo usaré como ejemplo:

Si tiene una aplicación CRUD simple con el modelo de Producto, ProductsController y una vista de índice que genera una tabla HTML con productos:

El resultado final de la aplicación muestra una tabla HTML con una lista de todos los productos que están activos.

Examen de la unidad

Modelo

El modelo que puedes probar con bastante facilidad. Hay diferentes métodos para ello; Usamos accesorios. Creo que eso es lo que llamas "conjuntos de datos falsos". Entonces, antes de ejecutar cada prueba, creamos la tabla y colocamos los datos originales. La mayoría de las plataformas tienen métodos para esto. Por ejemplo, en su clase de prueba, un método setUp () que se ejecuta antes de cada prueba.

Luego ejecutamos nuestra prueba, por ejemplo: productos testGetAllActive .

Entonces probamos directamente a una base de datos de prueba. No nos burlamos de la fuente de datos; Lo hacemos siempre igual. Esto nos permite, por ejemplo, probar con una nueva versión de la base de datos, y surgirán problemas de consulta.

En el mundo real, no siempre se puede seguir el 100% de responsabilidad individual . Si desea hacerlo aún mejor, puede usar una fuente de datos de la que se burla. Para nosotros (usamos un ORM) que se siente como probar tecnología ya existente. Además, las pruebas se vuelven mucho más complejas y realmente no prueban las consultas. Entonces lo mantenemos de esta manera.

Los datos codificados se almacenan por separado en los dispositivos. Por lo tanto, el dispositivo es como un archivo SQL con una instrucción de creación de tabla e inserciones para los registros que utilizamos. Los mantenemos pequeños a menos que haya una necesidad real de realizar pruebas con muchos registros.

class ProductModel {
  public function getAllActive() {
    return $this->find('all', array('conditions' => array('active' => 1)));
  }
}

Controlador

El controlador necesita más trabajo, porque no queremos probar el modelo con él. Entonces, lo que hacemos es burlarnos del modelo. Eso significa: Probamos: método index () que debería devolver una lista de registros.

Así que nos burlamos del método de modelo getAllActive () y agregamos datos fijos (dos registros, por ejemplo). Ahora probamos los datos que el controlador envía a la vista y comparamos si realmente recuperamos esos dos registros.

function testProductIndexLoggedIn() {
  $this->setLoggedIn();
  $this->ProductsController->mock('ProductModel', 'index', function(return array(your records) ));
  $result=$this->ProductsController->index();
  $this->assertEquals(2, count($result['products']));
}

Eso es suficiente. Intentamos agregar tan poca funcionalidad al controlador porque eso dificulta las pruebas. Pero, por supuesto, siempre hay algo de código en él. Por ejemplo, probamos requisitos como: Mostrar esos dos registros solo si ha iniciado sesión.

Por lo tanto, el controlador necesita un simulacro normalmente y una pequeña pieza de datos codificados. Para un sistema de inicio de sesión, tal vez otro. En nuestra prueba tenemos un método auxiliar para ello: setLoggedIn (). Eso simplifica la prueba con inicio de sesión o sin inicio de sesión.

class ProductsController {
  public function index() {
    if($this->loggedIn()) {
      $this->set('products', $this->ProductModel->getAllActive());
    }
  }
}

Puntos de vista

Las pruebas de vistas son difíciles. Primero separamos la lógica que se repite. Lo ponemos en Helpers y probamos esas clases estrictamente. Esperamos siempre la misma salida. Por ejemplo, generateHtmlTableFromArray ().

Luego tenemos algunas vistas específicas del proyecto. No los probamos. Realmente no es deseable probarlos. Los guardamos para pruebas de integración. Debido a que eliminamos gran parte del código en las vistas, aquí tenemos un riesgo menor.

Si comienza a probarlos, es probable que necesite cambiar sus pruebas cada vez que cambie una pieza de HTML que no es útil para la mayoría de los proyectos.

echo $this->tableHelper->generateHtmlTableFromArray($products);

Pruebas de integración

Dependiendo de su plataforma aquí, puede trabajar con historias de usuarios, etc. Puede estar basado en la web como Selenium u otras soluciones comparables.

En general, simplemente cargamos la base de datos con los dispositivos y afirmamos qué datos deberían estar disponibles. Para las pruebas de integración completa generalmente usamos requisitos muy globales. Entonces: configure el producto como activo y luego verifique si el producto está disponible.

No volvemos a probar todo, como si los campos correctos están disponibles. Probamos los requisitos más grandes aquí. Dado que no queremos duplicar nuestras pruebas desde el controlador o la vista. Si algo es realmente clave / parte central de su aplicación o por razones de seguridad (verifique que la contraseña NO esté disponible), las agregamos para asegurarnos de que sea correcta.

Los datos codificados se almacenan en los dispositivos.

function testIntegrationProductIndexLoggedIn() {
  $this->setLoggedIn();
  $result=$this->request('products/index');

  $expected='<table';
  $this->assertContains($expected, $result);

  // Some content from the fixture record
  $expected='<td>Product 1 name</td>';
  $this->assertContains($expected, $result);
}
Luc Franken
fuente
Esta es una gran respuesta, a una pregunta completamente diferente.
pdr
Gracias por la respuesta. Puede que tengas razón en que no lo mencioné demasiado específico. La razón de la respuesta detallada es porque veo una de las cosas más difíciles al probar en la pregunta que se hace. El resumen de cómo las pruebas aisladas se ajustan a los diferentes tipos de pruebas. Es por eso que agregué en cada parte cómo se manejan (o separan) los datos. Echaré un vistazo para ver si puedo aclararlo.
Luc Franken
La respuesta se ha actualizado con algunos ejemplos de código para explicar cómo realizar la prueba sin llamar a todo tipo de otras clases.
Luc Franken
4

Si está escribiendo pruebas que implican una gran cantidad de DI y cableado, hasta el uso de fuentes de datos "reales", probablemente abandonó el área de pruebas de unidades simples e ingresó al dominio de las pruebas de integración.

Para las pruebas de integración, creo, no es mala idea tener una lógica de configuración de datos común. El objetivo principal de tales pruebas es demostrar que todo está configurado correctamente. Esto es bastante independiente de los datos concretos enviados a través de su sistema.

Por otro lado, para las pruebas de Unidad, recomendaría mantener el objetivo de una clase de prueba en una sola clase "real" y burlarse de todo lo demás. Entonces, realmente debería codificar los datos de prueba para asegurarse de que cubrió tantas rutas de errores especiales / anteriores como sea posible.

Para agregar un elemento semi-codificado / aleatorio a las pruebas, me gusta presentar fábricas de modelos aleatorios. En una prueba que usa una instancia de mi modelo, luego uso estas fábricas para crear un objeto de modelo válido pero completamente aleatorio y luego codifico solo las propiedades que son de interés para la prueba en cuestión. De esta manera, usted especifica todos los datos relevantes directamente en su prueba, mientras le ahorra la necesidad de especificar también todos los datos irrelevantes y (hasta cierto punto) prueba de que no hay dependencias no deseadas en otros campos del modelo.

Sven Amann
fuente
-1

Creo que es bastante común codificar la mayoría de los datos para sus pruebas.

Considere una situación simple en la que un conjunto de datos en particular provoca un error. Puede crear específicamente una prueba unitaria para esos datos para ejercer la corrección y asegurarse de que el error no regrese. Con el tiempo, sus pruebas tendrán un conjunto de datos que cubren varios casos de prueba.

Los datos de prueba predefinidos también le permiten crear un conjunto de datos que cubre una amplia y conocida gama de situaciones.

Dicho esto, creo que también tiene valor tener algunos datos aleatorios en sus pruebas.

Sasbury
fuente
¿Realmente leíste la pregunta y no solo el título?
Jakob
valor en tener algunos datos aleatorios en sus pruebas : Sí, porque no hay nada como tratar de averiguar qué sucedió en una prueba la única vez que falla cada semana.
pdr
Es valioso tener datos aleatorios en sus pruebas para pruebas de novatadas / fuzzing / input. Pero no en las pruebas de tu unidad, eso sería una pesadilla.
glenatron