¿Por qué necesito pruebas unitarias para probar métodos de repositorio?

19

Necesito hacer un poco de defensa de los demonios en esta cuestión porque no puedo defenderla bien por falta de experiencia. Aquí está el trato, obtengo conceptualmente las diferencias entre las pruebas unitarias y las pruebas de integración. Al enfocarse específicamente en los métodos de persistencia y el repositorio, una prueba unitaria usaría un simulacro posiblemente a través de un marco como Moq para afirmar que una orden buscada se devolvió como se esperaba.

Digamos que he construido la siguiente prueba de unidad:

[TestMethod]
public void GetOrderByIDTest()
{
   //Uses Moq for dependency for getting order to make sure 
   //ID I set up in 'Arrange' is same one returned to test in 'Assertion'
}

Entonces, si configuro OrderIdExpected = 5y mi objeto simulado regresa 5como ID, mi prueba pasará. Lo entiendo. Me unidad a prueba el código para asegurarse de que lo que mis preformas de código devuelve el objeto esperado y el ID y no otra cosa.

El argumento que obtendré es este:

"¿Por qué no simplemente omitir las pruebas unitarias y hacer pruebas de integración? Es importante probar el procedimiento almacenado de la base de datos y su código juntos. Parece demasiado trabajo adicional tener pruebas unitarias y pruebas de integración cuando finalmente quiero saber si la base de datos llama y el código funciona. Sé que las pruebas llevan más tiempo, pero tienen que ejecutarse y probarse de todos modos, por lo que me parece inútil tener ambas. Simplemente pruebe contra lo que importa ".

Podría defenderlo con una definición de libro de texto como: "Bueno, esa es una prueba de integración y tenemos que probar el código por separado como una prueba unitaria y, yada, yada, yada ..." Este es un caso donde una explicación purista de las prácticas vs. la realidad se está perdiendo. A veces me encuentro con esto y si no puedo defender el razonamiento detrás del código de prueba de la unidad que finalmente depende de dependencias externas, entonces no puedo defenderlo.

Cualquier ayuda sobre esta pregunta es muy apreciada, ¡gracias!

atconway
fuente
2
digo enviarlo directamente a las pruebas de usuario ... van a cambiar los requisitos de todos modos ...
nathan hayfield
+1 para que el sarcasmo mantenga las cosas ligeras de vez en cuando
concierto el
2
La respuesta simple es que cada método puede tener x número de casos límite (digamos que desea probar ID positivas, ID de 0 e ID negativas). Si desea probar alguna funcionalidad que utiliza este método, que tiene 3 casos límite, necesitaría escribir 9 casos de prueba para probar cada combinación de casos límite. Al aislarlos, solo necesita escribir 6. Además, las pruebas le dan una idea más específica de por qué algo se rompió. Quizás su repositorio devuelva nulo en caso de falla, y la excepción nula se arroja varios cientos de líneas por el código.
Rob
Creo que está siendo demasiado estricto en su definición de una prueba de "unidad". ¿Qué es una "unidad de trabajo" para una clase de repositorio?
Caleb
Y asegúrese de considerar esto: si su prueba de unidad se burla de todo lo que está tratando de probar, ¿qué está realmente probando?
Caleb

Respuestas:

20

Las pruebas unitarias y las pruebas de integración tienen diferentes propósitos.

Las pruebas unitarias verifican la funcionalidad de su código ... que obtiene lo que espera del método cuando lo llama. Las pruebas de integración prueban cómo se comporta el código cuando se combinan juntas como un sistema. No esperaría que las pruebas unitarias evaluaran el comportamiento del sistema, ni esperaría que las pruebas de integración verificaran resultados específicos de un método en particular.

Las pruebas unitarias, cuando se realizan correctamente, son más fáciles de configurar que las pruebas de integración. Si confía únicamente en las pruebas de integración, sus pruebas van a:

  1. Ser más difícil de escribir, en general,
  2. Sea más frágil, debido a todas las dependencias requeridas, y
  3. Ofrecer menos cobertura de código.

En pocas palabras: utilice las pruebas de integración para verificar que las conexiones entre los objetos funcionen correctamente, pero primero apóyese en las pruebas unitarias para verificar los requisitos funcionales.


Dicho todo esto, es posible que no necesite pruebas unitarias para ciertos métodos de repositorio. Las pruebas unitarias no deben hacerse con métodos triviales ; Si todo lo que está haciendo es pasar una solicitud de un objeto al código generado por ORM y devolver un resultado, no necesita realizar una prueba unitaria, en la mayoría de los casos; Una prueba de integración es adecuada.

Robert Harvey
fuente
Sí, gracias por la rápida respuesta, lo aprecio. Mi única crítica de la respuesta es que cae en la preocupación de mi último párrafo. Mi oposición seguirá escuchando explicaciones y definiciones abstractas y no un razonamiento más profundo. Por ejemplo, ¿podría proporcionar un razonamiento aplicado a mi caso de uso para probar el procedimiento almacenado / DB en relación con mi código y por qué las pruebas unitarias siguen siendo valiosas en referencia a este caso de uso en particular ?
atconway
1
Si el SP solo devuelve un resultado de la base de datos, una prueba de integración puede ser adecuada. Si tiene lógica no trivial, se indican las pruebas unitarias. Consulte también stackoverflow.com/questions/1126614/… y msdn.microsoft.com/en-us/library/aa833169(v=vs.100).aspx (específico de SQL Server).
Robert Harvey
12

Estoy del lado de los pragmáticos. No pruebe su código dos veces.

Solo escribimos pruebas de integración para nuestros repositorios. Estas pruebas solo dependen de una configuración de prueba simple que se ejecuta en una base de datos en memoria. Creo que proporcionan todo lo que hace una prueba unitaria, y más.

  1. Pueden sustituir las pruebas unitarias al hacer TDD. Si bien hay algo más de código de prueba para escribir antes de que pueda comenzar con el código real, funciona muy bien con el enfoque rojo / verde / refactor una vez que todo esté en su lugar.
  2. Prueban el código real del repositorio: el código contenido en cadenas SQL o comandos ORM. Es más importante verificar que la consulta sea correcta que verificar que realmente haya enviado alguna cadena a un StatementExecutor.
  3. Son excelentes como pruebas de regresión. Si fallan, siempre se debe a un problema real, como un cambio en el esquema que no se ha tenido en cuenta.
  4. Son indispensables al cambiar el esquema de la base de datos. Puede estar seguro de que no ha cambiado el esquema de una manera que rompa su aplicación siempre que pase la prueba. (La prueba unitaria es inútil en este caso, porque cuando el procedimiento almacenado ya no exista, la prueba unitaria aún pasará).
Waxwing
fuente
Muy pragmático de hecho. He experimentado el caso de que la base de datos en memoria realmente se comporta de manera diferente (no en términos de rendimiento) a mi base de datos real, debido a un ORM ligeramente diferente. Tenga cuidado con eso en sus pruebas de integración.
Marcel
1
@ Marcel, me he encontrado con eso. Lo resolví ejecutando a veces todas mis pruebas contra una base de datos real.
Winston Ewert
4

Las pruebas unitarias proporcionan un nivel de modularidad que las pruebas de integración (por diseño) no pueden. Cuando un sistema es reprogramado o recompone, (y esto va a pasar) las pruebas de unidad a menudo pueden ser reutilizados, mientras que las pruebas de integración a menudo deben ser re-escrito. Las pruebas de integración que intentan hacer el trabajo de algo que debería estar en una prueba unitaria a menudo hacen demasiado, lo que las hace difíciles de mantener.

Además, incluir pruebas unitarias tiene los siguientes beneficios:

  1. Las pruebas unitarias le permiten descomponer rápidamente una prueba de integración fallida (posiblemente debido a un error de regresión) e identificar la causa. Además, esto comunicará el problema a todo el equipo más rápidamente que los diagramas u otra documentación.
  2. Las pruebas unitarias pueden servir como ejemplos y documentación (un tipo de documentación que realmente se compila ) junto con su código. A diferencia de otra documentación, sabrá instantáneamente cuando esté desactualizado.
  3. Las pruebas unitarias pueden servir como indicadores de rendimiento de referencia cuando se intenta descomponer problemas de rendimiento más grandes, mientras que las pruebas de integración tienden a requerir mucha instrumentación para encontrar el problema.

Es posible dividir su trabajo (y aplicar un enfoque DRY) mientras usa las pruebas de Unidad e Integración. Simplemente confíe en pruebas unitarias para unidades pequeñas de funcionalidad, y no repita ninguna lógica que ya esté en una prueba unitaria en una prueba de integración. Esto a menudo conducirá a menos trabajo (y, por lo tanto, menos reelaboración).

Zachary Yates
fuente
4

Las pruebas son útiles cuando se rompen: de forma proactiva o reactiva.

Las pruebas unitarias son proactivas y pueden ser una verificación funcional continua, mientras que las pruebas de integración son reactivas en datos falsos / por etapas.

Si el sistema en consideración es más cercano / dependiente de los datos que de la lógica de cálculo, las pruebas de integración deberían tener más importancia.

Por ejemplo, si estamos construyendo un sistema ETL, las pruebas más relevantes serán en torno a los datos (por etapas, falsificados o en vivo). El mismo sistema tendrá algunas pruebas unitarias, pero solo en torno a la validación, el filtrado, etc.

El patrón de repositorio no debe tener lógica computacional o de negocios. Está muy cerca de la base de datos. Pero el repositorio también está cerca de la lógica de negocios que lo usa. Por lo tanto, se trata de alcanzar un EQUILIBRIO.

Las pruebas unitarias pueden probar CÓMO se comporta el repositorio. Las pruebas de integración pueden probar LO QUE REALMENTE OCURRIÓ cuando se llamó al repositorio.

Ahora, "LO QUE REALMENTE SUCEDIÓ" podría parecer muy tentador. Pero esto podría ser muy costoso para ejecutar de manera consistente y repetida. La definición clásica de pruebas frágiles se aplica aquí.

Entonces, la mayoría de las veces, me parece lo suficientemente bueno como para escribir la prueba de la Unidad en un objeto que usa el repositorio. De esta forma, probamos si se llamó al método de repositorio adecuado y se devuelven los resultados simulados adecuados.

Otpidus
fuente
Me gusta esto: "Ahora," LO QUE REALMENTE SUCEDIÓ "podría parecer muy atractivo. Pero esto podría ser muy costoso para correr de manera consistente y repetida".
atconway
2

Hay 3 cosas separadas que necesitan pruebas: el procedimiento almacenado, el código que llama al procedimiento almacenado (es decir, su clase de repositorio) y el consumidor. El trabajo del repositorio es generar una consulta y convertir el conjunto de datos devuelto en un objeto de dominio. Hay suficiente código para admitir pruebas unitarias que se separan de la ejecución real de la consulta y la creación del conjunto de datos.

Entonces (este es un ejemplo muy muy simplificado):

interface IOrderRepository
{
    Order GetOrderByID(Guid id);
}

class OrderRepository : IOrderRepository
{
    private readonly ISqlExecutor sqlExecutor;
    public OrderRepository(ISqlExecutor sqlExecutor)
    {
        this.sqlExecutor = sqlExecutor;
    }

    public Order GetOrderByID(Guid id)
    {
        var sql = "SELECT blah, blah FROM Order WHERE OrderId = @p0";
        var dataset = this.sqlExecutor.Execute(sql, p0 = id);
        var result = this.orderFromDataset(dataset);
        return result;
    }
}

Luego, cuando realice la prueba OrderRepository, pase una prueba simulada ISqlExecutory verifique que el objeto bajo prueba pasa en el SQL correcto (ese es su trabajo) y devuelve un Orderobjeto apropiado dado algún conjunto de datos de resultados (también burlado). Probablemente, la única forma de probar la SqlExecutorclase concreta es con pruebas de integración, lo suficientemente justo, pero esa es una clase de envoltura delgada y rara vez cambiará, por lo que es un gran problema.

Todavía tiene que probar la unidad de sus procedimientos almacenados también.

Scott Whitlock
fuente
0

Puedo reducir esto a una línea de pensamiento muy simple: favorece las pruebas unitarias porque ayuda a localizar errores. Y haga esto constantemente porque las inconsistencias causan confusión.

Otras razones se derivan de las mismas reglas que guían el desarrollo de OO, ya que también se aplican a las pruebas. Por ejemplo, el principio de responsabilidad única.

Si algunas pruebas unitarias parecen no valer la pena, entonces tal vez sea una señal de que el tema realmente no vale la pena. O tal vez su funcionalidad es más abstracta que su contraparte, y las pruebas para ello (así como su código) pueden abstraerse a un nivel superior.

Como con cualquier cosa, hay excepciones. Y la programación sigue siendo algo así como una forma de arte, por lo que muchos problemas son lo suficientemente diferentes como para justificar la evaluación de cada uno para el mejor enfoque.

CL22
fuente