Así es como funciona mi código. Tengo un objeto que representa el estado actual de algo similar a un pedido de carrito de compras, almacenado en una API de compras de terceros. En mi código de controlador, quiero poder llamar:
myOrder.updateQuantity(2);
Para enviar realmente el mensaje al tercero, el tercero también necesita saber varias cosas que son específicas de ESTE pedido, como el orderID
y el loginID
, que no cambiarán durante la vigencia de la aplicación.
Entonces, cuando creo myOrder
originalmente, inyecto un MessageFactory
, que sabe loginID
. Luego, cuando updateQuantity
se llama, Order
pasa orderID
. El código de control es fácil de escribir. Otro hilo maneja la devolución de llamada y se actualiza Order
si su cambio fue exitoso, o informa Order
que su cambio falló si no fue así.
El problema es la prueba. Debido a que el Order
objeto depende de a MessageFactory
, y necesita MessageFactory
devolver Message
s reales (que llama .setOrderID()
, por ejemplo), ahora tengo que configurar simulaciones muy complicadas MessageFactory
. Además, no quiero matar a ninguna hada, ya que "cada vez que un Mock devuelve un Mock, un hada muere".
¿Cómo puedo resolver este problema manteniendo el código del controlador igual de simple? Leí esta pregunta: /programming/791940/law-of-demeter-on-factory-pattern-and-dependency-injection pero no ayudó porque no habló sobre el problema de la prueba .
Algunas soluciones que he pensado:
- Refactorice de alguna manera el código para que no requiera que el método de fábrica devuelva objetos reales. ¿Quizás es menos una fábrica y más una
MessageSender
? - Cree una implementación de solo prueba
MessageFactory
e inyecte eso.
El código está bastante involucrado, aquí está mi intento de un sscce:
public class Order implements UpdateHandler {
private final MessageFactory factory;
private final MessageLayer layer;
private OrderData data;
// Package private constructor, this should only be called by the OrderBuilder object.
Order(OrderBuilder builder, OrderData initial) {
this.factory = builder.getFactory();
this.layer = builder.getLayer();
this.data = original;
}
// Lots of methods like this
public String getItemID() {
return data.getItemID();
}
// Returns true if the message was placed in the outgoing network queue successfully. Doesn't block for receipt, though.
public boolean updateQuantity(int newQuantity) {
Message newMessage = factory.createOrderModification(messageInfo);
// *** THIS IS THE KEY LINE ***
// throws an NPE if factory is a mock.
newMessage.setQuantity(newQuantity);
return layer.send(newMessage);
}
// from interface UpdateHandler
// gets called asynchronously
@Override
public handleUpdate(OrderUpdate update) {
messageInfo.handleUpdate(update);
}
}
fuente
Order
objeto y elMessageFactory
. Esta es una buena descripción, pero es un poco abstracto abordarla directamente con una respuesta clara.layer.send
envía el mensaje o que envía el mensaje correcto ?verify(messageMock).setQuantity(2)
, yverify(layer).send(messageMock);
tambiénupdateQuantity
debería devolver falso siOrder
ya tiene una actualización pendiente, pero omití ese código por razones de seguridad.Respuestas:
La principal preocupación aquí es que los simulacros no pueden (o no deberían) devolver simulacros. Este es probablemente un buen consejo, pero habla de una solución: devolver un real
Message
. Si laMessage
clase está bien probada y es aprobada, puede considerarla tan amigable como una burla. Quizás sea aún más amigable ya que responderá como si fuera real porque es real.¿Qué tipo de
Message
s reales puedes devolver? Bueno, puede devolver un real completoMessage
, un real simplificadoMessage
(en el que se utilizan valores predeterminados conocidos), o puede devolver unNullMessage
(como en el Patrón de objetos nulos). ANullMessage
es tan válidoMessage
como cualquier otro, y se puede colocar en cualquier otro lugar de su aplicación. Cuál usar depende de la complejidad de crear y devolver un mensaje completo.En cuanto a la Ley de Deméter, hay múltiples preocupaciones aquí. Primero, su constructor toma su propio constructor como parámetro, luego extrae elementos de él. Esta es una clara violación de Demeter, y también crea una dependencia superflua. Peor aún, el constructor está actuando como un mini localizador de servicios, enmascarando las dependencias reales de la clase. El
OrderBuilder
debe crear estos objetos y pasarlos en como sus propios parámetros.Para probar esto, entonces, pasaría un simulacro
MessageFactory
, que devuelve un realMessage
(ya sea completo, simple o nulo), y un simulacroMessageLayer
que toma el mensaje. Si usa uno completo o simplificadoMessage
, puede recuperarlo de suMessageLayer
simulacro e inspeccionarlo para comprobar las afirmaciones.También consideraría el
MessageFactory
yMessageLayer
como un grupo funcional en un nivel diferente de abstracción, y entonces extraería unaMessageSender
clase que encapsulara esa funcionalidad. Puede probar esta clase utilizando un simulacro simpleMessageSender
y cambiar todo lo que mencioné anteriormente a lasMessageSender
pruebas de 's', y así cumplir más estrechamente con la Responsabilidad Única también.Veo que realmente hay dos preguntas aquí. Hay una pregunta específica sobre cómo probar este código, y una pregunta general sobre los simulacros que devuelven simulacros. La pregunta específica es lo que traté anteriormente en mayor medida, y tengo más pensamientos al final de esto ahora que algunos detalles más han salido a la luz, pero aún no hay una buena respuesta a la pregunta general: ¿Por qué los simulacros no devuelven simulacros?
La razón por la cual los simulacros no deberían devolver simulacros es que puede terminar probando sus pruebas en lugar de probar su código. En lugar de asegurarse de que la unidad sea completamente funcional, la prueba ahora depende de un código completamente nuevo que se encuentra solo en el caso de prueba en sí (que a menudo no se prueba). Esto crea dos problemas.
Primero, la prueba ahora no puede decirme con certeza si la unidad está rota o si los simulacros interrelacionados están rotos. El objetivo de una prueba es crear un entorno aislado donde solo debe haber una causa de falla. Un simulacro en sí mismo generalmente es muy simple y puede inspeccionarse directamente para detectar problemas, pero el cableado de simulacros múltiples de esta forma se vuelve exponencialmente más difícil de confirmar mediante inspección.
El segundo problema es que, a medida que las API cambian para los objetos reales, las pruebas pueden comenzar a fallar muy lejos, ya que los simulacros no cambian automáticamente también. Aquí entra en juego la Ley de Deméter, ya que estos son exactamente el tipo de efectos que la ley evita. En mis pruebas, tendría que preocuparme por mantener sincronizadas no solo las simulaciones de dependencias directas, sino también las simulaciones de dependencias de dependencias hasta el infinito . Esto tiene el efecto de la cirugía de escopeta en las pruebas cuando las clases cambian.
Ahora, en cuanto a la pregunta específica de cómo probar este fragmento de código en particular, analicemos algunos supuestos.
Pregunta 1: ¿Qué estamos probando realmente? Si bien esta es una parte abreviada del código, podemos ver tres actividades esenciales aquí. Primero, tenemos una fábrica que genera a
Message
. No estamos probando si la fábrica está produciendoMessage
, ya que ya te estás burlando de eso. No estamos probando elMessage
, como debería probarse en otro lugar, presumiblemente en un conjunto de pruebas para la API de terceros que genera elMessage
. En la segunda línea, podemos ver por inspección que el método simplemente se llama en elMessage
y, por lo tanto, realmente no hay nada que probar en la segunda línea. Una vez más, debería haber pruebas en otros lugares que hagan que las pruebas sean redundantes. La tercera línea llama a laMessageLayer
API y simplemente pasa por el resultado. Una vez más,MessageLayer
La API ya debería probarse en otro lugar. Esto nos deja esencialmente sin nada que probar. No hay efectos secundarios visibles directos en el código externo, y no deberíamos probar la implementación interna. Eso nos lleva a la conclusión de que sería inapropiado probar este código . (Para más información sobre esta línea de razonamiento, vea la presentación de Sandi Metz Trucos mágicos de prueba , [ diapositivas , video ])Pregunta 2: Espera, entonces ... ¿qué? Sí, es cierto, no pruebes esto en absoluto. Ahora, como se mencionó, esta es una versión abreviada del código. Si tiene otra lógica, pruebe eso, pero encapsule esto en una unidad separada (como la
MessageSender
implementación mencionada anteriormente). A continuación, puede burlarse de este aspecto completo del código fácilmente, sin dejar de tener la capacidad de probar otra lógica.Básicamente está utilizando una API de terceros directamente en su código. El código de terceros es notoriamente difícil de probar porque puede tener este tipo de problemas de dependencia que tiene aquí. Encapsularlo en un área acorralada puede hacer que sea más fácil probar su otro código y reducir la cirugía de escopeta si ese tercero cambia su código (o simplemente cambia). Si bien aún puede ser difícil probar la parte que interactúa con la API de terceros, está limitada a una pequeña faceta que puede aislar.
fuente
Message
debe usar otro tipo de real .Message
implementación real depende de la API de terceros; para poder crear instancias de sus clases, debe iniciar laEngine
que requiere, entre otras cosas, una clave de licencia ... no exactamente lo que desea en una prueba unitaria.Voy a estar de acuerdo con @Robert Harvey. Para ser claros: Demeter es un estilo de programación razonable y bueno. Es el "no burlarse de los simulacros" lo que me parece más una preferencia subjetiva que respalda un estilo de codificación particular en lugar de una práctica generalmente aplicable (y bien justificada). La regla de hadas saca interfaces "fluidas" como:
Es un ejemplo extremo, pero esencialmente la regla de hadas no permitiría incluir esa clase en ningún código, porque el código se volvería inestable. Pero ese es un paradigma popular en el código OO.
Además, el problema más general es cómo burlarse de una fábrica para devolver un simulacro con el que desea probar. Por lo general, me da miedo usar las fábricas como dependencias, pero a veces es mucho mejor que la alternativa. Si terminas con
No veo cómo puedes evitarlo. Necesitas un simulacro para devolver el simulacro. Entonces, esa regla elimina dos patrones de diseño OO realmente poderosos.
No puedo pensar en una forma de solucionar su problema sin dividir ese método en 2 o 3, empujando largas filas hacia el cliente, o creando una extraña clase de contenedor con estado.
Me interesaría mucho ver cómo se ve la alternativa.
Mi respuesta: ¡tu código está bien! Excelcior!
(en realidad tengo curiosidad por las alternativas)
...
Tomemos un caso límite: ¿se les permite a los simulacros regresar? Técnicamente, están devolviendo un simulacro. Si no, entonces eso noquea al prototipo GoF, y ese es uno de los patrones que se ha mantenido con el tiempo:
así lo permite la regla:
Además, la regla de hadas prácticamente prohíbe el uso de mónadas, ya que se basan en la operación de encadenamiento para un tipo de contenedor en particular. Puede probar el tipo Monad, pero no puede probar el código en el que aparece Monad.
fuente
obj.law().of().demeter();
es lo mismo queobj.law(); obj.of(); obj.demeter();
, lo cual es perfectamente aceptable. Se puede hacer una explicación similar sobre los prototipos.