Cuando realizo pruebas unitarias de la manera "correcta", es decir, en cada llamada pública y devuelvo valores o simulaciones preestablecidas, siento que en realidad no estoy probando nada. Literalmente estoy mirando mi código y creando ejemplos basados en el flujo de la lógica a través de mis métodos públicos.
Esto parece que el método que está probando necesita varias otras instancias de clase (de las que tiene que burlarse), y llama a varios métodos por sí solo.
Este tipo de código es realmente difícil de probar por unidad, por las razones que usted describe.
Lo que he encontrado útil es dividir tales clases en:
- Clases con la "lógica de negocios" real. Estos usan pocas o ninguna llamada a otras clases y son fáciles de probar (valor (es) dentro - valor fuera).
- Clases que interactúan con sistemas externos (archivos, bases de datos, etc.). Estos envuelven el sistema externo y proporcionan una interfaz conveniente para sus necesidades.
- Clases que "unen todo"
Luego, las clases de 1. son fáciles de probar por unidad, porque solo aceptan valores y devuelven un resultado. En casos más complejos, estas clases pueden necesitar realizar llamadas por su cuenta, pero solo llamarán a las clases desde 2. (y no llamarán directamente, por ejemplo, a una función de base de datos), y las clases desde 2. son fáciles de burlar (porque solo exponga las partes del sistema envuelto que necesita).
Las clases de 2. y 3. por lo general no se pueden probar de manera significativa en la unidad (porque no hacen nada útil por sí mismas, solo son códigos de "pegamento"). OTOH, estas clases tienden a ser relativamente simples (y pocas), por lo que deberían cubrirse adecuadamente con pruebas de integración.
Un ejemplo
Una clase
Supongamos que tiene una clase que recupera un precio de una base de datos, aplica algunos descuentos y luego actualiza la base de datos.
Si tiene todo esto en una clase, deberá llamar a las funciones de DB, que son difíciles de burlar. En pseudocódigo:
1 select price from database
2 perform price calculation, possibly fetching parameters from database
3 update price in database
Los tres pasos necesitarán acceso a la base de datos, por lo que una gran cantidad de burlas (complejas), que probablemente se rompan si el código o la estructura de la base de datos cambian.
Separar
Se divide en tres clases: PriceCalculation, PriceRepository, App.
PriceCalculation solo realiza el cálculo real y obtiene los valores que necesita. La aplicación une todo:
App:
fetch price data from PriceRepository
call PriceCalculation with input values
call PriceRepository to update prices
De esa manera:
- PriceCalculation encapsula la "lógica empresarial". Es fácil de probar porque no llama a nada por sí solo.
- PriceRepository se puede probar con pseudounidades configurando una base de datos simulada y probando las llamadas de lectura y actualización. Tiene poca lógica, por lo tanto, pocas rutas de código, por lo que no necesita demasiadas de estas pruebas.
- La aplicación no se puede probar de manera significativa en la unidad, porque es un código de pegamento. Sin embargo, también es muy simple, por lo que las pruebas de integración deberían ser suficientes. Si más tarde la aplicación se vuelve demasiado compleja, rompes más clases de "lógica de negocios".
Finalmente, puede resultar que PriceCalculation debe hacer sus propias llamadas a la base de datos. Por ejemplo, porque solo PriceCalculation sabe qué datos necesita, por lo que App no puede obtenerlos por adelantado. Luego puede pasarle una instancia de PriceRepository (o alguna otra clase de repositorio), adaptada a las necesidades de PriceCalculation. Entonces será necesario burlarse de esta clase, pero será simple, porque la interfaz de PriceRepository es simple, por ejemplo PriceRepository.getPrice(articleNo, contractType)
. Lo más importante es que la interfaz de PriceRepository aísla PriceCalculation de la base de datos, por lo que es poco probable que los cambios en el esquema de la base de datos o la organización de datos cambien su interfaz y, por lo tanto, rompan las simulaciones.