Share via


Este artículo proviene de un motor de traducción automática.

Controlado por pruebas diseño

Uso de simulacros Y pruebas para objetos basada en funciones de diseño

Isaiah Perumalla

Descarga de código de la Galería de código de MSDN
Examinar el código en línea

En este artículo se describen:

  • Las pruebas de interacción del, No implementación
  • Descubrir las funciones y a través de la extracción de las conversaciones
  • Través de la extracción de las conversaciones
  • Refactorizar el código para aclarar el propósito
En este artículo se utilizan las siguientes tecnologías:
Desarrollo controlado por pruebas, NMock Framework

Contenido

Interacción del, No implementación
Leer los códigos de barras
Descubrir las funciones
Finalizar la venta
Través de la extracción de las conversaciones
Calcular recibos
Obtener la descripción del producto
Refactorización
Ajustar hacia arriba

En el territorio de desarrollo Test-Driven (TDD), objetos de simulacro le ayudarán a descubrir las funciones que deben desempeñan objetos dentro de un sistema, haciendo hincapié en cómo se relacionan los objetos con cada uno en vez de su estructura interna. Esta técnica se puede emplear para admitir buen diseño orientado a objetos. Utilizando los objetos de simulacro como ayuda para diseñar resulta mucho más interesante de la práctica común de utilizarlas simplemente para aislar un sistema de dependencias externas.

Una de las ventajas más importantes de TDD es que mejora el diseño general del código mediante obligarle a pensar en el diseño de interfaz de un objeto basada en su uso en lugar de su implementación. Objetos de simulacro complementan el proceso de TDD de sistemas de orientado a objetos, al permitirle escribir el código de prueba para un objeto como si ya tenía todo 3 desde su entorno. Para ello, rellenar en lugar de colaboradores un objeto con los simulacros. Esto le permite diseñar las interfaces de colaboradores de un objeto en términos de las funciones que reproducir, antes de que las implementaciones concretas de ellos aún existen. Esto conduce a un proceso de descubrimiento que se incluyen las interfaces de colaboradores de un objeto en la existencia según los requisitos inmediatos, controlados por necesidad.

Después de este proceso de desarrollo de prueba de primera mediante objetos de simulacro, no sólo se puede diseñar la interfaz de un objeto de su uso, pero también puede descubrir y diseñe las interfaces de un objeto necesita de sus colaboradores.

En este artículo describe cómo utilizar TDD con objetos de simulacro para diseñar el código de orientado a objetos en términos de las funciones y responsabilidades, no clasificación de objetos en jerarquías de clase.

Interacción del, No implementación

Uno de los principios fundamentales de programación orientada a objetos es localizar toda la lógica en estado para el objeto que contiene el estado y para ocultar las estructuras internas de un objeto y sus transiciones de estado. Debe ser el énfasis en cómo objetos se comunican con otros objetos en el entorno cuando se desencadena un evento. En la práctica, puede ser difícil de lograr. El resultado de objetos de diseño de este modo es que cada objeto expone ningún estado visible ni en cualquiera de la estructura interna. Puesto que no hay ningún estado de visibilidad, no puede consultar las estructuras internas o estado para probar el comportamiento de los objetos haciendo aserciones en su estado. Lo único que ve es la forma que del objeto interactúa con su entorno. Sin embargo, puede registrar estas interacciones para comprobar un comportamiento de objeto.

Mediante objetos de simulacro permite descubrir cómo un objeto interactúa con sus colaboradores realizando aserciones que un objeto envía el mensaje correcto a sus colaboradores en un escenario determinado. Esto desplaza esfuerzo de enfoque y diseño de cómo objetos se clasifica y estructuradas para cómo objetos comunican entre sí. Esto, a su vez, conduce el diseño de su sistema para una " Tell, no preguntar " en cada objeto sabe muy poco sobre el estado de sus objetos adyacentes o la estructura de estilo de diseño. Esto hace que el sistema mucho más flexible al permitir cambiar el comportamiento del sistema al redactar conjuntos diferentes de objetos.

Para ilustrar esta técnica, le guiará a través de un breve ejemplo TDD mediante objetos de simulacro.

Leer los códigos de barras

Me generar un sistema punto de venta para una cadena grande supermercado. Los sistemas de catálogo de productos están en la oficina central y obtener acceso a un servicio RESTful. El enfoque principal de la primera función es para que el sistema para usar información de código de barras, bien manualmente especificado o leer desde un escáner de código de barras, para identificar un elemento, recuperar su precio y calcular el total albarán de compra para la venta.

El sistema se desencadena cuando el cashier entra en comandos con una pantalla táctil o un escáner de código de barras. Comandos de dispositivos de entrada se envían como una cadena en el siguiente formato:

  • El comando: NewSale;
  • El comando: EndSale;
  • Código de barras de entrada: = 100008888559, cantidad = 1;

Todos los códigos de barras siguen la combinación de UPC; los primeros seis caracteres del código de barras identificar el código de fabricante y los caracteres siguientes cinco identificar un código de producto. El sistema de información de producto requiere el fabricante del código y el código de producto para poder recuperar la descripción del producto para un artículo.

El sistema primero necesita recibir y interprete de comandos de dispositivos de entrada (teclado y escáneres). Cuando haya finalizado la venta, el sistema debe calcular y imprimir una confirmación mediante el catálogo de productos de la oficina central para recuperar la descripción del producto y el precio.

El primer paso es descodificar los mensajes sin procesar enviados desde los dispositivos de entrada en algo que representa los eventos de desprotección. Empiezo con el comando más sencillo. El nuevo comando venta inicial simplemente desencadena un nuevo evento de venta dentro del sistema. Necesita un objeto para descodificar los mensajes sin formato de los dispositivos de entrada y convertirlos en algo que representa un evento en función del dominio de aplicación. A continuación se muestra la primera prueba:

[TestFixture]
public class CommandParserTests {

  [Test]
  public void NotifiesListenerOfNewSaleEvent() {
    var commandParser = new CommandParser();
    var newSaleCommand= "Command:NewSale";
    commandParser.Parse(newSaleCommand);
  }
}

Tenga en cuenta que CommandParser aún no existe en este momento. ESTOY utilizando las pruebas en la unidad fuera de la interfaz de que este objeto proporciona.

¿Podría cómo PUEDO saber si este objeto interpreta el comando enviado desde dispositivos de entrada correctamente? Tras el principio "indicar a, No preguntar", el objeto CommandParser debe indicar el objeto tiene una relación con la que una nueva venta se inicia. En este momento, no sabe quién o qué lo tiene una relación con. Es todavía se detecte.

Descubrir las funciones

Hasta ahora, todo lo conocer el CommandParser collaborator es que necesita saber cuando se detectan los eventos relacionados con una venta en el sistema. Se elija un nombre que describa sólo esta funcionalidad, SaleEventListener y la nota que representa una función en el sistema. Una función se puede ver como una ranura con nombre en un sistema de software que puede rellenarse por cualquier objeto que puede cumplir las responsabilidades de la función. Para descubrir las funciones, debe examinar las interacciones visibles entre un objeto y su collaborator al realizar una actividad en el sistema, centrarse sólo en los aspectos de los objetos necesarios para que la actividad que se describen.

En C#, se puede especificar una función mediante una interfaz. En este ejemplo, la CommandParser requiere un objeto de su entorno que pueda reproducir la función de SaleEventListener. Sin embargo, las interfaces sólo son inadecuadas describir cómo se comunican un grupo de objetos para completar una tarea. Mediante objetos de simulacro, puede describir esto en la prueba como se muestra en la figura 1 .

Figura 1, definición de la función SaleEventListener

[TestFixture]
public class CommandParserTests {
  private Mockery mockery;

  [SetUp]
  public void BeforeTest() {
    mockery = new Mockery();
  }

  [Test]
  public void NotifiesListenerOfNewSaleEvent() {
    var saleEventListener = mockery.NewMock<ISaleEventListener>();
    var commandParser = new CommandParser();
    var newSaleCommand = "Command:NewSale";
    commandParser.Parse(newSaleCommand);
  }
}

Definir explícitamente la función SaleEventListener mediante una interfaz, ISaleEventListener. Estoy ¿por qué utilizando una interfaz escucha en lugar de la compatibilidad integrada con que C# proporciona para los eventos a través de delegados?

Hay un número de razones por las eligió para utilizar una interfaz de agente de escucha sobre los eventos y delegados. La interfaz de agente de escucha explícitamente identifica la función que el analizador de comando colabora con. Tenga en cuenta que no me acoplamiento el CommandParser para cualquier clase concreta, pero que mediante una interfaz de agente de escucha, me indica explícitamente la relación entre el CommandParser y la función que colabora con. Eventos y delegados es posible que permiten a enlazar en código arbitrario para el CommandParser, pero no expresa las relaciones posibles entre los objetos en el dominio. Mediante una interfaz de agente de escucha en este caso me permite utilizar los patrones de comunicación entre los objetos para expresar con claridad el modelo de dominio.

En mi ejemplo, la CommandParser se analizar comandos y enviar diferentes tipos de eventos en términos del dominio de aplicación. Estos se estar siempre conectados hacia arriba al mismo tiempo. En este caso es mucho más cómodo pasar una referencia a una instancia de una interfaz de agente de escucha que puede procesar un conjunto de eventos en lugar de adjuntar delegados diferentes a cada evento diferente. A continuación, necesitan una instancia de esta interfaz para la CommandParser puede interactuar con él. Utilizar el Marco NMock para crear dinámicamente una instancia de esta interfaz.

Ahora es necesario especificar lo que espero objeto de CommandParser para indicar el objeto reproducir la función de ISaleEventListener al que interpreta un comando desde el dispositivo de entrada. Especifique escribiendo una expectativa en la implementación de simulacro de ISaleEventListener:

[Test]
public void NotifiesListenerOfNewSaleEvent() {
  var saleEventListener = mockery.NewMock<ISaleEventListener>();
  var commandParser = new CommandParser();
  var newSaleCommand = "Command:NewSale";

  Expect.Once.On(saleEventListener).Method("NewSaleInitiated");

  commandParser.Parse(newSaleCommand);
  mockery.VerifyAllExpectationsHaveBeenMet();
}

El acto de escribir este pasó expectativa fuera de la interfaz del CommandParser requiere desde su collaborator. Mediante un objeto de simulacro, puede descubrir y diseñe las interfaces de que un objeto requiere de sus colaboradores antes de que las implementaciones de estos colaboradores aún existen. Esto permite que permanecer centrado en la CommandParser sin preocuparse por las implementaciones de sus colaboradores.

Para obtener que compilar esta prueba, necesita para crear la clase CommandParser y la interfaz ISaleEventListener:

public class CommandParser {
  public void Parse(string messageFromDevice) {
  }
}

public interface ISaleEventListener {
  void NewSaleInitiated();
}

Compila la prueba, ejecute la prueba y aparece el siguiente error:

TestCase 'Domain.Tests.CommandParserTests.NotifiesListenerOfNewSaleEvent'
failed: NMock2.Internal.ExpectationException : not all expected invocations were performed
Expected:
  1 time: saleEventListener.NewSaleInitiated(any arguments) [called 0 times]
    at NMock2.Mockery.FailUnmetExpectations()
    at NMock2.Mockery.VerifyAllExpectationsHaveBeenMet()

El marco de trabajo NMock informa de que la implementación de simulacro de ISaleEventListener esperaba el método NewSaleInitiated que se va a invocar una vez, pero esto no sucede. Para obtener el que se pase la prueba, es necesario pase la instancia simulacro de la saleEventListener al objeto CommandParser como dependencia.

[Test]
public void NotifiesListenerOfNewSaleEvent() {
  var saleEventListener = mockery.NewMock<ISaleEventListener>();
  var commandParser = new CommandParser(saleEventListener);
  var newSaleCommand = "Command:NewSale";

  Expect.Once.On(saleEventListener).Method("NewSaleInitiated");

  ommandParser.Parse(newSaleCommand);
  mockery.VerifyAllExpectationsHaveBeenMet();
}

La prueba ahora especifica explícitamente la dependencia que del CommandParser tiene en su entorno, que especifica qué mensaje (llamada del método) que debe recibir el saleEventListener.

Ésta es la implementación más sencilla que pase esta prueba:

public class CommandParser {
  private readonly ISaleEventListener saleEventListener;

  public CommandParser(ISaleEventListener saleEventListener) {
    this.saleEventListener = saleEventListener;
  }

  public void Parse(string messageFromDevice) {
    saleEventListener.NewSaleInitiated();
  }
}

Finalizar la venta

Ahora que se pasa la prueba, puede continuar con la siguiente prueba. El siguiente escenario simple éxito será comprobar que la CommandParser puede descodificar el comando de venta de fin y notificar el sistema.

[Test]
public void NotifiesListenerOfSaleCompletedEvent() {
  var saleEventListener = mockery.NewMock<ISaleEventListener>();
  var commandParser = new CommandParser(saleEventListener);
  var endSaleCommand = "Command:EndSale";

  Expect.Once.On(saleEventListener).Method("SaleCompleted");

  commandParser.Parse(endSaleCommand);
  mockery.VerifyAllExpectationsHaveBeenMet();
}

La prueba de unidades sin la necesidad de otro método la ISaleEventListener debe admitir la interfaz.

public interface ISaleEventListener {
  void NewSaleInitiated();
  void SaleCompleted();
}

Ejecutar la prueba de produce un error, como cabría esperar. NMock muestra la mensaje de error siguientes:

TestCase 'Domain.Tests.CommandParserTests.NotifiesListenerOfSaleCompletedEvent'
failed: NMock2.Internal.ExpectationException : unexpected invocation of saleEventListener.NewSaleInitiated()
Expected:
  1 time: saleEventListener.SaleCompleted(any arguments) [called 0 times]

Debe interpretar el comando sin formato y llamar al método correspondiente en la instancia del objeto saleEventListener. La implementación simple en la figura 2 debe obtener que se pase la prueba.

La Figura 2 implementación saleEventListener

public class CommandParser {
  private const string END_SALE_COMMAND = "EndSale";
  private readonly ISaleEventListener saleEventListener;

  public CommandParser(ISaleEventListener saleEventListener) {
    this.saleEventListener = saleEventListener;
  }

  public void Parse(string messageFromDevice) {
    var commandName = messageFromDevice.Split(':')[1].Trim();
    if (END_SALE_COMMAND.Equals(commandName))
      saleEventListener.SaleCompleted();
    else
      saleEventListener.NewSaleInitiated();
  }
}

Antes de pasar a la siguiente prueba, quitar duplicación en el código de prueba (consulte la La figura 3 ).

La figura 3 la actualización de las pruebas

[TestFixture]
public class CommandParserTests {
  private Mockery mockery;
  private CommandParser commandParser;
  private ISaleEventListener saleEventListener;

  [SetUp]
  public void BeforeTest() {
    mockery = new Mockery();
    saleEventListener = mockery.NewMock<ISaleEventListener>();
    commandParser = new CommandParser(saleEventListener);
    mockery = new Mockery();
  }

  [TearDown]
  public void AfterTest() {
    mockery.VerifyAllExpectationsHaveBeenMet();
  }

  [Test]
  public void NotifiesListenerOfNewSaleEvent() {
    var newSaleCommand = "Command:NewSale";

    Expect.Once.On(saleEventListener).Method("NewSaleInitiated");

    commandParser.Parse(newSaleCommand);
  }

  [Test]
  public void NotifiesListenerOfSaleCompletedEvent() {
    var endSaleCommand = "Command:EndSale";

    Expect.Once.On(saleEventListener).Method("SaleCompleted");

    commandParser.Parse(endSaleCommand);
  }
}

A continuación, desea garantizar que la CommandParser puede procesar un comando de entrada con información de código de barras. La aplicación recibe un mensaje sin procesar en el siguiente formato:

Input:Barcode=100008888559, Quantity =1

Desea saber el objeto que desempeña la función de un SaleEventListener que un elemento con un código de barras y se especifica la cantidad:

[Test]
public void NotifiesListenerOfItemAndQuantityEntered() {
  var message = "Input: Barcode=100008888559, Quantity =1";

  Expect.Once.On(saleEventListener).Method("ItemEntered")
    .With("100008888559", 1);

  commandParser.Parse(message);
}

La prueba de unidades sin la necesidad de todavía otro método que debe proporcionar la interfaz ISaleEventListener:

public interface ISaleEventListener {
  void NewSaleInitiated();
  void SaleCompleted();
  void ItemEntered(string barcode, int quantity);
}

Ejecutar la prueba, produce este error:

TestCase 'Domain.Tests.CommandParserTests.NotifiesListenerOfItemAndQuantityEntered'
failed: NMock2.Internal.ExpectationException : unexpected invocation of saleEventListener.NewSaleInitiated()
Expected:
  1 time: saleEventListener.ItemEntered(equal to "100008888559", equal to <1>) [called 0 times]

El mensaje de error indica que se llama al método incorrecto en el saleEventListener. Esto ocurrirá tal como aún no está implementado cualquier lógica en CommandParser para controlar los mensajes entrados que contiene código de barras y cantidad. la figura 4 muestra el CommandParser actualizada.

Mensajes de entrada de control de la figura 4

public class CommandParser {
  private const string END_SALE_COMMAND = "EndSale";
  private readonly ISaleEventListener saleEventListener;
  private const string INPUT = "Input";
  private const string START_SALE_COMMAND = "NewSale";

  public CommandParser(ISaleEventListener saleEventListener) {
    this.saleEventListener = saleEventListener;
  }

  public void Parse(string messageFromDevice) {
    var command = messageFromDevice.Split(':');
    var commandType = command[0].Trim();
    var commandBody = command[1].Trim();

    if(INPUT.Equals(commandType)) {
      ProcessInputCommand(commandBody);
    }
    else {
      ProcessCommand(commandBody);
    }
  }

  private void ProcessCommand(string commandBody) {
    if (END_SALE_COMMAND.Equals(commandBody))
      saleEventListener.SaleCompleted();
    else if (START_SALE_COMMAND.Equals(commandBody)) 
      saleEventListener.NewSaleInitiated();
  }

  private void ProcessInputCommand(string commandBody) {
    var arguments = new Dictionary<string, string>();
    var commandArgs = commandBody.Split(',');

    foreach(var argument in commandArgs) {
      var argNameValues = argument.Split('=');
      arguments.Add(argNameValues[0].Trim(), 
        argNameValues[1].Trim());
    }

    saleEventListener.ItemEntered(arguments["Barcode"], 
      int.Parse(arguments["Quantity"]));
  }
}

Través de la extracción de las conversaciones

Antes de pasar a la siguiente prueba, es necesario refactorizar y tidy hasta las interacciones entre CommandParser y saleEventListener. Deseo especificar interacciones entre un objeto y sus colaboradores en función del dominio de aplicación. El mensaje ItemEntered tiene en dos argumentos de una cadena que representa un código de barras y un entero que representa la cantidad. ¿Qué estos dos argumentos representan realmente en el dominio de la aplicación?

Regla general: si está pasando por tipos de datos primitivos entre los colaboradores de objeto, puede indicar que no se comunican en el nivel correcto de abstracción. Necesita ver si los tipos de datos primitivos representan conceptos en el dominio que es posible que haya perdidas.

En este caso, el código de barras se descomponerse en manufactureCode y itemCode, que representa un identificador del elemento. Puede presentar el concepto de un identificador del elemento en el código. Esto debe ser un tipo inmutable que puede crearse a partir de un código de barras, y puede dar el ItemIdentifier la responsabilidad de descomponer el código de barras en un código de fabricante y un código de artículo. De forma similar, cantidad debe ser un objeto de valor tal como representa una medida, por ejemplo, se puede medir la cantidad de un artículo por peso.

Por ahora, no tengo una necesidad aún descomponer el código de barras o controlar distintos tipos de mediciones de cantidad. Simplemente introducirá estos objetos de valor para garantizar que la comunicación entre objetos permanece en términos de dominio. Refactorizar el código para incluir el concepto de identificador del elemento y la cantidad en la prueba.

[Test]
public void NotifiesListenerOfItemAndQuantityEntered() {
  var message = "Input: Barcode=100008888559, Quantity=1";
  var expectedItemid = new ItemId("100008888559");
  var expectedQuantity = new Quantity(1);

  Expect.Once.On(saleEventListener).Method("ItemEntered").With(
    expectedItemid, expectedQuantity);

  commandParser.Parse(message);
}

Ni ItemId cantidad existe todavía. Para obtener el que se pase la prueba, es necesario crear estas nuevas clases y modificar el código para reflejar estos conceptos nuevos. Estos tipos implementar como objetos de valor. Las identidades de estos objetos se basan en los valores que contienen (consulte la figura 5 ).

La figura 5 ItemID y cantidad

public interface ISaleEventListener {
  void SaleCompleted();
  void NewSaleInitiated();
  void ItemEntered(ItemId itemId, Quantity quantity);
}

public class ItemId {
  private readonly string barcode;

  public ItemId(string barcode) {
    this.barcode = barcode;
  }

  public override bool Equals(object obj) {
    var other = obj as ItemId;
    if(other == null) return false;
    return this.barcode == other.barcode;
  }

  public override int GetHashCode() {
    return barcode.GetHashCode();
  }

  public override string ToString() {
    return barcode;
  }
}

public class Quantity {
  private readonly int value;

  public Quantity(int qty) {
    this.value = qty;
  }

  public override string ToString() {
    return value.ToString();
  }

  public override bool Equals(object obj) {
    var otherQty = obj as Quantity;
    if(otherQty == null) return false;
    return value == otherQty.value;
  }

  public override int GetHashCode() {
    return value.GetHashCode();
  }
}

Con pruebas de en función de interacción mediante objetos de simulacro, puede simplificar las interacciones entre un objeto y sus colaboradores al utilizar las pruebas para describir explícitamente los protocolos de comunicación entre objetos en un alto nivel de abstracción en términos del dominio de. Ya que los simulacros permiten que hacerlo sin tener ningún implementaciones concretas de los colaboradores existe, puede probar patrones de colaboración alternativa hasta que haya diseñado la colaboración entre objetos del dominio de aplicación en términos de. También examina y estrechamente siguiendo las interacciones entre objeto y su collaborator, también ayuda a cavar fuera de los conceptos de dominio es posible que haya pasado por alto.

Calcular recibos

Ahora que tiene un objeto para descodificar los comandos de dispositivos de entrada como punto de venta de eventos, necesita un objeto que puede responder y procesar estos eventos. El requisito es imprimir sin recibos, por lo que necesita un objeto que se puede calcular recibos. Para rellenar estas responsabilidades, busque un objeto que puede reproducir la función de un SaleEventListener. El concepto de un registro trata de cuenta y parece ajustar la función de un SaleEventListener, por lo que crear una nueva clase de registro. Puesto que esta clase responde a eventos de venta, hacerlo implementar ISaleEventListener:

public class Register : ISaleEventListener {
  public void SaleCompleted() { }
  public void NewSaleInitiated() { }
  public void ItemEntered(ItemId itemId, Quantity quantity) { }
}

Uno de las responsabilidades principales del registro es calcular recibos y enviarlos a una impresora. Algunos eventos en el objeto se plataforma invocando sus métodos. Empiezo con el escenario sencillo. Calcular el recibo de una venta sin elementos debe tener un total de 0. ¿SE necesita para formular la pregunta: que se sabe si el registro calcula el total de los elementos correctamente? Mi primera estimar es una impresora de recibo. Expresa en el código escribiendo una prueba:

[Test]
public void ReceiptTotalForASaleWithNoItemsShouldBeZero() {
  var receiptPrinter = mockery.NewMock<IReceiptPrinter>();
  var register = new Register();
  register.NewSaleInitiated();

  Expect.Once.On(receiptPrinter).Method("PrintTotalDue").With(0.00);

  register.SaleCompleted();
}  

La prueba implica que el objeto de registro indica a la impresora de recibo para imprimir el total vencido.

Antes de avance vamos a llevar un paso realizar de seguridad y examine el protocolo de comunicación entre el objeto de registro y la impresora de recibo. ¿Es el método PrintTotalDue significativo para el objeto de registro? Examina esta interacción, claramente no es responsable del registro se preocupe por los recibos de impresión. El objeto de registro se ocupa de calcular el albarán de compra y enviarlo a un objeto que recibe los recibos. Se elija un nombre para el método que se describe sólo ese comportamiento: ReceiveTotalDue. Esto es mucho más significativa en el objeto de registro. En si lo hace esto que descubrí que requiere la función de colaboración el registro es un ReceiptReceiver en lugar de un ReceiptPrinter. Buscar el nombre adecuado para una función es una parte importante de la actividad de diseño, como ayuda a diseñar los objetos que tienen un conjunto de responsabilidades uniformes. Vuelva a escribir la prueba para reflejar el nuevo nombre para la función.

[Test]
public void ReceiptTotalForASaleWithNoItemsShouldBeZero() {
  var receiptReceiver = mockery.NewMock<IReceiptReceiver>();
  var register = new Register();
  register.NewSaleInitiated();

  Expect.Once.On(receiptReceiver).Method("ReceiveTotalDue").With(0.00);

  register.SaleCompleted();
}  

Para obtener esta opción para compilar, crear una interfaz IReceiptReceiver para representar la función ReceiptReceiver.

public interface IReceiptReceiver {
  void ReceiveTotalDue(decimal amount);
}

Al ejecutar la prueba aparece un error, como se esperaba. El marco de simulacro indicando ReceiveTotalDue nunca se realizó la llamada de método. Para realizar la prueba pasar necesita pasar una implementación de IReceiptReceiver simulacro en el objeto de registro, para que cambie la prueba para reflejar esta dependencia.

[Test]
public void ReceiptTotalForASaleWithNoItemsShouldBeZero() {
  var receiptReceiver = mockery.NewMock<IReceiptReceiver>();
  var register = new Register(receiptReceiver);
  register.NewSaleInitiated();

  Expect.Once.On(receiptReceiver).Method("ReceiveTotalDue").With(0.00m);

  register.SaleCompleted();
} 

La implementación simple aquí debe obtener que se pase la prueba:

public class Register : ISaleEventListener {
  private readonly IReceiptReceiver receiptReceiver;

  public Register(IReceiptReceiver receiver) {
    this.receiptReceiver = receiver;
  }

  public void SaleCompleted() {
    receiptReceiver.ReceiveTotalDue(0.00m);
  }

  public void NewSaleInitiated() { }

  public void ItemEntered(ItemId itemId, Quantity quantity) { }
}

Los decimales de tipo primitivo utilizados para representar el monto total vencido es simplemente un escalar, que no tiene ningún significado en el dominio. Lo que esto realmente representa es valores monetarios, por lo que creará un objeto de valor inmutable para representar el dinero. En este momento, no es necesario para multi-currency o redondeo de los valores monetarios. Crear simplemente una clase de dinero que se ajusta el valor decimal. Cuando surge la necesidad, pueden agregar moneda y reglas de esta clase de redondeo. Por ahora, se permanecer centrado en la tarea actual y modificar el código para reflejarlo. La implementación se muestra en la figura 6 .

Figura 6 con dinero

public interface IReceiptReceiver {
  void ReceiveTotalDue(Money amount);
}

public class Register : ISaleEventListener {
  private readonly IReceiptReceiver receiptReceiver;

  public Register(IReceiptReceiver receiver) {
    this.receiptReceiver = receiver;
  }

  public void SaleCompleted() {
    receiptReceiver.ReceiveTotalDue(new Money(0.00m));
  }

  public void NewSaleInitiated() { }

  public void ItemEntered(ItemId itemId, Quantity quantity) { }
}
[Test]
public void ReceiptTotalForASaleWithNoItemsShouldBeZero() {
  var receiptReceiver = mockery.NewMock<IReceiptReceiver>();
  var register = new Register(receiptReceiver);
  register.NewSaleInitiated();

  var totalDue = new Money(0m);
  Expect.Once.On(receiptReceiver).Method("ReceiveTotalDue")
    .With(totalDue);
  register.SaleCompleted();
}  

La siguiente prueba se desarrolle sin algún comportamiento adicional en el objeto de registro. El registro no debe calcular recibos si nuevas ventas no están iniciados, por lo que escribir una prueba para especificar este comportamiento:

[SetUp]
public void BeforeTest() {
  mockery = new Mockery();
  receiptReceiver = mockery.NewMock<IReceiptReceiver>();
  register = new Register(this.receiptReceiver);
}

[Test]
public void ShouldNotCalculateRecieptWhenThereIsNoSale() {
  Expect.Never.On(receiptReceiver);
  register.SaleCompleted();
}

Esta prueba explícitamente especifica que la receiptReceiver nunca debe recibir ninguna llamada de método en él. La prueba falla como se esperaba con el siguiente error:

TestCase 'Domain.Tests.RegisterTests.  ShouldNotCalculateRecieptWhenThereIsNoSale'
failed: NMock2.Internal.ExpectationException : unexpected invocation of   receiptReceiver.ReceiveTotalDue(<0.00>)

Para obtener que se pase esta prueba, el objeto de registro tiene que realizar un seguimiento de algún estado, para realizar un seguimiento de si hay una venta en curso. Puede realizar la prueba pasar con la implementación que se muestra en la figura 7 .

La figura 7 mantener realizar el seguimiento del estado de registro

public class Register : ISaleEventListener {
  private readonly IReceiptReceiver receiptReceiver;
  private bool hasASaleInprogress;

  public Register(IReceiptReceiver receiver) {
    this.receiptReceiver = receiver;
  }

  public void SaleCompleted() {
    if(hasASaleInprogress)
      receiptReceiver.ReceiveTotalDue(new Money(0.00m));
  }

  public void NewSaleInitiated() {
    hasASaleInprogress = true;
  }

  public void ItemEntered(ItemId itemId, Quantity quantity) { }
}

Obtener la descripción del producto

Los sistemas de información de productos se encuentran en la oficina principal, que se expone como un servicio RESTful. El objeto de registro tendrá que recuperar información del producto de este sistema para averiguar el recibo de una venta. No quiero estar restringido por los detalles de implementación de este sistema externo; por lo que mi propia interfaz se definen en términos de dominio para los servicios que necesita el punto del sistema de venta.

Escribirá una prueba para calcular el recibo de una venta con un par de elementos. Para averiguar el total de una venta, el registro debe colaborar con otro objeto. Para recuperar [una] descripción de producto para un artículo, presento la función de un catálogo de productos. Esto podría ser un servicio RESTful, una base de datos o algún otro sistema. Los detalles de implementación no es importante ni de cualquier problema en el objeto de registro. Desea diseñar una interfaz que sea significativa para el objeto de registro. La prueba se muestra en la figura 8 .

Registro de pruebas de la figura 8

[TestFixture]
public class RegisterTests {
  private Mockery mockery;
  private IReceiptReceiver receiptReceiver;
  private Register register;
  private readonly ItemId itemId_1 = new ItemId("000000001");
  private readonly ItemId itemId_2 = new ItemId("000000002");
  private readonly 
    ProductDescription descriptionForItemWithId1 = 
    new ProductDescription("description 1", new Money(3.00m));

  private readonly 
    ProductDescription descriptionForItemWithId2 = 
    new ProductDescription("description 2", new Money(7.00m));
  private readonly Quantity single_item = new Quantity(1);
  private IProductCatalog productCatalog;

  [SetUp]
  public void BeforeTest() {
    mockery = new Mockery();
    receiptReceiver = mockery.NewMock<IReceiptReceiver>();
    productCatalog = mockery.NewMock<IProductCatalog>();
    register = new Register(receiptReceiver, productCatalog);

    Stub.On(productCatalog).Method("ProductDescriptionFor")
      .With(itemId_1)
      .Will(Return.Value(descriptionForItemWithId1));

    Stub.On(productCatalog).Method("ProductDescriptionFor")
      .With(itemId_2)
      .Will(Return.Value(descriptionForItemWithId2));

  }

  [TearDown]
  public void AfterTest() {
    mockery.VerifyAllExpectationsHaveBeenMet();
  }
...
  [Test]
  public void 
    ShouldCalculateRecieptForSaleWithMultipleItemsOfSingleQuantity() {

    register.NewSaleInitiated();
    register.ItemEntered(itemId_1, single_item);
    register.ItemEntered(itemId_2, single_item);

    Expect.Once.On(receiptReceiver).Method("ReceiveTotalDue")
      .With(new Money(10.00m));

   register.SaleCompleted();
  }
}

Esta prueba se ha diseñado la interfaz para la productCatalog requerido por el objeto de registro. También detectó la necesidad de un tipo nuevo, productDescription, que representa la descripción de un producto. Se modelo como un objeto de valor (tipo inmutable). Código auxiliar del productCatalog para dar un productDescription cuando se consulta con un ItemIdentifier. Código auxiliar la invocación de un método ProductDescriptionFor en productCatalog ya que es un método de consulta que devuelva el productDescription. El registro actúa en el resultado de la consulta devuelve. ¿Qué es importante aquí es que el registro genera los efectos secundarios correctos cuando la ProductCatalog devuelve un ProductDescription especificado. El resto de la prueba comprueba que el total correcto se calcule correctamente y se envía al receptor de recibo.

Ejecute la prueba para obtener el error esperado:

unexpected invocation of receiptReceiver.ReceiveTotalDue(<0.00>)
Expected:
  Stub: productCatalog.ProductDescriptionFor(equal to <000000001>), will
       return <Domain.Tests.ProductDescription> [called 0 times]
  Stub: productCatalog.ProductDescriptionFor(equal to <000000002>), will 
      return <Domain.Tests.ProductDescription> [called 0 times]
  1 time: receiptReceiver.ReceiveTotalDue(equal to <10.00>) [called 0 times]

El marco de simulacro indicando que debe receiptReceiver ha recibido un total de 10, pero se ha recibido un 0. Esto ocurrirá puesto que aún no hemos implementado todo lo que puede calcular el total. la figura 9 muestra un primer intento en la implementación para obtener el que se pase la prueba.

Figura 9 calcular el total

public class Register : ISaleEventListener {
  private readonly IReceiptReceiver receiptReceiver;
  private readonly IProductCatalog productCatalog;
  private bool hasASaleInprogress;
  private List<ProductDescription> purchasedProducts = 
    new List<ProductDescription>();

  public Register(IReceiptReceiver receiver, 
    IProductCatalog productCatalog) {

    this.receiptReceiver = receiver;
    this.productCatalog = productCatalog;
  }

  public void SaleCompleted() {
    if(hasASaleInprogress) {
      Money total = new Money(0m);
      purchasedProducts.ForEach(item => total += item.UnitPrice);
      receiptReceiver.ReceiveTotalDue(total);
    }
  }

  public void NewSaleInitiated() {
    hasASaleInprogress = true;
  }

  public void ItemEntered(ItemId itemId, Quantity quantity) {
    var productDescription = productCatalog.ProductDescriptionFor(itemId);
    purchasedProducts.Add(productDescription);
  }
}

Este código de error al compilar puesto que la clase dinero no define una operación para agregar el dinero. Ahora tiene una necesidad de objetos de dinero para realizar la adición, por lo que escribir una prueba rápida a de la clase de dinero para controlar este requisito inmediato.

[TestFixture]
public class MoneyTest {

  [Test]
  public void ShouldBeAbleToCreateTheSumOfTwoAmounts() {
    var twoDollars = new Money(2.00m);
    var threeDollars = new Money(3m);
    var fiveDollars = new Money(5m);
    Assert.That(twoDollars + threeDollars, Is.EqualTo(fiveDollars));
  }
}

La implementación para obtener esta prueba pasando se muestra en Figura 10 .

Figura 10 actualizaciones para agregar Money

public class Money {
  private readonly decimal amount;

  public Money(decimal value) {
    this.amount = value;
  }

  public static Money operator +(Money money1, Money money2) {
    return new Money(money1.amount + money2.amount);
  }

  public override string ToString() {
    return amount.ToString();
  }

  public override bool Equals(object obj) {
    var otherAmount = obj as Money;
    if(otherAmount == null) return false;
    return amount == otherAmount.amount;
  }

  public override int GetHashCode() {
    return amount.GetHashCode();
  }
}

Refactorización

Antes de pasar cualquier aún más, es necesario aclarar el propósito de mi código. Mis pruebas no seguimiento cualquiera de los detalles internos del objeto de registro. Todo esto está oculta en el objeto de registro, que permite refactorizar y aclarar el propósito del código.

El objeto de registro está administrando estado relacionados con la venta actual, pero no tiene un objeto que representa el concepto de una venta. Decide extraer una clase de venta para administrar todo el estado y comportamiento relacionado con una venta. El código refactorizado se muestra en la figura 11 .

Figura 11 agregar una clase de venta

public class Register : ISaleEventListener {
  private readonly IReceiptReceiver receiptReceiver;
  private readonly IProductCatalog productCatalog;
  private Sale sale;

  public Register(IReceiptReceiver receiver, 
    IProductCatalog productCatalog) {

    this.receiptReceiver = receiver;
    this.productCatalog = productCatalog;
  }

  public void SaleCompleted() {
    if(sale != null) {
      sale.SendReceiptTo(receiptReceiver);
    }
  }

  public void NewSaleInitiated() {
    sale = new Sale();
  }

  public void ItemEntered(ItemId itemId, Quantity quantity) {
    var productDescription = 
      productCatalog.ProductDescriptionFor(itemId);
    sale.PurchaseItemWith(productDescription);
  }
}

public class Sale {
  private readonly List<ProductDescription> itemPurchased = 
    new List<ProductDescription>();

  public void SendReceiptTo(IReceiptReceiver receiptReceiver) {
    var total = new Money(0m);
    itemPurchased.ForEach(item => total += item.UnitPrice);
    receiptReceiver.ReceiveTotalDue(total);
  }

  public void PurchaseItemWith(ProductDescription description) {
    itemPurchased.Add(description);
  }
}

Objetos de valor

tipos de valor en .NET terminología, consulte los tipos primitivos admitidos por el CLR como int, bool, estructuras, enum, etc.. Esto no debe confundirse con objetos de valor; éstos son objetos que describen las cosas. Lo importante a tener en cuenta es que los objetos de valor pueden implementar mediante las clases (tipos de referencia); estos objetos son inmutables y no tienen ninguna identidad conceptual. Puesto que los objetos de valor no tienen ninguna identidad individual, objetos de dos valores se consideran iguales si tienen el mismo estado.

Ajustar hacia arriba

En este ejemplo, se le mostramos cómo simulacro objetos y TDD puede guiar el diseño de programas orientado a objetos. Hay una serie de ventajas de parte importante de este proceso de descubrimiento iterativa. Ayudó mediante objetos de simulacro no sólo a mí descubrir los colaboradores y necesidades de objeto, pero también era capaz de describir y hacer que explícito en mis pruebas el debe admitir el entorno de un objeto de funciones y patrones de comunicación entre el objeto en el que se realiza la prueba y sus colaboradores.

Tenga en cuenta que las pruebas muestran sólo lo hace el objeto en términos de conceptos de dominio. Las pruebas no especificar cómo en que el registro interactúa con una venta. Son internos detalles acerca de cómo está estructurado del objeto de registro para realizar su trabajo, y estos detalles de implementación permanecen ocultos. Eligió simulacros sólo para especificar explícitamente las interacciones que deben estar visibles externamente entre un objeto y sus colaboradores en el sistema.

Las pruebas mediante objetos de simulacro disponer de un conjunto de restricciones que especifica lo que pueden enviar mensajes de objetos y se deben enviar los mensajes. Las interacciones entre los objetos se describen claramente en términos de conceptos de dominio. El dominio consta de las interfaces de función estrecha que hacer que el sistema conectable y y el comportamiento del sistema se puede modificar fácilmente cambiando la composición de sus objetos.

Ninguno de los objetos exponer su estado interno o estructura, y se pasan estados sólo inmutables (objetos de valor como dinero y itemId) alrededor. Esto facilita programas mantener y modificar. Algo a tener en cuenta es que normalmente se ha comenzado aún el desarrollo con una aceptación con errores, que se han controlada los requisitos y la necesidad de la CommandParser en mi ejemplo.

Isaiah Perumalla un desarrollador senior en ThoughtWorks, donde él está involucrado en entrega de software de escala de empresa, utiliza diseño orientado a objetos y desarrollo de Test-Driven.