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

12

Recientemente estaba TDDing un método de fábrica. El método consistía en 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 del objeto devuelto es la esperada. Eso es fácil cuando el objeto simple regresó, pero ¿qué hacer cuando está envuelto dentro de un decorador?

Codifico en PHP para poder usarlo ext/Reflectionpara encontrar una clase de objeto envuelto, pero me pareció que era demasiado complicado, y de alguna manera contra las reglas de TDD.

En su lugar, decidí introducir getClassName()que devolvería el nombre de la clase del objeto cuando se llama desde StrategyClass. Sin embargo, cuando se llama desde el decorador, devolvería el valor devuelto por el mismo método en el objeto decorado.

Algún código para hacerlo 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 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 bien.

Mchl
fuente
Posiblemente esta pregunta arroje más información sobre su pregunta, programmers.stackexchange.com/questions/231237/… , creo que depende del uso y de si los métodos ayudarán en gran medida a las pruebas unitarias y a la depuración de cualquier aplicación que esté desarrollando. .
Clemente Mark-Aaba

Respuestas:

13

Mi pensamiento es no, no deberías hacer nada solo porque es necesario para la comprobabilidad. Muchas de las decisiones que las personas toman tienen un beneficio para la capacidad de prueba y la capacidad de prueba puede incluso ser el beneficio principal, pero debería ser una buena decisión de diseño sobre otros méritos. Esto significa que algunas propiedades deseadas no son comprobables. Otro ejemplo es cuando necesita saber qué tan eficiente es alguna rutina, por ejemplo, su Hashmap utiliza un rango distribuido uniformemente de valores hash; nada en la interfaz externa podría 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 bueno cuando puedes probar la tubería interna pero no tienes que hacerlo, solo prueba girar la perilla y ver si tienes agua fría o caliente.

Jeremy
fuente
+1 OP está describiendo la prueba de unidad 'caja clara', no la prueba de características TDD
Steven A. Lowe
1
Veo la novena ere, aunque soy un poco reacio a agregar pruebas de algoritmos de StrategyClass cuando quiero probar si el método de fábrica está haciendo su trabajo. Este tipo de rompe el aislamiento en mi humilde opinión. Otra razón por la que quiero evitar eso es que estas clases particulares operan en la base de datos, por lo que probarlas requiere burlas / stubbing adicionales.
Mchl
Por otro lado y a la luz de esta pregunta: programmers.stackexchange.com/questions/86656/… cuando distinguimos las "pruebas TDD" de las "pruebas unitarias", esto se vuelve perfectamente
correcto
Si agrega los métodos, se convierten en parte del contrato con sus usuarios. Terminará con codificadores que llaman a sus funciones de prueba solamente y se ramifican en función de los resultados. En general, prefiero exponer lo menos posible de la clase.
BillThor
5

Mi opinión sobre esto es: a veces hay que volver a trabajar un poco el código fuente para que sea más comprobable. No es lo ideal y no debería ser una excusa para abarrotar la interfaz con funciones que se supone que solo deben usarse para las pruebas, por lo que la moderación es generalmente la clave aquí. Tampoco desea estar en la situación en la que los usuarios de su código de repente usan 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 hacerlo en PHP, ya que codifico principalmente en lenguajes de estilo C) es proporcionar las funciones de 'prueba' de una manera que no sean expuesto al mundo exterior por el objeto mismo, pero se puede acceder por objetos derivados. Para fines de prueba, derivaría una clase que manejaría la interacción con el objeto que realmente quiero probar y que la prueba unitaria 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();
};

Prueba de clase auxiliar:

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.

Timo Geusch
fuente
Un enfoque interesante. Necesito ver cómo puedo adaptar esto
Mchl
3

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

La prueba es importante, si es necesario, simplemente agregue algo para ello.

Pero intente algunas de las alternativas. Su 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 afirmar. Quizás pueda dividir su clase y probar una clase de hoja resultante directamente. Ocultar el método de prueba con una variable del compilador en su código fuente también es una opción (apenas conozco 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, sino probar el comportamiento esperado que debe tener la decoración. Quizás esto se centre un poco más en el comportamiento esperado del sistema y no tanto en las especificaciones técnicas (¿qué le compra el patrón decorador desde una perspectiva funcional?).

Joppe
fuente
1

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

Cuando esta bien:

  • Siempre que el método no rompa la encapsulación y cumpla 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 incoherente o confuso. En ese punto, enturbiaría mi comprensión de ese objeto.
  • El ejemplo de Jeremy, "... cuando necesita saber qué tan eficiente es una rutina, por ejemplo, su Hashmap utiliza un rango distribuido uniformemente de valores hash; nada en la interfaz externa podría decirle eso".
cachorro
fuente