Mi dominio consta de muchas clases simples inmutables como esta:
public class Person
{
public string FullName { get; }
public string NameAtBirth { get; }
public string TaxId { get; }
public PhoneNumber PhoneNumber { get; }
public Address Address { get; }
public Person(
string fullName,
string nameAtBirth,
string taxId,
PhoneNumber phoneNumber,
Address address)
{
if (fullName == null)
throw new ArgumentNullException(nameof(fullName));
if (nameAtBirth == null)
throw new ArgumentNullException(nameof(nameAtBirth));
if (taxId == null)
throw new ArgumentNullException(nameof(taxId));
if (phoneNumber == null)
throw new ArgumentNullException(nameof(phoneNumber));
if (address == null)
throw new ArgumentNullException(nameof(address));
FullName = fullName;
NameAtBirth = nameAtBirth;
TaxId = taxId;
PhoneNumber = phoneNumber;
Address = address;
}
}
Escribir las verificaciones nulas y la inicialización de propiedades ya se está volviendo muy tedioso, pero actualmente escribo pruebas unitarias para cada una de estas clases para verificar que la validación de argumentos funciona correctamente y que todas las propiedades se inicializan. Esto se siente como un trabajo extremadamente aburrido con beneficios inconmensurables.
La solución real sería que C # admitiera inmutabilidad y tipos de referencia no anulables de forma nativa. Pero, ¿qué puedo hacer para mejorar la situación mientras tanto? ¿Vale la pena escribir todas estas pruebas? ¿Sería una buena idea escribir un generador de código para tales clases para evitar escribir pruebas para cada una de ellas?
Esto es lo que tengo ahora basado en las respuestas.
Podría simplificar las verificaciones nulas y la inicialización de propiedades para que se vea así:
FullName = fullName.ThrowIfNull(nameof(fullName));
NameAtBirth = nameAtBirth.ThrowIfNull(nameof(nameAtBirth));
TaxId = taxId.ThrowIfNull(nameof(taxId));
PhoneNumber = phoneNumber.ThrowIfNull(nameof(phoneNumber));
Address = address.ThrowIfNull(nameof(address));
Usando la siguiente implementación de Robert Harvey :
public static class ArgumentValidationExtensions
{
public static T ThrowIfNull<T>(this T o, string paramName) where T : class
{
if (o == null)
throw new ArgumentNullException(paramName);
return o;
}
}
Probar las comprobaciones nulas es fácil usando GuardClauseAssertion
from AutoFixture.Idioms
(gracias por la sugerencia, Esben Skov Pedersen ):
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var assertion = new GuardClauseAssertion(fixture);
assertion.Verify(typeof(Address).GetConstructors());
Esto podría comprimirse aún más:
typeof(Address).ShouldNotAcceptNullConstructorArguments();
Usando este método de extensión:
public static void ShouldNotAcceptNullConstructorArguments(this Type type)
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var assertion = new GuardClauseAssertion(fixture);
assertion.Verify(type.GetConstructors());
}
fuente
Respuestas:
Creé una plantilla t4 exactamente para este tipo de casos. Para evitar escribir mucho repetitivo para clases inmutables.
https://github.com/xaviergonz/T4Immutable T4Immutable es una plantilla T4 para aplicaciones C # .NET que genera código para clases inmutables.
Hablando específicamente de pruebas no nulas, si usa esto:
El constructor será este:
Dicho esto, si usa las anotaciones de JetBrains para la comprobación nula, también puede hacer esto:
Y el constructor será este:
También hay algunas características más que esta.
fuente
Puede obtener un poco de mejora con una refactorización simple que puede aliviar el problema de escribir todas esas cercas. Primero, necesita este método de extensión:
Entonces puedes escribir:
Devolver el parámetro original en el método de extensión crea una interfaz fluida, por lo que puede ampliar este concepto con otros métodos de extensión si lo desea, y encadenarlos todos juntos en su tarea.
Otras técnicas son más elegantes en concepto, pero progresivamente más complejas en ejecución, como decorar el parámetro con un
[NotNull]
atributo y usar Reflection de esta manera .Dicho esto, es posible que no necesite todas estas pruebas, a menos que su clase sea parte de una API pública.
fuente
null
el argumento del constructor, no obtendrá una prueba fallida.throw
exterior del constructor; realmente no estás transfiriendo el control dentro del constructor a otro lugar. Es solo un método de utilidad, y una forma de hacer las cosas con fluidez , si así lo desea.En el corto plazo, no hay mucho que pueda hacer sobre el tedio de escribir tales exámenes. Sin embargo, hay ayuda que viene con las expresiones de lanzamiento que se implementarán como parte de la próxima versión de C # (v7), probablemente en los próximos meses:
Puede experimentar con expresiones de lanzamiento a través de la aplicación web Try Roslyn .
fuente
Me sorprende que nadie haya mencionado NullGuard.Fody todavía. Está disponible a través de NuGet y esos cheques nulos en la IL durante el tiempo de compilación.
Entonces su código de constructor simplemente sería
y NullGuard agregará esas comprobaciones nulas para que usted las transforme en exactamente lo que escribió.
Sin embargo, tenga en cuenta que NullGuard es una opción de exclusión voluntaria , es decir, agregará esas comprobaciones nulas a cada método y argumento de constructor, getter y setter de propiedades e incluso comprobará los valores de retorno del método a menos que permita explícitamente un valor nulo con el
[AllowNull]
atributo.fuente
[NotNull]
atributo en lugar de no permitir que nulo sea el predeterminado.No, probablemente no. ¿Cuál es la probabilidad de que lo arruines? ¿Cuál es la probabilidad de que algunas semánticas cambien de debajo de ti? ¿Cuál es el impacto si alguien lo arruina?
Si pasa mucho tiempo haciendo pruebas para algo que rara vez se romperá, y es una solución trivial si lo hiciera ... tal vez no valga la pena.
¿Tal vez? Ese tipo de cosas podría hacerse fácilmente con la reflexión. Algo a considerar es la generación de código para el código real, por lo que no tiene N clases que puedan tener un error humano. Prevención de errores> detección de errores.
fuente
if...throw
lo que tendránThrowHelper.ArgumentNull(myArg, "myArg")
, de esa manera la lógica sigue ahí y no te molestan en la cobertura del código. Donde elArgumentNull
método hará elif...throw
.¿Vale la pena escribir todas estas pruebas?
No.
Porque estoy bastante seguro de que has probado esas propiedades a través de otras pruebas de lógica en las que se usan esas clases.
Por ejemplo, puede tener pruebas para la clase Factory que tengan una aserción basada en esas propiedades (instancia creada con la propiedad asignada correctamente
Name
, por ejemplo).Si esas clases están expuestas a la API pública que utiliza un tercero / usuario final (@EJoshua gracias por darse cuenta), entonces las pruebas de lo esperado
ArgumentNullException
pueden ser útiles.Mientras espera C # 7, puede usar el método de extensión
Para las pruebas, puede usar un método de prueba parametrizado que usa la reflexión para crear
null
referencias para cada parámetro y afirmar para la excepción esperada.fuente
Siempre puedes escribir un método como el siguiente:
Como mínimo, eso le ahorrará algo de escritura si tiene varios métodos que requieren este tipo de validación. Por supuesto, esta solución supone que ninguno de los parámetros de su método puede serlo
null
, pero puede modificarlo para cambiarlo si así lo desea.También puede extender esto para realizar otra validación específica de tipo. Por ejemplo, si tiene una regla de que las cadenas no pueden ser espacios en blanco o vacíos, puede agregar la siguiente condición:
El método GetParameterInfo al que me refiero básicamente hace la reflexión (para que no tenga que seguir escribiendo lo mismo una y otra vez, lo que sería una violación del principio DRY ):
fuente
No sugiero lanzar excepciones del constructor. El problema es que tendrá dificultades para probar esto, ya que tendrá que pasar parámetros válidos, incluso si son irrelevantes para su prueba.
Por ejemplo: si desea probar si el tercer parámetro arroja una excepción, debe pasar el primero y el segundo correctamente. Entonces su prueba ya no está aislada. cuando las restricciones del primer y segundo parámetro cambian, este caso de prueba fallará incluso si prueba el tercer parámetro.
Sugiero usar la API de validación de Java y externalizar el proceso de validación. Sugiero tener cuatro responsabilidades (clases) involucradas:
Un objeto de sugerencia con parámetros anotados de validación de Java con un método de validación que devuelve un conjunto de restricciones de restricción. Una ventaja es que puede pasar estos objetos sin que la afirmación sea válida. Puede retrasar la validación hasta que sea necesario sin intentar crear una instancia del objeto de dominio. El objeto de validación se puede usar en diferentes capas, ya que es un POJO sin conocimiento específico de la capa. Puede ser parte de tu API pública.
Una fábrica para el objeto de dominio que es responsable de crear objetos válidos. Usted pasa el objeto de sugerencia y la fábrica llamará a la validación y creará el objeto de dominio si todo está bien.
El objeto de dominio en sí mismo que debería ser en su mayoría inaccesible para la creación de instancias para otros desarrolladores. Para fines de prueba, sugiero tener esta clase en el alcance del paquete.
Una interfaz de dominio público para ocultar el objeto de dominio concreto y dificultar el mal uso.
No sugiero verificar los valores nulos. Tan pronto como ingrese a la capa de dominio, se librará de pasar nulo o devolver nulo siempre que no tenga cadenas de objetos que tengan extremos o funciones de búsqueda de objetos individuales.
Otro punto: escribir la menor cantidad de código posible no es una métrica para la calidad del código. Piensa en 32k competiciones de juegos. Ese código es el más compacto pero también el más desordenado que puede tener, ya que solo se preocupa por cuestiones técnicas y no se preocupa por la semántica. Pero la semántica son los puntos que hacen que las cosas sean integrales.
fuente