¿Cómo lidiar con las clases de utilidad estática al diseñar para la capacidad de prueba?

62

Estamos intentando diseñar nuestro sistema para que sea verificable y, en la mayoría de los casos, desarrollado con TDD. Actualmente estamos tratando de resolver el siguiente problema:

En varios lugares es necesario que utilicemos 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 de Apache Commons). Pero es extremadamente difícil probar los métodos que usan tales clases de ayuda estática.

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 rendirse.
  2. Cree clases de envoltura instantáneas alrededor de todas las utilidades estáticas para que puedan ser inyectadas 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 envoltorio.
  3. Extraiga todas las llamadas a estas clases auxiliares estáticas en una función que pueda ser anulada 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 tienen que enfrentar al hacer TDD, por lo que ya debe haber soluciones para este problema.

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

    
pregunta Benedikt 27.04.2012 - 10:48

7 respuestas

33

(No hay fuentes "oficiales" aquí, me temo que no hay una especificación sobre cómo probar bien. Solo mis opiniones, que espero sean útiles).

Cuando estos métodos estáticos representan dependencias genuinas , cree envoltorios. Así que 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 hay ningún punto en burlarse de estos, simplemente deje que la llamada estática haga su trabajo normal. No quieres o necesitas reemplazar el comportamiento normal; no estás tratando con un recurso externo o algo con lo que es difícil trabajar, solo son datos. El resultado es predecible y nunca querrías que fuera algo distinto de lo que te dará de todos modos.

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

    
respondido por el Jon Skeet 09.05.2012 - 08:06
20

Esta es definitivamente una pregunta / respuesta de opinión, pero por lo que vale la pena, pensé que iba a lanzar mis dos centavos. En términos del método 2 de estilo de TDD es definitivamente el enfoque que sigue a la letra. El argumento para el método 2 es que si alguna vez quiso reemplazar la implementación de una de esas clases, por ejemplo, una biblioteca equivalente ImageIO , podría hacerlo manteniendo la confianza en las clases que aprovechan ese código.

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

Habiendo dicho eso, PowerMock existe por una razón. Es un problema bastante conocido que probar los métodos estáticos es muy doloroso, de ahí el inicio de PowerMock. Creo que necesitas sopesar tus opciones en términos de cuánto trabajo será envolver todas tus clases de ayuda en lugar de usar PowerMock. No creo que sea 'renunciar' a usar PowerMock; siento que envolver las clases te permite tener más flexibilidad en un proyecto grande. Cuantos más contratos públicos (interfaces) pueda proporcionar, más limpia será la separación entre la intención y la implementación.

    
respondido por el BeRecursive 27.04.2012 - 10:57
4

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

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

Esto significa que no tenemos que escribir una envoltura 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 pensamos que podríamos Necesito que sea hora de escribir una clase de contenedor y luego simular una instancia del contenedor).

    
respondido por el Benedikt 08.05.2012 - 11:02
2

Probaría estas clases utilizando Groovy . Groovy es fácil de agregar a cualquier proyecto Java. Con él, puedes burlarte de los métodos estáticos con bastante facilidad. Consulte Simular métodos estáticos usando Groovy para ver un ejemplo.

    
respondido por el trevorism 03.05.2012 - 21:58
1

Trabajo para una importante compañía de seguros y nuestro código fuente alcanza hasta 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 las pruebas de junit para cada componente individual.

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

Pero fue muy rápido y ha estado funcionando bastante bien hasta ahora. Lo recomiendo - enlace

    
respondido por el BizzyDizzy 08.05.2012 - 13:09
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 del servicio web, la capa dao que accede a la base de datos, los controladores que manejan los parámetros de solicitud http) o si desea probar su objeto aislamiento, entonces te burlas de esos objetos.

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

    
respondido por el Diego Lins de Freitas 08.05.2012 - 13:33
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. Código de un inicializador estático que se conecta en una implementación concreta. Conecte una implementación simulada para la prueba.

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 utilizar la clase en todo el código.

Math.sum(17, 29, 42);
// vs
new Math().sum(17, 29, 42);
    
respondido por el bigh_29 22.05.2015 - 18:46

Lea otras preguntas en las etiquetas