¿Cuál es el propósito de los objetos simulados?

167

Soy nuevo en las pruebas unitarias y escucho continuamente las palabras "objetos simulados". En términos simples, ¿alguien puede explicar qué son los objetos simulados y para qué se usan típicamente al escribir pruebas unitarias?

agentbanks217
fuente
12
Son una herramienta para la ingeniería masiva de cosas con flexibilidad que no necesita para el problema en cuestión.
dsimcha
2
posible duplicado de ¿Qué es burlarse?
nawfal

Respuestas:

361

Como usted dice que es nuevo en las pruebas unitarias y que solicita objetos simulados en "términos simples", intentaré con un ejemplo simple.

Examen de la unidad

Imagine pruebas unitarias para este sistema:

cook <- waiter <- customer

En general, es fácil imaginar probar un componente de bajo nivel como cook:

cook <- test driver

El conductor de prueba simplemente ordena diferentes platos y verifica que el cocinero devuelva el plato correcto para cada pedido.

Es más difícil probar un componente intermedio, como el camarero, que utiliza el comportamiento de otros componentes. Un probador ingenuo podría probar el componente de camarero de la misma manera que probamos el componente de cocción:

cook <- waiter <- test driver

El conductor de prueba pediría diferentes platos y se aseguraría de que el camarero devuelva el plato correcto. Desafortunadamente, eso significa que esta prueba del componente de camarero puede depender del comportamiento correcto del componente de cocción. Esta dependencia es aún peor si el componente de cocción tiene características poco amigables con la prueba, como un comportamiento no determinista (el menú incluye la sorpresa del chef como plato), muchas dependencias (el cocinero no cocinará sin todo su personal) o muchos recursos (algunos platos requieren ingredientes caros o tardan una hora en cocinarse).

Dado que esta es una prueba de camarero, idealmente, queremos probar solo al camarero, no al cocinero. Específicamente, queremos asegurarnos de que el camarero transmita el pedido del cliente al cocinero correctamente y entregue la comida del cocinero al cliente correctamente.

La prueba de unidad significa probar unidades de forma independiente, por lo que un mejor enfoque sería aislar el componente bajo prueba (el camarero) utilizando lo que Fowler llama dobles de prueba (maniquíes, trozos, falsificaciones, simulacros) .

    -----------------------
   |                       |
   v                       |
test cook <- waiter <- test driver

Aquí, el cocinero de prueba está "confabulado" con el conductor de prueba. Idealmente, el sistema bajo prueba está diseñado para que el cocinero de prueba pueda ser fácilmente sustituido ( inyectado ) para trabajar con el camarero sin cambiar el código de producción (por ejemplo, sin cambiar el código del camarero).

Objetos simulados

Ahora, la prueba de cocción (prueba doble) podría implementarse de diferentes maneras:

  • un cocinero falso: alguien que finge ser cocinero usando cenas congeladas y un microondas,
  • un cocinero auxiliar: un vendedor de perritos calientes que siempre te ofrece perritos calientes sin importar lo que pidas, o
  • un simulador de cocina: un policía encubierto que sigue un guión que finge ser un cocinero en una operación encubierta.

Vea el artículo de Fowler para obtener más detalles sobre falsificaciones vs trozos vs simulacros vs tontos , pero por ahora, centrémonos en un simulador de cocina.

    -----------------------
   |                       |
   v                       |
mock cook <- waiter <- test driver

Una gran parte de la unidad que prueba el componente de camarero se centra en cómo interactúa el camarero con el componente de cocción. Un enfoque basado en simulacro se centra en especificar completamente cuál es la interacción correcta y detectar cuándo sale mal.

El objeto simulado sabe de antemano qué se supone que sucederá durante la prueba (por ejemplo, cuál de sus métodos se invocarán las llamadas, etc.) y el objeto simulado sabe cómo se supone que reaccionará (por ejemplo, qué valor de retorno proporcionar). El simulacro indicará si lo que realmente sucede difiere de lo que se supone que debe suceder. Se podría crear un objeto simulado personalizado desde cero para cada caso de prueba para ejecutar el comportamiento esperado para ese caso de prueba, pero un marco de simulación se esfuerza por permitir que dicha especificación de comportamiento se indique clara y fácilmente directamente en el caso de prueba.

La conversación en torno a una prueba simulada podría verse así:

prueba del conductor para burlarse del cocinero : espera una orden de hot dog y dale este hot dog ficticio en respuesta

prueba del conductor (haciéndose pasar por el cliente) al camarero : me gustaría un perrito caliente, por favor,
camarero para burlarse del cocinero : 1 perro caliente, por favor,
burlarse del cocinero al camarero : pedir: 1 perro caliente listo (le da el perro caliente falso al camarero)
camarero para probar al conductor : aquí está su hot dog (le da un hot dog ficticio al conductor de prueba)

piloto de prueba : ¡PRUEBA EXITOSA!

Pero como nuestro camarero es nuevo, esto es lo que podría suceder:

prueba del conductor para burlarse del cocinero : espera una orden de hot dog y dale este hot dog ficticio en respuesta

prueba del conductor (haciéndose pasar por el cliente) para el camarero : me gustaría un hot dog, por favor, el
camarero para burlarse del cocinero : 1 hamburguesa, por favor, el
cocinero falso detiene la prueba: me dijeron que esperar una orden de perritos calientes!

el controlador de prueba observa el problema: ¡PRUEBA FALLIDA! - el camarero cambió el orden

o

prueba del conductor para burlarse del cocinero : espera una orden de hot dog y dale este hot dog ficticio en respuesta

prueba del conductor (haciéndose pasar por el cliente) al camarero : me gustaría un perrito caliente, por favor,
camarero para burlarse del cocinero : 1 perro caliente, por favor,
burlarse del cocinero al camarero : pedir: 1 perro caliente listo (le da el perro caliente falso al camarero)
camarero para probar al conductor : aquí están tus papas fritas (da papas fritas de otro orden para probar el controlador)

el conductor de prueba observa las inesperadas papas fritas: ¡LA PRUEBA FALLÓ! el camarero devolvió el plato equivocado

Puede ser difícil ver claramente la diferencia entre objetos simulados y trozos sin un ejemplo contrastante basado en trozos para ir con esto, pero esta respuesta ya es demasiado larga :-)

También tenga en cuenta que este es un ejemplo bastante simplista y que los marcos de imitación permiten algunas especificaciones bastante sofisticadas del comportamiento esperado de los componentes para admitir pruebas exhaustivas. Hay mucho material sobre objetos simulados y marcos burlones para obtener más información.

Bert F
fuente
12
Esta es una gran explicación, pero ¿no estás probando hasta cierto punto la implementación del camarero? En su caso, probablemente esté bien porque está comprobando que usa la API correcta, pero ¿qué pasa si hay diferentes formas de hacerlo y el camarero puede elegir una u otra? Pensé que el objetivo de las pruebas unitarias era probar la API, y no la implementación. (Esta es una pregunta que siempre me pregunto cuando leo sobre burlas.)
davidtbernal
8
Gracias. No puedo decir si estamos probando la "implementación" sin ver (o definir) la especificación para el camarero. Puede suponer que el camarero puede cocinar el plato él mismo o completar el pedido por la calle, pero supongo que las especificaciones para el camarero incluyen el uso del chef previsto; después de todo, el chef de producción es un chef costoso y gourmet y nosotros ' Prefiero que nuestro camarero lo use. Sin esa especificación, supongo que tendría que concluir que tiene razón: el camarero puede completar el pedido como quiera para ser "correcto". OTOH, sin una especificación, la prueba no tiene sentido. [Continúa ...]
Bert F
8
SIN EMBARGO, haces un gran punto que conduce al fantástico tema de la prueba de unidad de caja blanca vs caja negra. No creo que haya un consenso de la industria que diga que las pruebas unitarias deben ser de caja negra en lugar de caja blanca ("pruebe la API, no la implementación"). Creo que la mejor prueba unitaria probablemente deba ser una combinación de las dos para equilibrar la fragilidad de la prueba con la cobertura del código y la integridad del caso de prueba.
Bert F
1
Según mi opinión, esta respuesta no es lo suficientemente técnica. Quiero saber por qué debería usar un objeto simulado cuando puedo usar objetos reales.
Niklas R.
1
¡Gran explicación! ¡¡Gracias!! @BertF
Bharath Murali
28

Un objeto simulado es un objeto que sustituye a un objeto real. En la programación orientada a objetos, los objetos simulados son objetos simulados que imitan el comportamiento de los objetos reales de manera controlada.

Un programador de computadora generalmente crea un objeto simulado para probar el comportamiento de algún otro objeto, de la misma manera que un diseñador de automóviles usa un maniquí de prueba de choque para simular el comportamiento dinámico de un humano en los impactos de vehículos.

http://en.wikipedia.org/wiki/Mock_object

Los objetos simulados le permiten configurar escenarios de prueba sin tener que soportar recursos grandes y difíciles de manejar, como bases de datos. En lugar de llamar a una base de datos para realizar pruebas, puede simular su base de datos utilizando un objeto simulado en sus pruebas unitarias. Esto lo libera de la carga de tener que configurar y destruir una base de datos real, solo para probar un método único en su clase.

La palabra "simulacro" a veces se usa erróneamente de manera intercambiable con "trozo". Las diferencias entre las dos palabras se describen aquí. Esencialmente, un simulacro es un objeto auxiliar que también incluye las expectativas (es decir, "aserciones") para el comportamiento adecuado del objeto / método bajo prueba.

Por ejemplo:

class OrderInteractionTester...
  public void testOrderSendsMailIfUnfilled() {
    Order order = new Order(TALISKER, 51);
    Mock warehouse = mock(Warehouse.class);
    Mock mailer = mock(MailService.class);
    order.setMailer((MailService) mailer.proxy());

    mailer.expects(once()).method("send");
    warehouse.expects(once()).method("hasInventory")
      .withAnyArguments()
      .will(returnValue(false));

    order.fill((Warehouse) warehouse.proxy());
  }
}

Observe que los objetos simulados warehousey mailerestán programados con los resultados esperados.

Robert Harvey
fuente
2
La definición que ha dado difiere en particular de "objeto de código auxiliar" y, como tal, no explica qué es un objeto simulado.
Brent Arias
Otra corrección "la palabra 'Mock' a veces se usa erróneamente de manera intercambiable con 'stub'".
Brent Arias
@ Myst: El uso de las dos palabras no es universal; Varía entre los autores. Fowler lo dice, y el artículo de Wikipedia lo dice. Sin embargo, siéntase libre de editar el cambio y eliminar su voto negativo. :)
Robert Harvey
1
Estoy de acuerdo con Robert: el uso de la palabra "simulacro" tiende a variar en la industria, pero no existe una definición establecida de acuerdo con mi experiencia, excepto que normalmente NO es el objeto que realmente se está probando, sino que existe para facilitar las pruebas cuando se usa objeto o todas sus partes serían muy inconvenientes y de poca consecuencia.
mkelley33
15

Los objetos simulados son objetos simulados que imitan el comportamiento de los reales. Por lo general, escribe un objeto simulado si:

  • El objeto real es demasiado complejo para incorporarlo en una prueba de unidad (por ejemplo, una comunicación de red, puede tener un objeto simulado que simule ser el otro par)
  • El resultado de su objeto no es determinista
  • El objeto real aún no está disponible.
Dani Cricco
fuente
12

Un objeto simulado es un tipo de prueba doble . Está utilizando mockobjects para probar y verificar el protocolo / interacción de la clase bajo prueba con otras clases.

Por lo general, tendrá una especie de 'programa' o 'registro' de expectativas: llamadas de método que espera que su clase le haga a un objeto subyacente.

Digamos, por ejemplo, que estamos probando un método de servicio para actualizar un campo en un widget. Y que en su arquitectura hay un WidgetDAO que se ocupa de la base de datos. Hablar con la base de datos es lento y configurarlo y limpiarlo después es complicado, por lo que nos burlaremos del WidgetDao.

pensemos qué debe hacer el servicio: debe obtener un widget de la base de datos, hacer algo con él y guardarlo nuevamente.

Entonces, en pseudo-idioma con una biblioteca de pseudo-simulacro, tendríamos algo como:

Widget sampleWidget = new Widget();
WidgetDao mock = createMock(WidgetDao.class);
WidgetService svc = new WidgetService(mock);

// record expected calls on the dao
expect(mock.getById(id)).andReturn(sampleWidget);   
expect(mock.save(sampleWidget);

// turn the dao in replay mode
replay(mock);

svc.updateWidgetPrice(id,newPrice);

verify(mock);    // verify the expected calls were made
assertEquals(newPrice,sampleWidget.getPrice());

De esta manera, podemos probar fácilmente el desarrollo de clases que dependen de otras clases.

Peter Tillemans
fuente
11

Recomiendo encarecidamente un gran artículo de Martin Fowler que explique qué son exactamente los simulacros y en qué se diferencian de los trozos.

Adam Byrtek
fuente
10
No es exactamente amigable para principiantes, ¿verdad?
Robert Harvey
@Robert Harvey: Tal vez, de todos modos, es bueno ver que fue útil para aclarar su respuesta :)
Adam Byrtek
Los artículos de Martin Fowler están escritos a la manera de RFC: secos y fríos.
revo
9

Cuando la unidad prueba alguna parte de un programa de computadora, lo ideal es probar solo el comportamiento de esa parte en particular.

Por ejemplo, mire el pseudocódigo siguiente de una pieza imaginaria de un programa que usa otro programa para llamar a print algo:

If theUserIsFred then
    Call Printer(HelloFred)
Else
   Call Printer(YouAreNotFred)
End

Si estuviera probando esto, principalmente querría probar la parte que analiza si el usuario es Fred o no. Realmente no quieres probar la Printerparte de las cosas. Esa sería otra prueba.

Aquí es donde entran los objetos simulados. Pretenden ser otro tipo de cosas. En este caso, usaría un Mock Printerpara que actuara como una impresora real, pero no haría cosas inconvenientes como imprimir.


Hay varios otros tipos de objetos de simulación que puede usar que no son simulacros. Lo principal que hace que Mocks Mocks es que se pueden configurar con comportamientos y expectativas.

Las expectativas le permiten a tu simulacro generar un error cuando se usa incorrectamente. Entonces, en el ejemplo anterior, puede estar seguro de que se llama a la impresora con HelloFred en el caso de prueba "el usuario es Fred". Si eso no sucede, tu Mock puede advertirte.

Comportamiento en simulacros significa que, por ejemplo, su código hizo algo como:

If Call Printer(HelloFred) Returned SaidHello Then
    Do Something
End

Ahora desea probar qué hace su código cuando se llama a la Impresora y devuelve SaidHello, de modo que puede configurar el Mock para que devuelva SaidHello cuando se llama con HelloFred.

Un buen recurso en torno a esto es Martin Fowlers post Mocks An't Stubs

David Hall
fuente
7

Los objetos simulados y trozos son una parte crucial de las pruebas unitarias. De hecho, hacen un largo camino para asegurarse de que esté probando unidades , en lugar de grupos de unidades.

En pocas palabras, usa stubs para romper la dependencia de SUT (Sistema bajo prueba) en otros objetos y simulacros para hacer eso y verificar que SUT haya llamado ciertos métodos / propiedades en la dependencia. Esto se remonta a los principios fundamentales de las pruebas unitarias: que las pruebas deben ser fáciles de leer, rápidas y no requieren configuración, lo que puede implicar el uso de todas las clases reales.

En general, puede tener más de un trozo en su prueba, pero solo debe tener un simulacro. Esto se debe a que el propósito del simulacro es verificar el comportamiento y su prueba solo debe probar una cosa.

Escenario simple usando C # y Moq:

public interface IInput {
  object Read();
}
public interface IOutput {
  void Write(object data);
}

class SUT {
  IInput input;
  IOutput output;

  public SUT (IInput input, IOutput output) {
    this.input = input;
    this.output = output;
  }

  void ReadAndWrite() { 
    var data = input.Read();
    output.Write(data);
  }
}

[TestMethod]
public void ReadAndWriteShouldWriteSameObjectAsRead() {
  //we want to verify that SUT writes to the output interface
  //input is a stub, since we don't record any expectations
  Mock<IInput> input = new Mock<IInput>();
  //output is a mock, because we want to verify some behavior on it.
  Mock<IOutput> output = new Mock<IOutput>();

  var data = new object();
  input.Setup(i=>i.Read()).Returns(data);

  var sut = new SUT(input.Object, output.Object);
  //calling verify on a mock object makes the object a mock, with respect to method being verified.
  output.Verify(o=>o.Write(data));
}

En el ejemplo anterior, utilicé Moq para demostrar trozos y simulacros. Moq usa la misma clase para ambos, Mock<T>lo que lo hace un poco confuso. Independientemente, en tiempo de ejecución, la prueba fallará si output.Writeno se llama con datos como parameter, mientras que la falla en la llamada input.Read()no fallará.

Igor Zevaka
fuente
4

Como sugirió otra respuesta a través de un enlace a "Los simulacros no son trozos ", los son una forma de "prueba doble" para usar en lugar de un objeto real. Lo que los hace diferentes de otras formas de dobles de prueba, como los objetos de código auxiliar, es que otros dobles de prueba ofrecen verificación de estado (y opcionalmente simulación) mientras que los simulacros ofrecen verificación de comportamiento (y opcionalmente simulación).

Con un apéndice, puede llamar a varios métodos en el apéndice en cualquier orden (o incluso de forma repetitiva) y determinar el éxito si el apéndice ha capturado un valor o estado que deseaba. En contraste, un objeto simulado espera que se invoquen funciones muy específicas, en un orden específico e incluso un número específico de veces. La prueba con un objeto simulado se considerará "fallida" simplemente porque los métodos se invocaron en una secuencia o recuento diferente, ¡incluso si el objeto simulado tenía el estado correcto cuando concluyó la prueba!

De esta manera, los objetos simulados a menudo se consideran más estrechamente acoplados al código SUT que los objetos de código auxiliar. Eso puede ser algo bueno o malo, dependiendo de lo que intente verificar.

Brent Arias
fuente
3

Parte del punto de usar objetos simulados es que no tienen que implementarse realmente de acuerdo con las especificaciones. Solo pueden dar respuestas ficticias. Por ejemplo, si tiene que implementar los componentes A y B, y ambos "se llaman" (interactúan) entre sí, entonces no puede probar A hasta que se implemente B, y viceversa. En el desarrollo basado en pruebas, este es un problema. Entonces creas objetos simulados ("ficticios") para A y B, que son muy simples, pero dan algún tipo de respuesta cuando interactúan con ellos. De esa manera, puede implementar y probar A usando un objeto simulado para B.

LarsH
fuente
1

Para php y phpunit se explica bien en phpunit documentaion. ver aquí la documentación de phpunit

En palabras simples, el objeto de burla es solo un objeto ficticio del original y devuelve su valor de retorno, este valor de retorno se puede usar en la clase de prueba

Gautam Rai
fuente
0

Es una de las principales perspectivas de las pruebas unitarias. Sí, está intentando probar su única unidad de código y los resultados de su prueba no deberían ser relevantes para el comportamiento de otros beans u objetos. por lo tanto, debe burlarse de ellos utilizando objetos Mock con alguna respuesta correspondiente simplificada.

Mohsen Msr
fuente