No me considero un experto en DDD pero, como arquitecto de soluciones, intento aplicar las mejores prácticas siempre que sea posible. Sé que hay mucha discusión sobre los pros y los contras del "estilo" de setter no (público) en DDD y puedo ver ambos lados del argumento. Mi problema es que trabajo en un equipo con una amplia diversidad de habilidades, conocimientos y experiencia, lo que significa que no puedo confiar en que cada desarrollador haga las cosas de la manera "correcta". Por ejemplo, si nuestros objetos de dominio están diseñados para que los cambios en el estado interno del objeto se realicen mediante un método pero proporcionen establecedores de propiedad pública, alguien inevitablemente establecerá la propiedad en lugar de llamar al método. Usa este ejemplo:
public class MyClass
{
public Boolean IsPublished
{
get { return PublishDate != null; }
}
public DateTime? PublishDate { get; set; }
public void Publish()
{
if (IsPublished)
throw new InvalidOperationException("Already published.");
PublishDate = DateTime.Today;
Raise(new PublishedEvent());
}
}
Mi solución ha sido hacer que los establecedores de propiedades sean privados, lo cual es posible porque el ORM que estamos usando para hidratar los objetos usa la reflexión para que pueda acceder a los establecedores privados. Sin embargo, esto presenta un problema al intentar escribir pruebas unitarias. Por ejemplo, cuando quiero escribir una prueba unitaria que verifique el requisito de que no podemos volver a publicar, debo indicar que el objeto ya ha sido publicado. Ciertamente puedo hacer esto llamando a Publicar dos veces, pero luego mi prueba asume que Publicar se implementa correctamente para la primera llamada. Eso parece un poco maloliente.
Hagamos el escenario un poco más real con el siguiente código:
public class Document
{
public Document(String title)
{
if (String.IsNullOrWhiteSpace(title))
throw new ArgumentException("title");
Title = title;
}
public String ApprovedBy { get; private set; }
public DateTime? ApprovedOn { get; private set; }
public Boolean IsApproved { get; private set; }
public Boolean IsPublished { get; private set; }
public String PublishedBy { get; private set; }
public DateTime? PublishedOn { get; private set; }
public String Title { get; private set; }
public void Approve(String by)
{
if (IsApproved)
throw new InvalidOperationException("Already approved.");
ApprovedBy = by;
ApprovedOn = DateTime.Today;
IsApproved = true;
Raise(new ApprovedEvent(Title));
}
public void Publish(String by)
{
if (IsPublished)
throw new InvalidOperationException("Already published.");
if (!IsApproved)
throw new InvalidOperationException("Cannot publish until approved.");
PublishedBy = by;
PublishedOn = DateTime.Today;
IsPublished = true;
Raise(new PublishedEvent(Title));
}
}
Quiero escribir pruebas unitarias que verifiquen:
- No puedo publicar a menos que el documento haya sido aprobado
- No puedo volver a publicar un documento
- Cuando se publican, los valores PublicadosPor y Publicados están establecidos correctamente
- Cuando se publica, se publica el Evento publicado
Sin acceso a los setters, no puedo poner el objeto en el estado necesario para realizar las pruebas. Abrir el acceso a los setters anula el propósito de evitar el acceso.
¿Cómo (tiene) que resuelve (d) este problema?
fuente
Respuestas:
Si no puede poner el objeto en el estado necesario para realizar una prueba, entonces no puede poner el objeto en el estado en el código de producción, por lo que no hay necesidad de probar ese estado. Obviamente, esto no es cierto en su caso, puede poner su objeto en el estado necesario, solo llame a Aprobar.
No puedo publicar a menos que el documento haya sido aprobado: escriba una prueba de que llamar a publicar antes de llamar a aprobar causa el error correcto sin cambiar el estado del objeto.
No puedo volver a publicar un documento: escriba una prueba que apruebe un objeto, luego llame a publicar una vez que tenga éxito, pero la segunda vez causa el error correcto sin cambiar el estado del objeto.
Cuando se publican, los valores PublicadosPor y Publicados están establecidos correctamente: escriba una prueba que llame a aprobar y luego llame a publicar, afirme que el estado del objeto cambia correctamente
Cuando se publica, se publica el Evento publicado: conéctese al sistema de eventos y establezca un indicador para asegurarse de que se llame
También necesita escribir una prueba para aprobar.
En otras palabras, no pruebe la relación entre los campos internos y IsPublished e IsApproved, su prueba sería bastante frágil si lo hace, ya que cambiar su campo significaría cambiar su código de prueba, por lo que la prueba sería bastante inútil. En su lugar, debe probar la relación entre llamadas de métodos públicos, de esta manera, incluso si modifica los campos, no necesitaría modificar la prueba.
fuente
setup()
método, no en la prueba misma.approve()
alguna manera quebradizo, pero depende desetApproved(true)
alguna manera no?approve()
es una dependencia legítima en las pruebas porque es una dependencia en los requisitos. Si la dependencia solo existiera en las pruebas, ese sería otro problema.push()
y de formapop()
independiente?Otro enfoque es crear un constructor de la clase que permita establecer las propiedades internas en la instanciación:
fuente
Una estrategia es que herede la clase (en este caso, Documento) y escriba pruebas contra la clase heredada. La clase heredada permite alguna forma de establecer el estado del objeto en las pruebas.
En C # una estrategia podría ser hacer que los setters sean internos y luego exponer los elementos internos para probar el proyecto.
También puede usar la API de clase como la describió ("Ciertamente puedo hacer esto llamando a Publicar dos veces"). Esto sería establecer el estado del objeto utilizando los servicios públicos del objeto, no me parece demasiado maloliente. En el caso de su ejemplo, esta sería probablemente la forma en que lo haría.
fuente
Para probar en aislamiento absoluto los comandos y consultas que reciben los objetos de dominio, estoy acostumbrado a proporcionar a cada prueba una serialización del objeto en el estado esperado. En la sección de organización de la prueba, carga el objeto a probar desde un archivo que he preparado previamente. Al principio comencé con las serializaciones binarias, pero json ha demostrado ser mucho más fácil de mantener. Esto demostró funcionar bien, siempre que el aislamiento absoluto en las pruebas proporcione un valor real.
edite solo una nota, algunas veces la serialización JSON falla (como en el caso de los gráficos de objetos cíclicos, eso es un olor, por cierto). En tales situaciones, rescato a la serialización binaria. Es un poco pragmático, pero funciona. :-)
fuente
Tu dices
y
y tengo que pensar que usar la reflexión para evitar los controles de acceso en sus clases no es lo que describiría como "mejor práctica". También va a ser terriblemente lento.
Personalmente, descartaría su marco de prueba de unidad e iría con algo en la clase: parece que está escribiendo pruebas desde el punto de vista de probar toda la clase de todos modos, lo cual es bueno. En el pasado, para algunos componentes difíciles que necesitaban pruebas, he incrustado las afirmaciones y el código de configuración en la clase misma (solía ser un patrón de diseño común para tener un método test () en cada clase), por lo que crea un cliente eso simplemente crea una instancia de un objeto y llama al método de prueba, que puede configurarse como desee sin asperezas como hacks de reflexión.
Si le preocupa la acumulación de código, simplemente ajuste los métodos de prueba en #ifdefs para que solo estén disponibles en el código de depuración (probablemente una práctica recomendada en sí misma)
fuente