¿Está bien introducir métodos que se usan solo durante las pruebas unitarias?

12

Recientemente estuve utilizando un método de fábrica en TDD. El método era crear un objeto simple o un objeto envuelto en un decorador. El objeto decorado podría ser de uno de varios tipos, todos extendiendo StrategyClass.

En mi prueba, quería comprobar si la clase de objeto devuelto es la esperada. Eso es fácil cuando se devuelve el objeto simple, pero ¿qué hacer cuando está envuelto dentro de un decorador?

Codifico en PHP para poder usar ext/Reflection para encontrar una clase de objeto envuelto, pero me pareció que estaba complicando las cosas, y de alguna manera contra las reglas de TDD.

En lugar de eso, decidí introducir getClassName() que devolvería el nombre de clase del objeto cuando se llamara desde StrategyClass. Sin embargo, cuando recibe una llamada del decorador, devolverá el valor devuelto por el mismo método en el objeto decorado.

Algún código para que sea más claro:

interface StrategyInterface {
  public function getClassName();
}

abstract class StrategyClass implements StrategyInterface {
  public function getClassName() {
    return \get_class($this);
  }
}

abstract class StrategyDecorator implements StrategyInterface {

  private $decorated;

  public function __construct(StrategyClass $decorated) {
    $this->decorated = $decorated;
  }

  public function getClassName() {
    return $this->decorated->getClassName();
  }

}

Y una prueba de PHPUnit

 /**
   * @dataProvider providerForTestGetStrategy
   * @param array $arguments
   * @param string $expected
   */
  public function testGetStrategy($arguments, $expected) {

    $this->assertEquals(
      __NAMESPACE__.'\'.$expected,  
      $this->object->getStrategy($arguments)->getClassName()
    )
  }

//below there's another test to check if proper decorator is being used

Mi punto aquí es: ¿está bien introducir tales métodos, que no tienen otro uso que hacer que las pruebas unitarias sean más fáciles? De alguna manera no me parece correcto.

    
pregunta Mchl 24.06.2011 - 21:25

4 respuestas

13

Mi pensamiento es no, no deberías hacer nada solo porque es necesario para la verificación. Muchas de las decisiones que toman las personas tienen beneficios en cuanto a la capacidad de prueba y la capacidad de prueba, incluso puede ser el principal beneficio, pero debe ser una buena decisión de diseño por otros méritos. Esto significa que algunas propiedades deseadas no son comprobables. Otro ejemplo es cuando necesita saber cuán eficiente es alguna rutina, por ejemplo, si su Hashmap utiliza un rango de valores hash distribuidos uniformemente, nada en la interfaz externa podrá decirle eso.

En lugar de pensar, "¿estoy obteniendo la clase de estrategia correcta" piensa "la clase que obtengo realiza lo que esta especificación está tratando de probar?" Es agradable cuando puedes probar la tubería interna pero no tienes que hacerlo, solo prueba girando la perilla y ve si tienes agua fría o caliente.

    
respondido por el Jeremy 24.06.2011 - 21:51
5

Mi opinión sobre esto es que, a veces, tienes que modificar un poco el código fuente para que sea más comprobable. No es ideal y no debería ser una excusa para saturar la interfaz con funciones que se supone que solo deben usarse para realizar pruebas, por lo que la moderación es generalmente la clave aquí. Tampoco desea estar en una situación en la que los usuarios de su código usen repentinamente las funciones de la interfaz de prueba para interacciones normales con su objeto.

Mi forma preferida de manejar esto (y tengo que disculparme por no poder mostrar cómo hacer esto en PHP porque codifico principalmente en lenguajes de estilo C) es proporcionar las funciones de "prueba" de manera que no están expuestos al mundo exterior por el objeto en sí, sino que se puede acceder por objetos derivados. Para fines de prueba, obtendría una clase que manejaría la interacción con el objeto que realmente quiero probar y hacer que la prueba de unidad use ese objeto en particular. Un ejemplo de C ++ se vería así:

Clase de tipo de producción:

class ProdObj
{
  ...
  protected:
     virtual bool checkCertainState();
};

Clase de ayudante de prueba:

class TestHelperProdObj : public ProdObj
{
  ...
  public:
     virtual bool checkCertainState() { return ProdObj::checkCertainState(); }
};

De esa manera, al menos está en una posición en la que no tiene que exponer las funciones de tipo "prueba" en su objeto principal.

    
respondido por el Timo Geusch 24.06.2011 - 21:57
3

Hace unos meses, cuando coloqué el lavavajillas recién comprado, salió mucha agua de su manguera, me di cuenta de que probablemente se deba al hecho de que se probó correctamente en la fábrica de la que procedía. No es raro ver agujeros de montaje y cosas en la maquinaria que están allí, solo con el propósito de realizar pruebas en una línea de ensamblaje.

Las pruebas son importantes, si es necesario, solo añada algo para ello.

Pero prueba algunas de las alternativas. Tu opción basada en la reflexión no es tan mala. Podría tener un acceso virtual protegido a lo que necesita y crear una clase derivada para probar y hacer valer. Quizás pueda dividir su clase y probar una clase de hoja resultante directamente. Ocultar el método de prueba con una variable de compilación en su código fuente también es una opción (apenas sé PHP, no estoy seguro de si eso es posible en PHP).

En su contexto, puede decidir no probar la composición adecuada dentro del decorador, pero probar el comportamiento esperado que debe tener la decoración. Quizás esto se centre más en el comportamiento esperado del sistema y no tanto en la especificación técnica (¿qué le ofrece el patrón de decoración desde una perspectiva funcional?).

    
respondido por el Joppe 25.06.2011 - 02:14
1

Soy un novato absoluto en TDD, pero parece depender del método que se agregue. Por lo que entiendo de TDD, se supone que sus pruebas deben "impulsar" la creación de su API hasta cierto punto.

Cuando está bien:

  • Siempre que el método no rompa la encapsulación y tenga un propósito que esté en línea con la responsabilidad del objeto.

Cuando no está bien:

  • Si el método parece algo que nunca sería útil, o no tiene sentido en relación con otros métodos de interfaz, puede ser incohesivo o confuso. En ese momento, enturbiaría mi comprensión de ese objeto.
  • El ejemplo de Jeremy, "... cuando necesita saber qué tan eficiente es alguna rutina, por ejemplo, su Hashmap usa un rango de valores hash distribuidos uniformemente; nada en la interfaz externa podrá decirle eso". / li>
respondido por el pup 14.02.2012 - 18:35

Lea otras preguntas en las etiquetas