Está en la página 1de 16

Los simulacros no son talones

El término 'Objetos simulados' se ha vuelto popular para describir objetos de casos especiales que imitan
objetos reales para la prueba. La mayoría de los entornos de lenguaje ahora tienen marcos que facilitan la
creación de objetos simulados. Sin embargo, lo que a menudo no se comprende es que los objetos
simulados no son más que una forma de objeto de prueba de casos especiales, uno que permite un estilo
diferente de prueba. En este artículo, explicaré cómo funcionan los objetos simulados, cómo fomentan las
pruebas basadas en la verificación del comportamiento y cómo la comunidad que los rodea los usa para
desarrollar un estilo diferente de prueba.
Me encontré por primera vez con el término "objeto simulado" hace unos años en la comunidad
de Programación Extrema (XP). Desde entonces, me he encontrado con objetos simulados cada vez más. En
parte, esto se debe a que muchos de los principales desarrolladores de objetos simulados han sido colegas
míos en Thoughtworks en varias ocasiones. En parte es porque los veo cada vez más en la literatura de
prueba influenciada por XP.
Pero muy a menudo veo objetos simulados mal descritos. En particular, los veo a menudo confundidos con
stubs, un ayudante común para los entornos de prueba. Entiendo esta confusión: también los vi como
similares durante un tiempo, pero las conversaciones con los desarrolladores simulados han permitido que
un poco de comprensión simulada penetre en mi cráneo de carey.
Esta diferencia es en realidad dos diferencias separadas. Por un lado, hay una diferencia en cómo se
verifican los resultados de las pruebas: una distinción entre verificación de estado y verificación de
comportamiento. Por otro lado, hay una filosofía completamente diferente a la forma en que las pruebas y el
diseño juegan juntos, lo que aquí llamo los estilos clásico y burlón del desarrollo dirigido por pruebas .
Pruebas Regulares
Comenzaré ilustrando los dos estilos con un ejemplo simple. (El ejemplo está en Java, pero los principios
tienen sentido con cualquier lenguaje orientado a objetos). Queremos tomar un objeto de pedido y llenarlo
desde un objeto de almacén. El pedido es muy sencillo, con un solo producto y una cantidad. El almacén
mantiene inventarios de diferentes productos. Cuando pedimos que un pedido se surta solo desde un
almacén hay dos respuestas posibles. Si hay suficiente producto en el almacén para completar el pedido, el
pedido se completa y la cantidad del producto del almacén se reduce en la cantidad adecuada. Si no hay
suficiente producto en el almacén, el pedido no se completa y no sucede nada en el almacén.
Estos dos comportamientos implican un par de pruebas, que parecen pruebas JUnit bastante convencionales.
Clase pública OrderStateTester extiende TestCase {
cadena estática privada TALISKER = "Talisker";
cadena estática privada HIGHLAND_PARK = "Highland Park";
Almacén privado warehouse = new WarehouseImpl();

La configuración vacía protegida () arroja una excepción {


almacén.add(TALISKER, 50);
1
almacén.add(HIGHLAND_PARK, 25);
}
testOrderIsFilledIfEnoughInWarehouse() {
Pedido pedido = nuevo Pedido(TALISKER, 50);
order.fill(almacén);
afirmarVerdadero(pedido.estáLlenado());
afirmarEquals(0, almacén.obtenerInventario(TALISKER));
}
testOrderDoesNotRemoveIfNotEnough public void () {
Pedido pedido = nuevo Pedido(TALISKER, 51);
order.fill(almacén);
afirmarFalse(pedido.estálleno());
afirmarEquals(50, almacén.obtenerInventario(TALISKER));
}
Las pruebas de xUnit siguen una secuencia típica de cuatro fases: configuración, ejercicio, verificación y
desmontaje. En este caso la fase de configuración se realiza en parte en el método setUp (configuración del
almacén) y en parte en el método de prueba (configuración del pedido). La llamada a order.filles la fase
de ejercicio. Aquí es donde se empuja al objeto a hacer lo que queremos probar. Las declaraciones de
afirmación son entonces la etapa de verificación, verificando si el método ejercido realizó su tarea
correctamente. En este caso, no hay una fase de desmontaje explícita, el recolector de elementos no
utilizados lo hace por nosotros implícitamente.
Durante la configuración, hay dos tipos de objetos que estamos ensamblando. Order es la clase que estamos
probando, pero para Order.fillque funcione también necesitamos una instancia de Warehouse. En esta
situación, el Orden es el objeto que estamos enfocados en probar. A las personas orientadas a las pruebas les
gusta usar términos como objeto bajo prueba o sistema bajo prueba para nombrar tal cosa. Decir cualquiera
de los dos términos es un bocado feo, pero como es un término ampliamente aceptado, me taparé la nariz y
lo usaré. Siguiendo a Meszaros usaré System Under Test, o más bien la abreviatura SUT.
Entonces para esta prueba necesito el SUT ( Order) y un colaborador ( warehouse). Necesito el almacén por
dos razones: una es hacer que el comportamiento probado funcione (ya que Order.fillllama a los métodos
del almacén) y, en segundo lugar, lo necesito para la verificación (ya que uno de los resultados de Order.fill
es un cambio potencial en el estado de El almacén). A medida que exploremos más este tema, verá que
haremos muchas distinciones entre SUT y colaboradores. (En la versión anterior de este artículo me referí al
SUT como el "objeto principal" y a los colaboradores como "objetos secundarios")

2
Este estilo de prueba utiliza verificación de estado : lo que significa que determinamos si el método
ejercitado funcionó correctamente al examinar el estado del SUT y sus colaboradores después de que se
ejercitó el método. Como veremos, los objetos simulados permiten un enfoque diferente para la verificación.
Pruebas con objetos simulados
Ahora tomaré el mismo comportamiento y usaré objetos simulados. Para este código, estoy usando la
biblioteca jMock para definir simulacros. jMock es una biblioteca de objetos simulados de Java. Existen
otras bibliotecas de objetos simulados, pero esta es una biblioteca actualizada escrita por los creadores de la
técnica, por lo que es una buena opción para comenzar.
Clase pública OrderInteractionTester extiende MockObjectTestCase {
cadena estática privada TALISKER = "Talisker";

public void testFillingRemovesInventoryIfInStock() {


//configuración - datos
Pedido pedido = nuevo Pedido(TALISKER, 50);
Mock warehouseMock = new Mock(Almacén.clase);

//configuración - expectativas
warehouseMock.expects(once()).method("hasInventory")
.with(eq(TALISKER),eq(50))
.will(returnValue(true));
warehouseMock.expects(once()).method("remove")
.con(eq(TALISKER), eq(50))
.después("tieneInventario");

//ejercicio
order.fill((Almacén) warehouseMock.proxy());

//verificar
almacénMock.verify();
afirmarVerdadero(pedido.estáLlenado());
}

test public voidFillingDoesNotRemoveIfNotEnoughInStock() {


Pedido pedido = nuevo Pedido(TALISKER, 51);
Almacén simulado = simulacro(Almacén.clase);

3
warehouse.expects(once()).method("hasInventory")
.conCualquierArgumento()
.will(returnValue(falso));

order.fill((Almacén) warehouse.proxy());

afirmarFalse(pedido.estálleno());
}
Concéntrate en testFillingRemovesInventoryIfInStocklo primero, ya que he tomado un par de atajos
con la última prueba.
Para empezar, la fase de configuración es muy diferente. Para empezar, se divide en dos partes: datos y
expectativas. La parte de datos configura los objetos con los que nos interesa trabajar, en ese sentido es
similar a la configuración tradicional. La diferencia está en los objetos que se crean. El IVU es lo mismo -
una orden. Sin embargo, el colaborador no es un objeto de almacén, sino un almacén simulado, técnicamente
una instancia de la clase Mock.
La segunda parte de la configuración crea expectativas en el objeto simulado. Las expectativas indican qué
métodos deben llamarse en los simulacros cuando se ejerce el SUT.
Una vez que todas las expectativas están en su lugar, ejerzo el SUT. Después del ejercicio hago la
verificación, que tiene dos aspectos. Ejecuto afirmaciones contra el SUT, como antes. Sin embargo, también
verifico los simulacros, verificando que hayan sido llamados de acuerdo con sus expectativas.
La diferencia clave aquí es cómo verificamos que el pedido hizo lo correcto en su interacción con el
almacén. Con la verificación del estado, hacemos esto afirmando el estado del almacén. Los simulacros
usan la verificación de comportamiento , donde en su lugar verificamos si el pedido hizo las llamadas
correctas en el almacén. Hacemos esta verificación diciéndole al simulacro qué esperar durante la
configuración y pidiéndole al simulacro que se verifique a sí mismo durante la verificación. Solo se verifica
el orden mediante afirmaciones, y si el método no cambia el estado del pedido, no hay afirmaciones en
absoluto.
En la segunda prueba hago un par de cosas diferentes. En primer lugar, creo el simulacro de manera
diferente, usando el mockmétodo en MockObjectTestCase en lugar del constructor. Este es un método de
conveniencia en la biblioteca jMock que significa que no necesito llamar explícitamente a verificar más
adelante, cualquier simulacro creado con el método de conveniencia se verifica automáticamente al final de
la prueba. También podría haber hecho esto en la primera prueba, pero quería mostrar la verificación de
manera más explícita para mostrar cómo funciona la prueba con simulacros.

4
La segunda cosa diferente en el segundo caso de prueba es que he relajado las restricciones sobre la
expectativa usando withAnyArguments. La razón de esto es que la primera prueba verifica que el número se
pasa al almacén, por lo que la segunda prueba no necesita repetir ese elemento de la prueba. Si es necesario
cambiar la lógica del pedido más adelante, solo fallará una prueba, lo que facilita el esfuerzo de migrar las
pruebas. Resulta que podría haberme omitido withAnyArgumentspor completo, ya que ese es el valor
predeterminado.
Uso de EasyMock
Hay una serie de bibliotecas de objetos simulados por ahí. Uno con el que me encuentro un poco es
EasyMock, tanto en su versión java como .NET. EasyMock también permite la verificación del
comportamiento, pero tiene un par de diferencias de estilo con jMock que vale la pena discutir. Aquí están
las pruebas familiares de nuevo:
Clase pública OrderEasyTester extiende TestCase {
cadena estática privada TALISKER = "Talisker";

control de almacén MockControl privado;


Almacén privado almacénMock;

configuración de vacío público () {


warehouseControl = MockControl.createControl(Warehouse.class);
warehouseMock = (Almacén) warehouseControl.getMock();
}

public void testFillingRemovesInventoryIfInStock() {


//configuración - datos
Pedido pedido = nuevo Pedido(TALISKER, 50);

//configuración - expectativas
almacénMock.hasInventory(TALISKER, 50);
almacénControl.setReturnValue(verdadero);
almacénMock.remove(TALISKER, 50);
almacénControl.replay();

//ejercicio
order.fill(warehouseMock);

5
//verificar
controldealmacén.verificar();
afirmarVerdadero(pedido.estáLlenado());
}

test public voidFillingDoesNotRemoveIfNotEnoughInStock() {


Pedido pedido = nuevo Pedido(TALISKER, 51);

almacénMock.hasInventory(TALISKER, 51);
warehouseControl.setReturnValue(falso);
almacénControl.replay();

order.fill((Warehouse) warehouseMock);

afirmarFalse(pedido.estálleno());
controldealmacén.verificar();
}
}
EasyMock utiliza una metáfora de grabación/reproducción para establecer expectativas. Para cada objeto
que desee simular, cree un control y un objeto simulado. El simulacro satisface la interfaz del objeto
secundario, el control le brinda funciones adicionales. Para indicar una expectativa, llama al método, con los
argumentos que espera en el simulacro. Sigue esto con una llamada al control si desea un valor de
retorno. Una vez que haya terminado de establecer las expectativas, llame a la reproducción en el control,
momento en el que el simulacro finaliza la grabación y está listo para responder al objeto principal. Una vez
hecho esto, llama a verificar en el control.
Parece que, si bien la metáfora de grabar/reproducir a menudo desconcierta a las personas a primera vista,
rápidamente se acostumbran. Tiene una ventaja sobre las restricciones de jMock en el sentido de que está
realizando llamadas de métodos reales al simulacro en lugar de especificar nombres de métodos en
cadenas. Esto significa que puede usar la finalización de código en su IDE y cualquier refactorización de
nombres de métodos actualizará automáticamente las pruebas. La desventaja es que no puede tener las
restricciones más flexibles.
Los desarrolladores de jMock están trabajando en una nueva versión que usará otras técnicas para permitirle
usar llamadas a métodos reales.

6
La diferencia entre simulacros y talones
Cuando se introdujeron por primera vez, muchas personas confundieron fácilmente los objetos simulados
con la noción común de prueba de usar stubs. Desde entonces, parece que la gente ha entendido mejor las
diferencias (y espero que la versión anterior de este documento haya ayudado). Sin embargo, para
comprender completamente la forma en que las personas usan los simulacros, es importante comprender los
simulacros y otros tipos de dobles de prueba. ("dobles"? No se preocupe si este es un término nuevo para
usted, espere unos párrafos y todo estará claro).
Cuando realiza pruebas como esta, se enfoca en un elemento del software a la vez, de ahí el término común
prueba unitaria. El problema es que para que una sola unidad funcione, a menudo se necesitan otras
unidades, de ahí la necesidad de algún tipo de almacén en nuestro ejemplo.
En los dos estilos de prueba que he mostrado arriba, el primer caso usa un objeto de almacén real y el
segundo caso usa un almacén simulado, que por supuesto no es un objeto de almacén real. El uso de
simulacros es una forma de no usar un almacén real en la prueba, pero hay otras formas de objetos irreales
que se usan en pruebas como esta.
El vocabulario para hablar de esto pronto se complica: se usan todo tipo de palabras: stub, fake, fake,
dummy. Para este artículo voy a seguir el vocabulario del libro de Gerard Meszaros. No es lo que todos
usan, pero creo que es un buen vocabulario y como es mi ensayo puedo elegir qué palabras usar.
Meszaros utiliza el término Test Double como término genérico para cualquier tipo de objeto ficticio
utilizado en lugar de un objeto real con fines de prueba. El nombre proviene de la noción de doble de acción
en las películas. (Uno de sus objetivos era evitar el uso de cualquier nombre que ya fuera ampliamente
utilizado). Meszaros luego definió cinco tipos particulares de doble:
 Los objetos ficticios se pasan de un lado a otro, pero en realidad nunca se
usan. Por lo general, solo se usan para completar listas de parámetros.
 Los objetos falsos en realidad tienen implementaciones que funcionan, pero
generalmente toman algún atajo que los hace no aptos para la producción
(una base de datos en memoria es un buen ejemplo).
 Los stubs brindan respuestas enlatadas a las llamadas realizadas durante la prueba
y, por lo general, no responden a nada fuera de lo programado para la prueba.
 Los espías son talones que también registran cierta información basada en cómo
fueron llamados. Una forma de esto podría ser un servicio de correo electrónico que
registre cuántos mensajes se enviaron.

7
 Los simulacros son de lo que estamos hablando aquí: objetos preprogramados con
expectativas que forman una especificación de las llamadas que se espera que
reciban.
De este tipo de dobles, solo los simulacros insisten en la verificación del comportamiento. Los otros dobles
pueden, y generalmente lo hacen, usar verificación de estado. Los simulacros en realidad se comportan
como otros dobles durante la fase de ejercicio, ya que necesitan hacer que el SUT crea que está hablando
con sus colaboradores reales, pero los simulacros difieren en las fases de configuración y verificación.
Para explorar un poco más los dobles de prueba, necesitamos extender nuestro ejemplo. Muchas personas
solo usan un doble de prueba si es difícil trabajar con el objeto real. Un caso más común para un doble de
prueba sería si dijéramos que queremos enviar un mensaje de correo electrónico si no cumplimos con un
pedido. El problema es que no queremos enviar mensajes de correo electrónico reales a los clientes durante
las pruebas. Entonces, en su lugar, creamos un doble de prueba de nuestro sistema de correo electrónico, uno
que podemos controlar y manipular.
Aquí podemos comenzar a ver la diferencia entre simulacros y stubs. Si estuviéramos escribiendo una
prueba para este comportamiento de envío de correos, podríamos escribir un talón simple como este.
servicio de correo de interfaz pública {
envío de anulación pública (mensaje de mensaje);
}
clase pública MailServiceStub implementa MailService {
Lista privada<Mensaje> mensajes = new ArrayList<Mensaje>();
envío de vacío público (mensaje de mensaje) {
mensajes.add(mensaje);
}
public int numeroEnviado() {
devolver mensajes.tamaño();
}
}
Luego podemos usar la verificación de estado en el talón de esta manera.
clase OrderStateTester...
testOrderSendsMailIfUnfilled() {
Pedido pedido = nuevo Pedido(TALISKER, 51);
MailServiceStub mailer = nuevo MailServiceStub();
order.setMailer(correo);
order.fill(almacén);
afirmarEquals(1, correo.númeroEnviado());
8
}
Por supuesto, esta es una prueba muy simple, solo que se ha enviado un mensaje. No hemos probado que se
envió a la persona adecuada o con el contenido correcto, pero servirá para ilustrar el punto.
Usando simulacros, esta prueba se vería bastante diferente.
clase OrderInteractionTester...
testOrderSendsMailIfUnfilled() {
Pedido pedido = nuevo Pedido(TALISKER, 51);
Almacén simulado = simulacro(Almacén.clase);
Correo simulado = simulacro (MailService.class);
order.setMailer((MailService) mailer.proxy());

mailer.expects(once()).method("send");
warehouse.expects(once()).method("hasInventory")
.conCualquierArgumento()
.will(returnValue(falso));

order.fill((Almacén) warehouse.proxy());
}
}

En ambos casos, estoy usando un doble de prueba en lugar del servicio de correo real. Hay una diferencia en
que el stub usa verificación de estado mientras que el simulacro usa verificación de comportamiento.
Para usar la verificación de estado en el talón, necesito hacer algunos métodos adicionales en el talón para
ayudar con la verificación. Como resultado, el stub implementa MailServicepero agrega métodos de
prueba adicionales.
Los objetos simulados siempre usan verificación de comportamiento, un código auxiliar puede ir en
cualquier dirección. Meszaros se refiere a los stubs que utilizan la verificación de comportamiento como
Test Spy. La diferencia está en cómo se ejecuta y verifica exactamente el doble y dejaré que lo explores por
tu cuenta.
Pruebas clásicas y simuladas
Ahora estoy en el punto donde puedo explorar la segunda dicotomía: la que existe entre el TDD clásico y el
burlón. El gran problema aquí es cuándo usar un simulacro (u otro doble).
El estilo clásico de TDD es usar objetos reales si es posible y un doble si es incómodo usar objetos
reales. Entonces, un TDDer clásico usaría un almacén real y un doble para el servicio de correo. El tipo de
doble en realidad no importa tanto.

9
Sin embargo, un practicante de TDD simulado siempre usará un simulacro para cualquier objeto con un
comportamiento interesante. En este caso tanto para el almacén como para el servicio de correo.
Aunque los diversos marcos simulados se diseñaron teniendo en cuenta las pruebas simuladas, muchos
clasicistas los encuentran útiles para crear dobles.
Una rama importante del estilo burlón es el del Desarrollo Impulsado por el Comportamiento (BDD). BDD
fue desarrollado originalmente por mi colega Daniel Terhorst-North como una técnica para ayudar mejor a
las personas a aprender el desarrollo basado en pruebas centrándose en cómo funciona TDD como una
técnica de diseño. Esto llevó a cambiar el nombre de las pruebas como comportamientos para explorar mejor
dónde TDD ayuda a pensar en lo que debe hacer un objeto. BDD adopta un enfoque burlón, pero lo amplía,
tanto con sus estilos de nomenclatura como con su deseo de integrar el análisis dentro de su técnica. No
entraré más en esto aquí, ya que la única relevancia para este artículo es que BDD es otra variación de TDD
que tiende a usar pruebas falsas. Te dejo seguir el enlace para más información.
A veces se ve el estilo "Detroit" usado para "clásico" y "Londres" para "burlón". Esto alude al hecho de que
XP se desarrolló originalmente con el proyecto C3 en Detroit y el estilo simulado fue desarrollado por los
primeros usuarios de XP en Londres. También debo mencionar que a muchos TDD simulados no les gusta
ese término y, de hecho, cualquier terminología que implique un estilo diferente entre las pruebas clásicas y
simuladas. No consideran que haya que hacer una distinción útil entre los dos estilos.
Elegir entre las diferencias
En este artículo he explicado un par de diferencias: verificación de estado o comportamiento / TDD clásico
o falso. ¿Cuáles son los argumentos a tener en cuenta a la hora de elegir entre ellos? Comenzaré con la
opción de verificación de estado versus verificación de comportamiento.
Lo primero a considerar es el contexto. ¿Estamos pensando en una colaboración fácil, como pedido y
almacén, o incómoda, como pedido y servicio de correo?
Si se trata de una colaboración sencilla, la elección es sencilla. Si soy un TDDer clásico, no uso un
simulacro, un trozo o cualquier tipo de doble. Utilizo un objeto real y verificación de estado. Si soy un
TDDer falso, uso un simulacro y una verificación de comportamiento. No hay decisiones en absoluto.
Si se trata de una colaboración incómoda, entonces no hay decisión si soy un simulacro: solo uso simulacros
y verificación de comportamiento. Si soy un clasicista, entonces tengo una opción, pero no es gran cosa cuál
usar. Por lo general, los clasicistas decidirán caso por caso, utilizando la ruta más fácil para cada situación.
Entonces, como vemos, la verificación de estado versus verificación de comportamiento no es en su mayoría
una gran decisión. El verdadero problema es entre TDD clásico y burlón. Resulta que las características del
estado y la verificación del comportamiento afectan esa discusión, y ahí es donde concentraré la mayor parte
de mi energía.
Pero antes de hacerlo, permítanme lanzar un caso extremo. De vez en cuando te encuentras con cosas en las
que es realmente difícil usar la verificación de estado, incluso si no son colaboraciones incómodas. Un gran
10
ejemplo de esto es un caché. El punto central de un caché es que no se puede saber a partir de su estado si el
caché acertó o falló; este es un caso en el que la verificación del comportamiento sería la elección inteligente
incluso para un TDD clásico de núcleo duro. Estoy seguro de que hay otras excepciones en ambas
direcciones.
A medida que profundizamos en la elección clásica/simulada, hay muchos factores a considerar, por lo que
los dividí en grupos aproximados.
TDD de conducción
Los objetos simulados surgieron de la comunidad de XP, y una de las características principales de XP es su
énfasis en el desarrollo dirigido por pruebas, donde el diseño de un sistema evoluciona a través de la
iteración impulsada por pruebas de escritura.
Por lo tanto, no sorprende que los simulacros hablen particularmente sobre el efecto de las pruebas
simuladas en un diseño. En particular, defienden un estilo llamado desarrollo impulsado por la
necesidad. Con este estilo, comienza a desarrollar una historia de usuario escribiendo su primera prueba para
el exterior de su sistema, convirtiendo algún objeto de interfaz en su SUT. Al pensar en las expectativas
sobre los colaboradores, explora la interacción entre el SUT y sus vecinos, diseñando efectivamente la
interfaz de salida del SUT.
Una vez que ejecuta su primera prueba, las expectativas en los simulacros proporcionan una especificación
para el siguiente paso y un punto de partida para las pruebas. Convierta cada expectativa en una prueba para
un colaborador y repita el proceso para ingresar al sistema, un SUT a la vez. Este estilo también se conoce
como de afuera hacia adentro, que es un nombre muy descriptivo para él. Funciona bien con sistemas en
capas. Primero comienza programando la interfaz de usuario usando capas simuladas debajo. Luego escribe
pruebas para la capa inferior, avanzando gradualmente a través del sistema una capa a la vez. Este es un
enfoque muy estructurado y controlado, uno que muchas personas creen que es útil para guiar a los recién
llegados a OO y TDD.
Classic TDD no proporciona exactamente la misma orientación. Puede hacer un enfoque paso a paso
similar, utilizando métodos stub en lugar de simulacros. Para hacer esto, cada vez que necesite algo de un
colaborador, simplemente codifique exactamente la respuesta que requiere la prueba para que el SUT
funcione. Luego, una vez que esté verde con eso, reemplace la respuesta codificada con un código adecuado.
Pero el TDD clásico también puede hacer otras cosas. Un estilo común es el mid-out. En este estilo, toma
una función y decide qué necesita en el dominio para que esta función funcione. Obtiene los objetos de
dominio para hacer lo que necesita y una vez que están funcionando, coloca la interfaz de usuario en la parte
superior. Al hacer esto, es posible que nunca necesites falsificar nada. A mucha gente le gusta esto porque
primero enfoca la atención en el modelo de dominio, lo que ayuda a evitar que la lógica del dominio se filtre
en la interfaz de usuario.

11
Debo enfatizar que tanto los burlones como los clasicistas hacen esta historia una a la vez. Hay una escuela
de pensamiento que crea aplicaciones capa por capa, sin comenzar una capa hasta que se completa
otra. Tanto los clasicistas como los burlones tienden a tener antecedentes ágiles y prefieren iteraciones
detalladas. Como resultado, funcionan característica por característica en lugar de capa por capa.
Configuración del dispositivo
Con TDD clásico, debe crear no solo el SUT sino también todos los colaboradores que el SUT necesita en
respuesta a la prueba. Si bien el ejemplo solo tenía un par de objetos, las pruebas reales a menudo involucran
una gran cantidad de objetos secundarios. Por lo general, estos objetos se crean y se destruyen con cada
ejecución de las pruebas.
Sin embargo, las pruebas simuladas solo necesitan crear el SUT y los simulacros para sus vecinos
inmediatos. Esto puede evitar parte del trabajo involucrado en la construcción de accesorios complejos (al
menos en teoría. Me he encontrado con historias de configuraciones simuladas bastante complejas, pero eso
puede deberse a que no se usan bien las herramientas).
En la práctica, los probadores clásicos tienden a reutilizar accesorios complejos tanto como sea posible. De
la forma más sencilla, puede hacer esto colocando el código de configuración del dispositivo en el método
de configuración de xUnit. Varias clases de prueba deben usar accesorios más complicados, por lo que en
este caso se crean clases de generación de accesorios especiales. Usualmente llamo a estas Madres Objeto,
basado en una convención de nomenclatura utilizada en uno de los primeros proyectos de Thoughtworks
XP. El uso de madres es esencial en pruebas clásicas más grandes, pero las madres son un código adicional
que debe mantenerse y cualquier cambio en las madres puede tener un efecto dominó significativo a través
de las pruebas. También puede haber un costo de rendimiento en la configuración del dispositivo, aunque no
he oído que esto sea un problema grave cuando se hace correctamente. La mayoría de los objetos fijos son
baratos de crear, los que no lo son generalmente se duplican.
Como resultado, he escuchado que ambos estilos acusan al otro de ser demasiado trabajo. Los simulacros
dicen que crear los accesorios requiere mucho esfuerzo, pero los clasicistas dicen que esto se reutiliza, pero
hay que crear simulacros con cada prueba.
Aislamiento de prueba
Si introduce un error en un sistema con pruebas simuladas, generalmente solo fallarán las pruebas cuyo SUT
contiene el error. Sin embargo, con el enfoque clásico, cualquier prueba de los objetos del cliente también
puede fallar, lo que conduce a fallas en las que el objeto con errores se usa como colaborador en la prueba de
otro objeto. Como resultado, una falla en un objeto muy utilizado provoca una serie de pruebas fallidas en
todo el sistema.
Los evaluadores de Mockist consideran que esto es un problema importante; da como resultado una gran
cantidad de depuración para encontrar la raíz del error y corregirlo. Sin embargo, los clasicistas no expresan
esto como una fuente de problemas. Por lo general, el culpable es relativamente fácil de detectar al observar
12
qué pruebas fallan y los desarrolladores pueden decir que otras fallas se derivan de la falla raíz. Además, si
está probando regularmente (como debería), entonces sabe que la rotura fue causada por lo que editó por
última vez, por lo que no es difícil encontrar la falla.
Un factor que puede ser significativo aquí es la granularidad de las pruebas. Dado que las pruebas clásicas
ejercitan múltiples objetos reales, a menudo encuentra una sola prueba como la prueba principal para un
grupo de objetos, en lugar de solo una. Si ese grupo abarca muchos objetos, entonces puede ser mucho más
difícil encontrar la fuente real de un error. Lo que sucede aquí es que las pruebas son demasiado gruesas.
Es bastante probable que las pruebas simuladas tengan menos probabilidades de sufrir este problema, porque
la convención es simular todos los objetos más allá del primario, lo que deja en claro que se necesitan
pruebas más detalladas para los colaboradores. Dicho esto, también es cierto que el uso de pruebas de grano
demasiado grueso no es necesariamente una falla de las pruebas clásicas como técnica, sino más bien una
falla en hacer las pruebas clásicas correctamente. Una buena regla general es asegurarse de separar las
pruebas detalladas para cada clase. Si bien los grupos a veces son razonables, deben limitarse a solo unos
pocos objetos, no más de media docena. Además, si se encuentra con un problema de depuración debido a
pruebas de granularidad demasiado gruesa, debe depurar de forma guiada por pruebas, creando pruebas de
granularidad más fina sobre la marcha.
En esencia, las pruebas xunit clásicas no son solo pruebas unitarias, sino también pruebas de
miniintegración. Como resultado, a muchas personas les gusta el hecho de que las pruebas del cliente
pueden detectar errores que las pruebas principales de un objeto pueden haber pasado por alto, en particular
las áreas de sondeo donde interactúan las clases. Las pruebas simuladas pierden esa cualidad. Además,
también corre el riesgo de que las expectativas en las pruebas simuladas puedan ser incorrectas, lo que da
como resultado pruebas unitarias que se ejecutan en verde pero enmascaran errores inherentes.
Es en este punto que debo enfatizar que cualquiera que sea el estilo de prueba que use, debe combinarlo con
pruebas de aceptación de grano más grueso que operen en todo el sistema como un todo. A menudo me he
encontrado con proyectos que se retrasaron en el uso de pruebas de aceptación y me arrepentí.
Acoplamiento de pruebas a implementaciones
Cuando escribe una prueba simulada, está probando las llamadas salientes del SUT para asegurarse de que
se comunique correctamente con sus proveedores. Una prueba clásica solo se preocupa por el estado final,
no por cómo se derivó ese estado. Las pruebas simuladas están, por lo tanto, más acopladas a la
implementación de un método. Cambiar la naturaleza de las llamadas a los colaboradores generalmente hace
que se rompa una prueba simulada.
Este acoplamiento lleva a un par de preocupaciones. El más importante es el efecto sobre el desarrollo
dirigido por pruebas. Con las pruebas simuladas, escribir la prueba te hace pensar en la implementación del
comportamiento; de hecho, los evaluadores simulados ven esto como una ventaja. Los clasicistas, sin

13
embargo, piensan que es importante pensar solo en lo que sucede desde la interfaz externa y dejar toda
consideración de implementación hasta después de que haya terminado de escribir la prueba.
El acoplamiento a la implementación también interfiere con la refactorización, ya que es mucho más
probable que los cambios en la implementación rompan las pruebas que con las pruebas clásicas.
Esto puede empeorar por la naturaleza de los juegos de herramientas simulados. A menudo, las herramientas
simuladas especifican llamadas a métodos muy específicos y coincidencias de parámetros, incluso cuando
no son relevantes para esta prueba en particular. Uno de los objetivos del conjunto de herramientas de jMock
es ser más flexible en la especificación de las expectativas para permitir que las expectativas sean menos
estrictas en áreas donde no importa, a costa de usar cadenas que pueden hacer que la refactorización sea más
complicada.
Estilo de diseño
Para mí, uno de los aspectos más fascinantes de estos estilos de prueba es cómo afectan las decisiones de
diseño. A medida que hablé con ambos tipos de probadores, me di cuenta de algunas diferencias entre los
diseños que fomentan los estilos, pero estoy seguro de que apenas estoy rascando la superficie.
Ya he mencionado una diferencia en las capas de abordaje. Las pruebas simuladas admiten un enfoque de
afuera hacia adentro, mientras que los desarrolladores que prefieren un estilo de modelo de dominio tienden
a preferir las pruebas clásicas.
En un nivel más pequeño, me di cuenta de que los probadores simulados tienden a alejarse de los métodos
que devuelven valores, a favor de los métodos que actúan sobre un objeto de recolección. Tome el ejemplo
del comportamiento de recopilar información de un grupo de objetos para crear una cadena de informe. Una
forma común de hacer esto es hacer que el método de informes llame a los métodos de devolución de cadena
en los diversos objetos y monte la cadena resultante en una variable temporal. Es más probable que un
probador falso pase un búfer de cadenas a los diversos objetos y haga que agreguen las diversas cadenas al
búfer, tratando el búfer de cadenas como un parámetro de recopilación.
Los probadores simulados hablan más sobre evitar 'choques de trenes': cadenas de métodos de estilo
de getThis().getThat().getTheOther(). Evitar las cadenas de métodos también se conoce como seguir
la Ley de Deméter. Si bien las cadenas de métodos son un olor, el problema opuesto de los objetos
intermediarios inflados con métodos de reenvío también es un olor. (Siempre he sentido que estaría más
cómodo con la Ley de Deméter si se llamara la Sugerencia de Deméter).
Una de las cosas más difíciles de entender para las personas en el diseño orientado a objetos es el principio
"Decir, no preguntar" , que lo alienta a decirle a un objeto que haga algo en lugar de extraer datos de un
objeto para hacerlo en el código del cliente. Los simulacros dicen que el uso de pruebas simuladas ayuda a
promover esto y evitar el confeti de getter que impregna demasiado código en estos días. Los clasicistas
argumentan que hay muchas otras formas de hacer esto.

14
Un problema reconocido con la verificación basada en el estado es que puede conducir a la creación de
métodos de consulta solo para admitir la verificación. Nunca es cómodo agregar métodos a la API de un
objeto únicamente para realizar pruebas, el uso de la verificación de comportamiento evita ese problema. El
contraargumento a esto es que tales modificaciones suelen ser menores en la práctica.
Los simulacros favorecen las interfaces de roles y afirman que el uso de este estilo de prueba fomenta más
interfaces de roles, ya que cada colaboración se simula por separado y, por lo tanto, es más probable que se
convierta en una interfaz de roles. Entonces, en mi ejemplo anterior usando un búfer de cadena para generar
un informe, es más probable que un imitador invente un rol particular que tenga sentido en ese dominio,
que puede implementarse mediante un búfer de cadena.
Es importante recordar que esta diferencia en el estilo de diseño es un motivador clave para la mayoría de
los burlones. Los orígenes de TDD fueron el deseo de obtener pruebas de regresión automática sólidas que
respaldaran el diseño evolutivo. En el camino, sus practicantes descubrieron que escribir pruebas primero
mejoró significativamente el proceso de diseño. Los simulacros tienen una idea clara de qué tipo de diseño
es un buen diseño y han desarrollado bibliotecas simuladas principalmente para ayudar a las personas a
desarrollar este estilo de diseño.
Entonces, ¿debo ser un clasicista o un burlón?
Creo que esta es una pregunta difícil de responder con confianza. Personalmente, siempre he sido un TDDer
clásico a la antigua y hasta ahora no veo ninguna razón para cambiar. No veo ningún beneficio convincente
para el TDD simulado y me preocupan las consecuencias de acoplar las pruebas a la implementación.
Esto me llamó especialmente la atención cuando observé a un programador falso. Me gusta mucho el hecho
de que mientras escribes la prueba te enfocas en el resultado del comportamiento, no en cómo se hace. Un
simulacro está constantemente pensando en cómo se va a implementar el SUT para escribir las
expectativas. Esto se siente realmente antinatural para mí.
También sufro la desventaja de no probar el TDD falso en nada más que juguetes. Como aprendí del mismo
Desarrollo dirigido por pruebas, a menudo es difícil juzgar una técnica sin probarla seriamente. Conozco a
muchos buenos desarrolladores que están muy contentos y se burlan convencidos. Entonces, aunque todavía
soy un clasicista convencido, prefiero presentar ambos argumentos de la manera más justa posible para que
pueda tomar su propia decisión.
Entonces, si las pruebas de simulación le parecen atractivas, le sugiero que lo pruebe. Vale la pena intentarlo
especialmente si tiene problemas en algunas de las áreas que el simulador de TDD pretende mejorar. Veo
dos áreas principales aquí. Una es si pasa mucho tiempo depurando cuando las pruebas fallan porque no se
rompen limpiamente y no le dicen dónde está el problema. (También puede mejorar esto usando TDD
clásico en clústeres más detallados). La segunda área es que si sus objetos no contienen suficiente
comportamiento, las pruebas simuladas pueden alentar al equipo de desarrollo a crear más objetos ricos en
comportamiento.
15
Pensamientos finales
A medida que ha crecido el interés en las pruebas unitarias, los marcos xunit y el desarrollo basado en
pruebas, cada vez más personas se encuentran con objetos simulados. La mayoría de las veces, las personas
aprenden un poco sobre los marcos de objetos simulados, sin comprender completamente la división
simulada/clásica que los sustenta. Cualquiera que sea el lado de esa división en el que te apoyes, creo que es
útil comprender esta diferencia de puntos de vista. Si bien no es necesario ser un burlón para encontrar útiles
los marcos simulados, es útil comprender el pensamiento que guía muchas de las decisiones de diseño del
software.
El propósito de este artículo fue, y es, señalar estas diferencias y exponer las compensaciones entre
ellas. Hay más en el pensamiento burlón de lo que he tenido tiempo de analizar, particularmente sus
consecuencias en el estilo de diseño. Espero que en los próximos años veamos más escritos sobre esto y eso
profundice nuestra comprensión de las fascinantes consecuencias de escribir pruebas antes del código.

16

También podría gustarte