Mon domaine se compose de nombreuses classes immuables simples comme celle-ci:
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;
}
}
L'écriture des vérifications nulles et de l'initialisation des propriétés devient déjà très fastidieuse mais actuellement j'écris des tests unitaires pour chacune de ces classes pour vérifier que la validation des arguments fonctionne correctement et que toutes les propriétés sont initialisées. Cela ressemble à un travail extrêmement ennuyeux avec des avantages incommensurables.
La vraie solution serait que C # prenne en charge nativement les types de référence immuables et non nullables. Mais que puis-je faire pour améliorer la situation entre-temps? Vaut-il la peine d'écrire tous ces tests? Serait-ce une bonne idée d'écrire un générateur de code pour de telles classes pour éviter d'écrire des tests pour chacune d'elles?
Voici ce que j'ai maintenant basé sur les réponses.
Je pourrais simplifier les vérifications nulles et l'initialisation des propriétés pour ressembler à ceci:
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));
En utilisant l'implémentation suivante 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;
}
}
Test des contrôles null est facile à l' aide du GuardClauseAssertion
de AutoFixture.Idioms
(merci pour la suggestion, Esben Skov Pedersen ):
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var assertion = new GuardClauseAssertion(fixture);
assertion.Verify(typeof(Address).GetConstructors());
Cela pourrait être encore plus compressé:
typeof(Address).ShouldNotAcceptNullConstructorArguments();
En utilisant cette méthode d'extension:
public static void ShouldNotAcceptNullConstructorArguments(this Type type)
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var assertion = new GuardClauseAssertion(fixture);
assertion.Verify(type.GetConstructors());
}
la source
Réponses:
J'ai créé un modèle t4 exactement pour ce type de cas. Pour éviter d'écrire beaucoup de passe-partout pour les classes immuables.
https://github.com/xaviergonz/T4Immutable T4Immutable est un modèle T4 pour les applications C # .NET qui génère du code pour les classes immuables.
En parlant spécifiquement de tests non nuls, alors si vous utilisez ceci:
Le constructeur sera le suivant:
Cela dit, si vous utilisez les annotations JetBrains pour la vérification nulle, vous pouvez également le faire:
Et le constructeur sera le suivant:
Il y a aussi quelques fonctionnalités de plus que celle-ci.
la source
Vous pouvez obtenir un peu d'amélioration avec un simple refactoring qui peut atténuer le problème de l'écriture de toutes ces clôtures. Tout d'abord, vous avez besoin de cette méthode d'extension:
Vous pouvez alors écrire:
Le retour du paramètre d'origine dans la méthode d'extension crée une interface fluide, vous pouvez donc étendre ce concept avec d'autres méthodes d'extension si vous le souhaitez, et les chaîner tous ensemble dans votre affectation.
D'autres techniques sont plus élégantes dans leur concept, mais progressivement plus complexes dans leur exécution, comme la décoration du paramètre avec un
[NotNull]
attribut et l'utilisation de la réflexion comme celle-ci .Cela dit, vous n'aurez peut-être pas besoin de tous ces tests, sauf si votre classe fait partie d'une API accessible au public.
la source
null
l'argument du constructeur, vous n'obtiendrez pas de test échoué.throw
extérieur du constructeur; vous ne transférez pas vraiment le contrôle au sein du constructeur ailleurs. C'est juste une méthode utilitaire et un moyen de faire les choses couramment , si vous le souhaitez.À court terme, il n'y a pas grand-chose que vous puissiez faire au sujet de l'ennui de passer de tels tests. Cependant, il existe une aide à venir avec les expressions throw qui doivent être implémentées dans le cadre de la prochaine version de C # (v7), probablement au cours des prochains mois:
Vous pouvez expérimenter avec des expressions de projection via la webapp Try Roslyn .
la source
Je suis surpris que personne n'ait encore mentionné NullGuard.Fody . Il est disponible via NuGet et tissera automatiquement ces vérifications nulles dans l'IL pendant la compilation.
Donc, votre code constructeur serait simplement
et NullGuard ajoutera ces contrôles nuls pour que vous le transformiez exactement en ce que vous avez écrit.
Notez cependant que NullGuard est opt-out , c'est-à-dire qu'il ajoutera ces vérifications nulles à chaque méthode et argument constructeur, getter et setter de propriété et même vérifier les valeurs de retour de méthode sauf si vous autorisez explicitement une valeur nulle avec l'
[AllowNull]
attribut.la source
[NotNull]
attribut au lieu de ne pas autoriser null comme valeur par défaut.Non, probablement pas. Quelle est la probabilité que vous allez foirer ça? Quelle est la probabilité que certaines sémantiques changent sous vous? Quel est l'impact si quelqu'un le fait foirer?
Si vous passez beaucoup de temps à faire des tests pour quelque chose qui se cassera rarement, et c'est une solution triviale si c'était le cas ... peut-être que cela n'en vaut pas la peine.
Peut être? Ce genre de chose pourrait être fait facilement avec réflexion. Quelque chose à considérer est la génération de code pour le vrai code, donc vous n'avez pas N classes qui peuvent avoir une erreur humaine. Prévention des bugs> détection des bugs.
la source
if...throw
qu'ils aurontThrowHelper.ArgumentNull(myArg, "myArg")
, de cette façon la logique est toujours là et vous ne vous fiez pas à la couverture du code. Où laArgumentNull
méthode fera leif...throw
.Vaut-il la peine d'écrire tous ces tests?
Non.
Parce que je suis quasiment sûr que vous avez testé ces propriétés à travers d'autres tests de logique où ces classes sont utilisées.
Par exemple, vous pouvez avoir des tests pour la classe Factory qui ont une assertion basée sur ces propriétés (Instance créée avec une
Name
propriété correctement affectée par exemple).Si ces classes sont exposées à l'API publique utilisée par un tiers utilisateur / utilisateur final (@EJoshua merci de l'avoir remarqué), les tests attendus
ArgumentNullException
peuvent être utiles.En attendant C # 7, vous pouvez utiliser la méthode d'extension
Pour les tests, vous pouvez utiliser une méthode de test paramétrée qui utilise la réflexion pour créer une
null
référence pour chaque paramètre et affirmer l'exception attendue.la source
Vous pouvez toujours écrire une méthode comme celle-ci:
Au minimum, cela vous évitera de taper si vous avez plusieurs méthodes qui nécessitent ce type de validation. Bien sûr, cette solution suppose qu'aucun des paramètres de votre méthode ne peut l'être
null
, mais vous pouvez le modifier pour le changer si vous le souhaitez.Vous pouvez également l'étendre pour effectuer une autre validation spécifique au type. Par exemple, si vous avez une règle selon laquelle les chaînes ne peuvent pas être purement des espaces ou vides, vous pouvez ajouter la condition suivante:
La méthode GetParameterInfo à laquelle je fais référence fait essentiellement la réflexion (de sorte que je n'ai pas à continuer de taper la même chose encore et encore, ce qui serait une violation du principe DRY ):
la source
Je ne suggère pas de lever des exceptions du constructeur. Le problème est que vous aurez du mal à tester cela car vous devez passer des paramètres valides même s'ils ne sont pas pertinents pour votre test.
Par exemple: si vous voulez tester si le troisième paramètre lève une exception, vous devez passer correctement le premier et le second. Votre test n'est donc plus isolé. lorsque les contraintes des premier et deuxième paramètres changent, ce scénario de test échoue même s'il teste le troisième paramètre.
Je suggère d'utiliser l'API de validation Java et d'externaliser le processus de validation. Je suggère d'avoir quatre responsabilités (classes) impliquées:
Un objet de suggestion avec des paramètres annotés de validation Java avec une méthode de validation qui renvoie un ensemble de violations de contraintes. Un avantage est que vous pouvez contourner ces objets sans l'assertion pour être valide. Vous pouvez retarder la validation jusqu'à ce qu'elle soit nécessaire sans avoir à essayer d'instancier l'objet de domaine. L'objet de validation peut être utilisé dans différentes couches car il s'agit d'un POJO sans connaissance spécifique de couche. Il peut faire partie de votre API publique.
Une fabrique pour l'objet de domaine qui est responsable de la création d'objets valides. Vous passez l'objet de suggestion et l'usine appellera la validation et créera l'objet de domaine si tout va bien.
L'objet de domaine lui-même qui devrait être inaccessible pour les autres développeurs. À des fins de test, je suggère d'avoir cette classe dans la portée du package.
Une interface du domaine public pour masquer l'objet de domaine concret et rendre difficile l'utilisation abusive.
Je ne suggère pas de vérifier les valeurs nulles. Dès que vous entrez dans la couche de domaine, vous vous débarrassez de passer null ou de retourner null tant que vous n'avez pas de chaînes d'objets qui ont des extrémités ou des fonctions de recherche pour des objets uniques.
Un autre point: écrire le moins de code possible n'est pas une mesure de la qualité du code. Pensez aux compétitions de jeux 32k. Ce code est le code le plus compact mais aussi le plus salissant que vous puissiez avoir car il ne se soucie que des problèmes techniques et ne se soucie pas de la sémantique. Mais la sémantique sont les points qui rendent les choses complètes.
la source