2010-09-27 16 views
15

Estoy aprendiendo el desarrollo basado en pruebas, y he notado que fuerza a los objetos débilmente acoplados, lo cual es básicamente una buena cosa. Sin embargo, esto también a veces me obliga a proporcionar accesos para propiedades que normalmente no necesitaría, y creo que la mayoría de las personas que están en SO están de acuerdo en que los accesorios generalmente son un signo de mal diseño. ¿Es esto inevitable al hacer TDD?¿Cómo evitar los usuarios en desarrollo basado en pruebas?

Aquí se muestra un ejemplo, el código de dibujo simplificado de una entidad sin TDD:

class Entity { 
    private int x; 
    private int y; 
    private int width; 
    private int height; 

    void draw(Graphics g) { 
     g.drawRect(x, y, width, height); 
    } 
} 

La entidad sabe cómo dibujar en sí, eso es bueno. Todo en un lugar. Sin embargo, estoy haciendo TDD, así que quiero verificar si mi entidad fue movida correctamente por el método "fall()" que estoy a punto de implementar. Esto es lo que el caso de prueba podría ser:

@Test 
public void entityFalls() { 
    Entity e = new Entity(); 
    int previousY = e.getY(); 
    e.fall(); 
    assertTrue(previousY < e.getY()); 
} 

tengo que mirar a (bueno, al menos lógicamente,) el estado interno del objeto y ver si la posición se actualiza correctamente. Puesto que es en realidad en el camino (no quiero que mis casos de prueba que dependen de mi biblioteca de gráficos), moví el código de dibujo a un "Procesador" clase:

class Renderer { 
    void drawEntity(Graphics g, Entity e) { 
     g.drawRect(e.getX(), e.getY(), e.getWidth(), e.getHeight()); 
    } 
} 

ligeramente agrupado, buena. Incluso puedo reemplazar el renderizador con uno que muestra la entidad de una manera completamente diferente. Sin embargo, tuve que exponer el estado interno de la entidad, es decir, los descriptores de acceso de todas sus propiedades, de modo que el renderizador pudiera leerlo.

Creo que esto fue específicamente forzado por TDD. ¿Qué puedo hacer sobre esto? ¿Es mi diseño aceptable? ¿Necesita Java la palabra clave "friend" de C++?

Actualización:

Gracias por sus valiosas aportaciones hasta el momento! Sin embargo, me temo que he elegido un mal ejemplo para ilustrar mi problema. Esto fue hecho completamente, voy a demostrar que está más cerca de mi código real:

@Test 
public void entityFalls() { 
    game.tick(); 
    Entity initialEntity = mockRenderer.currentEntity; 
    int numTicks = mockRenderer.gameArea.height 
        - mockRenderer.currentEntity.getHeight(); 
    for (int i = 0; i < numTicks; i++) 
     game.tick(); 
    assertSame(initialEntity, mockRenderer.currentEntity); 
    game.tick(); 
    assertNotSame(initialEntity, mockRenderer.currentEntity); 
    assertEquals(initialEntity.getY() + initialEntity.getHeight(), 
       mockRenderer.gameArea.height); 
} 

Ésta es una implementación basada en bucle del juego de un juego en el que una entidad puede caer, entre otras cosas. Si golpea el suelo, se crea una nueva entidad.

El "mockRenderer" es una implementación simulada de una interfaz "Renderer". Este diseño fue parcialmente forzado por TDD, pero también debido al hecho de que voy a escribir la interfaz de usuario en GWT, y no hay un dibujo explícito en el navegador (todavía), así que no creo que sea posible. la clase de Entidad para asumir esa responsabilidad. Además, me gustaría mantener la posibilidad de llevar el juego a Java/Swing nativo en el futuro.

Actualización 2:

Pensando en esto un poco más, tal vez está bien como es. Tal vez está bien que la entidad y el dibujo estén separados y que la entidad cuente otros objetos lo suficiente como para ser dibujados. Quiero decir, ¿de qué otra manera podría lograr esta separación? Y realmente no veo cómo vivir sin eso. Incluso grandes programadores orientados a objetos usan objetos con getters/setters a veces, especialmente para algo así como un objeto de entidad. Quizás getter/setter no son todos malvados. ¿Qué piensas?

+1

Mírelo de esta manera: si 'Entity' no tiene' getY', ¿debería probarlo? Teóricamente, cualquier cosa que no sea accesible con otro código es interna a 'Entity' y no de valor para el usuario, y por extensión, la prueba. –

+0

Pero podría agregar que los juegos (que parecen ser) son especialmente difíciles de probar en una unidad. –

+0

La "palabra clave friend" de java es el modificador de acceso predeterminado, cada clase en el mismo paquete puede acceder a campos y métodos que no tienen un modificador de acceso (no público, privado o protegido). Las pruebas pueden acceder a estos campos siempre que utilicen la misma estructura de paquete, por lo que puede mantener estos detalles privados para el paquete. – josefx

Respuesta

3

Dices que sientes que la clase de Renderer que surgió fue "específicamente forzada" por TDD. Entonces, veamos a dónde te llevó TDD. De una clase Rectangle que fue responsable de sus coordenadas y de dibujarse a sí misma, a una clase Rectangle que tiene la Responsabilidad Individual de mantener sus coordenadas y un Renderer que tiene la Responsabilidad Individual de, bueno, renderizar un Rectángulo. A eso nos referimos cuando decimos Test Driven: esta práctica afecta su diseño. En este caso, lo llevó a un diseño que se adhirió más estrechamente al Single Responsibility Principle, un diseño al que no habría acudido sin las pruebas. Creo que eso es algo bueno. Creo que estás practicando TDD bien, y creo que está funcionando para ti.

5

Los programadores pragmáticos discuten tell, don't ask. No quiere saber sobre la entidad, quiere que se dibuje. Dile que se dibuje en los gráficos dados.

Puede refactorizar el código anterior para que la entidad haga el dibujo, lo que es útil si la entidad no es un rectángulo sino un círculo.

void Entity::draw(Graphics g) { 
    g.drawRect(x,y, width, height); 
} 

Comprobará que g tiene los métodos correctos solicitados en sus pruebas.

+0

Como se explicó anteriormente, solía tener el método de dibujo de la entidad, pero lo moví deliberadamente para evitar un acoplamiento estrecho con el sistema de gráficos. – Noarth

+0

Creo que es un problema con la abstracción de Gráficos, no con la abstracción de Entidad. Al igual que en el MVC, quieres una capa de abstracción entre el cómo (concreto) y el qué (abstracto) de tus gráficos. –

0

Lo que desea probar es cómo su objeto responderá a ciertas llamadas, no cómo funciona internamente.

Así que no es realmente necesario (y sería una mala idea) acceder a campos/métodos no accesibles.

Si desea ver la interacción entre una llamada a método y uno de los argumentos, debe burlarse de dicho argumento de una manera para poder probar si el método funcionó como quería.

2

Así que, si no hubiera movido el método draw(Graphics) de Entity tenía un código perfectamente comprobable. Solo necesita inyectar una implementación de Graphics que informó el estado interno de Entity en su arnés de prueba. Solo mi opinión sin embargo.

1

En primer lugar, ¿conoce la clase java.awt.Rectangle que es cómo se trata este problema en la Biblioteca de tiempo de ejecución de Java?

En segundo lugar, creo que los valores reales de TDD son primero que aleja su enfoque del "cómo hago este detalle específico con los datos que supongo que está presente así" a "cómo llamo al código y qué resultado espero volver ". El enfoque tradicional es "arreglar los detalles, y luego descubriremos cómo llamar al código", y hacerlo al revés le permite encontrar más rápido si algo simplemente no puede hacerse o no.

Esto es muy importante en el diseño de las API, que es muy probable también por lo que ha encontrado que su resultado está ligeramente acoplado.

El segundo valor es que sus pruebas son "comentarios vivientes" en lugar de comentarios antiguos, intactos. La prueba muestra cómo debe llamarse su código y puede verificar instantáneamente que se comporta como se especifica. Esto no es relevante para lo que pregunta, pero debe demostrar que las pruebas tienen más propósitos que llamar ciegamente algún código que haya escrito hace un tiempo.

0

creo que el ejemplo tiene mucho en común con el ejemplo usado aquí:

http://www.m3p.co.uk/blog/2009/03/08/mock-roles-not-objects-live-and-in-person/

Usando su ejemplo original y la sustitución de la Entidad con el héroe, caída() con jumpFrom (Balcón), y dibujar() como moveTo (Room) se vuelve notablemente similar. Si usa el enfoque de objeto falso, sugiere Steve Freeman, su primera implementación no fue tan mala después de todo. Creo que @Colin Hebert ha dado la mejor respuesta cuando señaló en esta dirección. No hay necesidad de exponer nada aquí.Usas los objetos simulados para verificar si el comportamiento del héroe ha tenido lugar.

Tenga en cuenta que el autor del artículo ha co-escrito un gran libro que puede ayudarle a:

http://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/dp/0321503627

Hay algunos buenos papeles libremente disponible como PDF por los autores sobre el uso de objetos simulados para guiar tu diseño en TDD.

+0

No creo que pueda usar un objeto simulado para dibujar, porque implementaré un procesador para GWT. Realmente no puedo crear un objeto tipo Gráficos para GWT, eso no funcionaría porque no hay dibujo en GWT, solo hay modificación de elementos DOM, y no quiero elementos DOM en mi paquete de lógica de juego principal. – Noarth

+0

Además, no creo que el método de dibujo sea el mismo que el método moveTo. Mi método de dibujo es un dibujo de visualización de bajo nivel, mientras que moveTo es una operación lógica, una que probaría. – Noarth

Cuestiones relacionadas