Cómo lidiar con las clases de utilidad estática cuando se diseña para la comprobabilidad

63

Estamos tratando de diseñar nuestro sistema para que sea comprobable y en la mayoría de las partes desarrollado utilizando TDD. Actualmente estamos tratando de resolver el siguiente problema:

En varios lugares, es necesario que usemos métodos auxiliares estáticos como ImageIO y URLEncoder (ambos API Java estándar) y varias otras bibliotecas que consisten principalmente en métodos estáticos (como las bibliotecas Apache Commons). Pero es extremadamente difícil probar esos métodos que usan tales clases auxiliares estáticas.

Tengo varias ideas para resolver este problema:

  1. Use un marco simulado que pueda simular clases estáticas (como PowerMock). Esta puede ser la solución más simple, pero de alguna manera se siente como darse por vencido.
  2. Cree clases de contenedor instanciables alrededor de todas esas utilidades estáticas para que puedan inyectarse en las clases que las usan. Esto suena como una solución relativamente limpia, pero me temo que terminaremos creando una gran cantidad de esas clases de contenedor.
  3. Extraiga cada llamada a estas clases auxiliares estáticas en una función que se pueda anular y pruebe una subclase de la clase que realmente quiero probar.

Pero sigo pensando que esto solo tiene que ser un problema que muchas personas deben enfrentar al hacer TDD, por lo que ya debe haber soluciones para este problema.

¿Cuál es la mejor estrategia para mantener comprobables las clases que usan estos ayudantes estáticos?

Benedikt
fuente
No estoy seguro de lo que quiere decir con "fuentes confiables y / u oficiales", pero estoy de acuerdo con lo que @berecursive ha escrito en su respuesta. PowerMock existe por una razón y no debería sentirse como "darse por vencido", especialmente si no desea escribir clases de envoltura usted mismo. Los métodos finales y estáticos son difíciles cuando se trata de pruebas unitarias (y TDD). ¿Personalmente? Yo uso el método 2 que describiste.
Deco
"fuentes confiables y / u oficiales" es solo una de las opciones que puede seleccionar al comenzar una recompensa por una pregunta. Lo que realmente quiero decir: experiencias o referencias a artículos escritos por expertos de TDD. O cualquier tipo de experiencia de alguien que haya enfrentado el mismo problema ...

Respuestas:

34

(Me temo que no hay fuentes "oficiales" aquí; no es que haya una especificación sobre cómo hacer una buena prueba. Solo mis opiniones, que espero sean útiles).

Cuando estos métodos estáticos representan dependencias genuinas , cree envoltorios. Entonces para cosas como:

  • ImageIO
  • Clientes HTTP (o cualquier otra cosa relacionada con la red)
  • El sistema de archivos
  • Obtener la hora actual (mi ejemplo favorito de dónde ayuda la inyección de dependencia)

... tiene sentido crear una interfaz.

Pero muchos de los métodos en Apache Commons probablemente no deberían ser burlados / falsificados. Por ejemplo, tome un método para unir una lista de cadenas, agregando una coma entre ellas. No tiene sentido burlarse de estos: simplemente deje que la llamada estática haga su trabajo normal. No quiere ni necesita reemplazar el comportamiento normal; no se trata de un recurso externo o algo con lo que es difícil trabajar, solo son datos. El resultado es predecible y nunca querrás que sea otra cosa que lo que te dará de todos modos.

Sospecho que habiendo eliminado todas las llamadas estáticas que realmente son métodos convenientes con resultados predecibles, "puros" (como base64 o codificación URL) en lugar de puntos de entrada en un gran lío de dependencias lógicas (como HTTP), encontrará que es completamente práctico para hacer lo correcto con las dependencias genuinas.

Jon Skeet
fuente
20

Definitivamente esta es una pregunta / respuesta obvia, pero por lo que vale, pensé en invertir mis dos centavos. En términos del estilo de TDD, el método 2 es definitivamente el enfoque que sigue a la letra. El argumento para el método 2 es que si alguna vez quisiste reemplazar la implementación de una de esas clases, digamos una ImageIObiblioteca equivalente, entonces podrías hacerlo mientras mantienes la confianza en las clases que aprovechan ese código.

Sin embargo, como mencionó, si usa muchos métodos estáticos, terminará escribiendo mucho código contenedor. Esto podría no ser algo malo a largo plazo. En términos de mantenibilidad, ciertamente hay argumentos para esto. Personalmente, preferiría este enfoque.

Dicho esto, PowerMock existe por una razón. Es un problema bastante conocido que probar cuando hay métodos estáticos involucrados es muy doloroso, de ahí el inicio de PowerMock. Creo que debe sopesar sus opciones en términos de cuánto trabajo será envolver todas sus clases de ayuda frente al uso de PowerMock. No creo que sea 'darse por vencido' usar PowerMock, solo siento que envolver las clases le permite una mayor flexibilidad en un proyecto grande. Cuantos más contratos públicos (interfaces) pueda proporcionar, más limpia será la separación entre intención e implementación.


fuente
1
Un problema adicional sobre el que no estoy seguro: al implementar los contenedores, ¿implementaría todos los métodos de la clase que está empaquetada o solo los que se necesitan actualmente?
3
Al seguir ideas ágiles, debe hacer lo más simple que funcione y evitar hacer el trabajo que no necesita. Por lo tanto, debe exponer solo los métodos que realmente necesita.
Assaf Stone
@AssafStone estuvo de acuerdo
Tenga cuidado con PowerMock, toda la manipulación de clase que tiene que hacer para burlarse de los métodos conlleva una gran sobrecarga. Sus pruebas serán mucho más lentas si lo usa extensamente.
bcarlso
¿Realmente tiene que escribir mucho envoltorio si combina sus pruebas / migraciones con la adopción de una biblioteca DI / IoC?
4

Como referencia para todos los que también están lidiando con este problema y se encuentran con esta pregunta, voy a describir cómo decidimos abordar el problema:

Básicamente estamos siguiendo la ruta descrita como # 2 (clases de contenedor para utilidades estáticas). Pero solo los usamos cuando es demasiado complejo proporcionar a la utilidad los datos requeridos para producir la salida deseada (es decir, cuando absolutamente tenemos que burlarnos del método).

Esto significa que no tenemos que escribir un contenedor para una utilidad simple como Apache Commons StringEscapeUtils(porque las cadenas que necesitan pueden proporcionarse fácilmente) y no usamos simulacros para métodos estáticos (si creemos que podríamos necesitarlo, es hora de escribir una clase de contenedor y luego se burlan de una instancia del contenedor).

Benedikt
fuente
1

Trabajo para una importante compañía de seguros y nuestro código fuente sube a 400 MB de archivos java puros. Hemos estado desarrollando toda la aplicación sin pensar en TDD. A partir de enero de este año comenzamos con pruebas de junit para cada componente individual.

La mejor solución en nuestro departamento era usar objetos Mock en algunos métodos JNI que eran confiables para el sistema (escritos en C) y, como tal, no podía estimar exactamente los resultados cada vez en cada sistema operativo. No teníamos otra opción que usar clases simuladas e implementaciones específicas de métodos JNI específicamente con el propósito de probar cada módulo individual de la aplicación para cada sistema operativo que admitimos.

Pero fue realmente rápido y ha funcionado bastante bien hasta ahora. Lo recomiendo: http://www.easymock.org/


fuente
1

Los objetos interactúan entre sí para lograr un objetivo, cuando tiene un objeto difícil de probar debido al entorno (un punto final de servicio web, una capa de dao que accede a la base de datos, los controladores que manejan los parámetros de solicitud http) o si desea probar su objeto de forma aislada, luego te burlas de esos objetos.

la necesidad de burlarse de los métodos estáticos es un mal olor, debe diseñar su aplicación más orientada a objetos, y los métodos estáticos de utilidad de prueba de unidad no agregan mucho valor al proyecto, la clase de envoltura es un buen enfoque dependiendo de la situación, pero intente para probar aquellos objetos que usan los métodos estáticos.


fuente
1

A veces uso la opción 4

  1. Usa el patrón de estrategia. Cree una clase de utilidad con métodos estáticos que deleguen la implementación a una instancia de interfaz conectable. Codifique un inicializador estático que se conecte a una implementación concreta. Conecte una implementación simulada para realizar pruebas.

Algo como esto.

public class DateUtil {
    public interface ITimestampGenerator {
        long getUtcNow();
    }

    class ConcreteTimestampGenerator implements ITimestampGenerator {
        public long getUtcNow() { return System.currentTimeMillis(); }
    }

    private static ITimestampGenerator timestampGenerator;

    static {
        timestampGenerator = new ConcreteTimeStampGenerator;
    }

    public static DateTime utcNow() {
        return new DateTime(timestampGenerator.getUtcNow(), DateTimeZone.UTC);
    }

    public static void setTimestampGenerator(ITimestampGenerator t) {...}

    // plus other util routines, which may or may not use the timestamp generator 
}

Lo que me gusta de este enfoque es que mantiene los métodos de utilidad estáticos, lo que me parece correcto cuando intento usar la clase en todo el código.

Math.sum(17, 29, 42);
// vs
new Math().sum(17, 29, 42);
bigh_29
fuente