Je ne me considère pas comme un expert DDD mais, en tant qu'architecte de solution, j'essaie d'appliquer les meilleures pratiques chaque fois que possible. Je sais qu'il y a beaucoup de discussions autour du pour et du contre du "style" de setter (public) dans DDD et je peux voir les deux côtés de l'argument. Mon problème est que je travaille dans une équipe avec une grande diversité de compétences, de connaissances et d'expérience, ce qui signifie que je ne peux pas croire que chaque développeur fera les choses de la "bonne" manière. Par exemple, si nos objets de domaine sont conçus de sorte que les modifications de l'état interne de l'objet soient effectuées par une méthode mais fournissent des paramètres de propriété publique, quelqu'un définira inévitablement la propriété au lieu d'appeler la méthode. Utilisez cet exemple:
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());
}
}
Ma solution a été de rendre les setters privés privés, ce qui est possible car l'ORM que nous utilisons pour hydrater les objets utilise la réflexion pour pouvoir accéder aux setters privés. Cependant, cela présente un problème lors de l'écriture de tests unitaires. Par exemple, lorsque je veux écrire un test unitaire qui vérifie l'exigence selon laquelle nous ne pouvons pas republier, je dois indiquer que l'objet a déjà été publié. Je peux certainement le faire en appelant deux fois Publish, mais mon test suppose que Publish est correctement implémenté pour le premier appel. Cela semble un peu malodorant.
Rendons le scénario un peu plus réel avec le code suivant:
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));
}
}
Je veux écrire des tests unitaires qui vérifient:
- Je ne peux publier que si le document a été approuvé
- Je ne peux pas republier un document
- Lorsqu'elles sont publiées, les valeurs PublishedBy et PublishedOn sont correctement définies
- Une fois publié, le PublishedEvent est déclenché
Sans accès aux setters, je ne peux pas mettre l'objet dans l'état nécessaire pour effectuer les tests. Ouvrir l'accès aux colons va à l'encontre du but d'empêcher l'accès.
Comment (avez-vous) résolu (d) ce problème?
la source
Réponses:
Si vous ne pouvez pas mettre l'objet dans l'état requis pour effectuer un test, vous ne pouvez pas mettre l'objet dans l'état dans le code de production, il n'est donc pas nécessaire de tester cet état. Évidemment, ce n'est pas vrai dans votre cas, vous pouvez mettre votre objet dans l'état requis, appelez simplement Approuver.
Je ne peux pas publier à moins que le document n'ait été approuvé: écrivez un test qui appelle publier avant d'appeler approuver provoque la bonne erreur sans changer l'état de l'objet.
Je ne peux pas republier un document: écrire un test qui approuve un objet, puis appeler publier une fois réussit, mais la deuxième fois provoque la bonne erreur sans changer l'état de l'objet.
Une fois publiées, les valeurs PublishedBy et PublishedOn sont correctement définies: écrire un test qui appelle approuver puis appeler publier, affirmer que l'état de l'objet change correctement
Une fois publié, le PublishedEvent est déclenché: accrochez-vous au système d'événements et définissez un indicateur pour vous assurer qu'il est appelé
Vous devez également écrire un test pour approuver.
En d'autres termes, ne testez pas la relation entre les champs internes et IsPublished et IsApproved, votre test serait assez fragile si vous le faites car changer votre champ signifierait changer votre code de test, donc le test serait tout à fait inutile. Au lieu de cela, vous devez tester la relation entre les appels de méthodes publiques, de cette façon, même si vous modifiez les champs, vous n'auriez pas besoin de modifier le test.
la source
setup()
méthode --- pas le test lui-même.approve()
manière ou d'une autre fragile, mais dépendant d'unesetApproved(true)
manière ou d'une autre non?approve()
est une dépendance légitime dans les tests car c'est une dépendance dans les exigences. Si la dépendance n'existait que dans les tests, ce serait un autre problème.push()
etpop()
indépendamment?Une autre approche consiste à créer un constructeur de la classe qui permet de définir les propriétés internes lors de l'instanciation:
la source
Une stratégie consiste à hériter de la classe (dans ce cas, Document) et à écrire des tests sur la classe héritée. La classe héritée permet de définir l’état de l’objet dans les tests.
En C #, une stratégie pourrait être de rendre les setters internes, puis d'exposer les internes au projet de test.
Vous pouvez également utiliser l'API de classe comme vous l'avez décrit ("Je peux certainement le faire en appelant Publier deux fois"). Ce serait définir l'état de l'objet en utilisant les services publics de l'objet, cela ne me semble pas trop malodorant. Dans le cas de votre exemple, ce serait probablement la façon dont je le ferais.
la source
Pour tester dans l'isolement absolu les commandes et les requêtes que les objets de domaine reçoivent, je suis utilisé pour fournir à chaque test une sérialisation de l'objet dans l'état attendu. Dans la section d'arrangement du test, il charge l'objet à tester à partir d'un fichier que j'ai préparé précédemment. Au début, j'ai commencé avec des sérialisations binaires, mais json s'est révélé beaucoup plus facile à maintenir. Cela s'est avéré efficace, chaque fois que l'isolement absolu dans les tests fournit une valeur réelle.
éditez juste une note, parfois la sérialisation JSON échoue (comme dans le cas des graphiques d'objets cycliques, qui sont une odeur, au fait). Dans de telles situations, je sauve la sérialisation binaire. C'est un peu pragmatique, mais ça marche. :-)
la source
Vous dites
et
et je dois penser que l'utilisation de la réflexion pour contourner les contrôles d'accès sur vos classes n'est pas ce que je qualifierais de "meilleure pratique". Ça va être horriblement lent aussi.
Personnellement, je supprimerais votre cadre de tests unitaires et j'irais avec quelque chose dans la classe - il semble que vous écrivez des tests du point de vue de tester la classe entière de toute façon, ce qui est bien. Dans le passé, pour certains composants délicats qui nécessitaient des tests, j'ai intégré les asserts et le code de configuration dans la classe elle-même (c'était un modèle de conception commun pour avoir une méthode test () dans chaque classe), donc vous créez un client qui instancie simplement un objet et appelle la méthode de test qui peut se configurer comme vous le souhaitez sans méchanceté comme des hacks de réflexion.
Si vous êtes préoccupé par le gonflement du code, enveloppez simplement les méthodes de test dans #ifdefs pour les rendre uniquement disponibles dans le code de débogage (probablement une meilleure pratique en soi)
la source