Prueba unitaria para probar la creación de un objeto de dominio

11

Tengo una prueba de unidad, que se ve así:

[Test]
public void Should_create_person()
{
     Assert.DoesNotThrow(() => new Person(Guid.NewGuid(), new DateTime(1972, 01, 01));
}

Estoy afirmando que un objeto Persona se crea aquí, es decir, que la validación no falla. Por ejemplo, si el Guid es nulo o la fecha de nacimiento es anterior al 01/01/1900, entonces la validación fallará y se lanzará una excepción (lo que significa que la prueba falla).

El constructor se ve así:

public Person(Id id, DateTime dateOfBirth) :
        base(id)
    {
        if (dateOfBirth == null)
            throw new ArgumentNullException("Date of Birth");
        elseif (dateOfBith < new DateTime(1900,01,01)
            throw new ArgumentException("Date of Birth");
        DateOfBirth = dateOfBirth;
    }

¿Es esta una buena idea para una prueba?

Nota : Estoy siguiendo un enfoque clasicista para la Unidad de Prueba del Modelo de Dominio si eso tiene alguna relación.

w0051977
fuente
¿El constructor tiene alguna lógica que valga la pena afirmar después de la inicialización?
Laiv
2
¡Nunca te molestes en probar a los constructores! La construcción debe ser sencilla. ¿Espera fallas en Guid.NewGuid () o el constructor de DateTime?
ivenxu
@Laiv, consulte la actualización de la pregunta.
w0051977
1
No vale nada implementar una prueba como la que compartiste. Sin embargo, probaría también lo contrario. Probaría el caso donde birthDate causa un error. Esa es la invariante de la clase que quieres que esté bajo control y prueba.
Laiv
3
La prueba se ve bien, salvo por una cosa: el nombre. Should_create_person? ¿Qué debería crear una persona? Dale un nombre significativo, como Creating_person_with_valid_data_succeeds.
David Arno el

Respuestas:

18

Esta es una prueba válida (aunque bastante entusiasta) y a veces lo hago para probar la lógica del constructor, sin embargo, como mencionó Laiv en los comentarios, debe preguntarse por qué.

Si su constructor se ve así:

public Person(Guid guid, DateTime dob)
{
  this.Guid = guid;
  this.Dob = dob;
}

¿Tiene mucho sentido probar si arroja? Si los parámetros están asignados correctamente, puedo entenderlo, pero su prueba es bastante exagerada.

Sin embargo, si su prueba hace algo como esto:

public Person(Guid guid, DateTime dob)
{
  if(guid == default(Guid)) throw new ArgumentException("Guid is invalid");
  if(dob == default(DateTime)) throw new ArgumentException("Dob is invalid");

  this.Guid = guid;
  this.Dob = dob;
}

Luego, su prueba se vuelve más relevante (ya que en realidad está lanzando excepciones en algún lugar del código).

Una cosa que diría es que, en general, es una mala práctica tener mucha lógica en su constructor. La validación básica (como las comprobaciones nulas / predeterminadas que estoy haciendo arriba) están bien. Pero si se está conectando a bases de datos y cargando datos de alguien, entonces es donde el código comienza a oler realmente ...

Debido a esto, si vale la pena probar su constructor (porque hay mucha lógica en marcha), entonces tal vez algo más esté mal.

Es casi seguro que tendrá otras pruebas que cubren esta clase en capas de lógica de negocios, los constructores y las asignaciones variables seguramente obtendrán una cobertura completa de estas pruebas. Por lo tanto, quizás no tenga sentido agregar pruebas específicas específicamente para el constructor. Sin embargo, nada es en blanco y negro y no tendría nada en contra de estas pruebas si estuviera revisando el código, pero me preguntaría si agregan mucho valor por encima y más allá de las pruebas en otra parte de su solución.

En tu ejemplo:

public Person(Id id, DateTime dateOfBirth) :
        base(id)
    {
        if (dateOfBirth == null)
            throw new ArgumentNullException("Date of Birth");
        elseif (dateOfBith < new DateTime(1900,01,01)
            throw new ArgumentException("Date of Birth");
        DateOfBirth = dateOfBirth;
    }

No solo está validando, sino que también está llamando a un constructor base. Para mí, esto proporciona más razones para tener estas pruebas, ya que ahora tienen la lógica del constructor / validación dividida en dos clases, lo que disminuye la visibilidad y aumenta el riesgo de cambios inesperados.

TLDR

Estas pruebas tienen cierto valor, sin embargo, es probable que la lógica de validación / asignación esté cubierta por otras pruebas en su solución. Si hay mucha lógica en estos constructores que requiere pruebas significativas, entonces me sugiere que hay un desagradable olor a código acechando allí.

Liath
fuente
@Laith, consulte la actualización de mi pregunta
w0051977
Noté que estás llamando a un constructor base en tu ejemplo. En mi humilde opinión, esto agrega más valor a su prueba, la lógica del constructor ahora se divide en dos clases y, por lo tanto, es un riesgo ligeramente mayor de cambio, por lo que da más razones para probarla.
Liath
"Sin embargo, si su prueba hace algo como esto:" <¿No quiere decir "si su constructor hace algo como esto" ?
Kodos Johnson el
"Hay algo de valor en estas pruebas". Curiosamente para mí de todos modos, el valor muestra que podríamos hacer que esta prueba sea redundante mediante el uso de una nueva clase para representar el dob de la persona (por ejemplo PersonBirthdate) que realiza la validación de la fecha de nacimiento. Del mismo modo, la Guidverificación podría implementarse en la Idclase. Esto significa que ya no es necesario tener esa lógica de validación en el Personconstructor, ya que no es posible construir uno con datos no válidos, a excepción de las nullreferencias. Por supuesto, tienes que escribir pruebas para las otras dos clases :)
Stephen Byrne
12

Ya hay una buena respuesta aquí, pero creo que vale la pena mencionar una cosa adicional.

Cuando se hace TDD "por libro", primero se necesita escribir una prueba que llame al constructor, incluso antes de que el constructor se implemente. Esa prueba en realidad podría parecerse a la que presentó, incluso si no hubiera lógica de validación dentro de la implementación del constructor.

Tenga en cuenta también que para TDD, primero se debe escribir otra prueba como

  Assert.Throws<ArgumentException>(() => new Person(Guid.NewGuid(), 
        new DateTime(1572, 01, 01));

antes de agregar la verificación para DateTime(1900,01,01)al constructor.

En el contexto TDD, la prueba mostrada tiene mucho sentido.

Doc Brown
fuente
¡Buen ángulo que no había considerado!
Liath
1
Esto me demuestra por qué una forma tan rígida de TDD es una pérdida de tiempo: la prueba debe tener valor después de escribir el código, o simplemente está escribiendo cada línea de código dos veces, una como afirmación y otra como código. Yo diría que el constructor en sí no es una pieza lógica que necesita ser probada; la regla de negocio "las personas nacidas antes de 1900 no deben ser representables" es comprobable, y el constructor es donde se implementa esa regla, pero ¿cuándo la prueba de un constructor vacío alguna vez agregaría valor al proyecto?
IMSoP
¿Es realmente tdd por el libro? Crearía una instancia y llamaría a su método de inmediato en un código. Luego escribiría una prueba para ese método, y al hacerlo también tendría que crear una instancia para ese método, por lo que tanto el constructor como el método se cubrirán en esa prueba. A menos que en el constructor haya algo de lógica, pero esa parte está cubierta por Liath.
Rafał Łużyński
@ RafałŁużyński: TDD "por libro" se trata de escribir primero las pruebas . En realidad, significa escribir siempre una prueba fallida primero (no compilar cuenta como falla también). Entonces, primero escribe una prueba llamando al constructor incluso cuando no hay constructor . Luego intenta compilar (que falla), luego implementa un constructor vacío, compila, ejecuta la prueba, resultado = verde. Luego escribe la primera prueba fallida y la ejecuta: resultado = rojo, luego agrega la funcionalidad para hacer que la prueba sea "verde" nuevamente, y así sucesivamente.
Doc Brown
Por supuesto. No quise decir que primero escribo la implementación, luego la prueba. Simplemente escribo "uso" de ese código en un nivel superior, luego pruebo ese código, luego lo implemento. Estoy haciendo "Fuera de TDD" por lo general.
Rafał Łużyński