Test unitaire pour tester la création d'un objet de domaine

11

J'ai un test unitaire, qui ressemble à ceci:

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

J'affirme qu'un objet Personne est créé ici, c'est-à-dire que la validation n'échoue pas. Par exemple, si le Guid est nul ou si la date de naissance est antérieure au 01/01/1900, la validation échouera et une exception sera levée (ce qui signifie que le test échoue).

Le constructeur ressemble à ceci:

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;
    }

Est-ce une bonne idée pour un test?

Remarque : je suis une approche classique des tests unitaires du modèle de domaine si cela a une incidence.

w0051977
la source
Le constructeur a-t-il une logique qui mérite d'être affirmée après l'initialisation?
Laiv
2
Ne vous embêtez jamais à tester les constructeurs !!! La construction devrait être simple. Vous attendez-vous à des échecs dans Guid.NewGuid () ou le constructeur de DateTime?
ivenxu
@Laiv, veuillez consulter la mise à jour de la question.
w0051977
1
Cela ne vaut rien d'implémenter un test comme celui que vous avez partagé. Cependant, je testerais également le contraire. Je testerais le cas où birthDate provoque une erreur. C'est l'invariant de la classe que vous souhaitez contrôler et tester.
Laiv
3
Le test semble bien, sauf pour une chose: le nom. Should_create_person? Qu'est-ce qui devrait créer une personne? Donnez-lui un nom significatif, comme Creating_person_with_valid_data_succeeds.
David Arno

Réponses:

18

C'est un test valide (bien que plutôt zélé) et je le fais parfois pour tester la logique du constructeur, cependant, comme Laiv l'a mentionné dans les commentaires, vous devriez vous demander pourquoi.

Si votre constructeur ressemble à ceci:

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

Y a-t-il beaucoup d'intérêt à tester s'il lance? Que les paramètres soient correctement attribués, je peux comprendre, mais votre test est plutôt exagéré.

Cependant, si votre test fait quelque chose comme ceci:

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;
}

Ensuite, votre test devient plus pertinent (car vous lancez des exceptions quelque part dans le code).

Une chose que je dirais, c'est généralement une mauvaise pratique d'avoir beaucoup de logique dans votre constructeur. La validation de base (comme les vérifications nulles / par défaut que je fais ci-dessus) est correcte. Mais si vous vous connectez à des bases de données et chargez les données de quelqu'un, c'est là que le code commence à vraiment sentir ...

Pour cette raison, si votre constructeur mérite d'être testé (car il y a beaucoup de logique), peut-être que quelque chose d'autre ne va pas.

Vous allez presque certainement avoir d'autres tests couvrant cette classe dans les couches de logique métier, les constructeurs et les affectations de variables vont presque certainement obtenir une couverture complète de ces tests. Par conséquent, il peut être inutile d'ajouter des tests spécifiques spécifiquement pour le constructeur. Cependant, rien n'est noir et blanc et je n'aurais rien contre ces tests si je les examinais - mais je me demanderais s'ils ajoutent beaucoup de valeur au-delà des tests ailleurs dans votre solution.

Dans votre exemple:

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;
    }

Vous faites non seulement la validation, mais vous appelez également un constructeur de base. Pour moi, cela fournit plus de raisons d'avoir ces tests car ils ont maintenant la logique constructeur / validation divisée en deux classes, ce qui diminue la visibilité et augmente le risque de changement inattendu.

TLDR

Ces tests ont une certaine valeur, mais la logique de validation / affectation est susceptible d'être couverte par d'autres tests dans votre solution. S'il y a beaucoup de logique dans ces constructeurs qui nécessite des tests importants, cela me suggère qu'il y a une mauvaise odeur de code qui se cache là-dedans.

Liath
la source
@Laith, veuillez voir la mise à jour de ma question
w0051977
Je remarque que vous appelez un constructeur de base dans votre exemple. À mon humble avis, cela ajoute plus de valeur à votre test, la logique du constructeur est maintenant divisée en deux classes et présente donc un risque de changement légèrement plus élevé, ce qui donne donc plus de raisons de le tester.
Liath
"Cependant, si votre test fait quelque chose comme ceci:" <Vous ne voulez pas dire "si votre constructeur fait quelque chose comme ça" ?
Kodos Johnson
"Il y a une certaine valeur à ces tests" - intéressant pour moi de toute façon, la valeur montre que nous pouvons rendre ce test redondant en utilisant une nouvelle classe pour représenter le dob de la personne (par exemple PersonBirthdate) qui effectue la validation de la date de naissance. De même, la Guidvérification pourrait être implémentée sur la Idclasse. Cela signifie que vous n'avez plus vraiment besoin d'avoir cette logique de validation dans le Personconstructeur car il n'est pas possible d'en construire une avec des données invalides - sauf pour les nullréférences. Bien sûr, vous devez passer des tests pour les deux autres classes :)
Stephen Byrne
12

Déjà une bonne réponse ici, mais je pense qu'une chose supplémentaire mérite d'être mentionnée.

Quand on fait TDD "à la livre", il faut d'abord écrire un test qui appelle le constructeur, avant même que le constructeur soit implémenté. Ce test pourrait en fait ressembler à celui que vous avez présenté, même s'il n'y aurait aucune logique de validation à l'intérieur de l'implémentation du constructeur.

Notez également que pour TDD, il faut d'abord écrire un autre test comme

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

avant d' ajouter la vérification pour DateTime(1900,01,01)le constructeur.

Dans le contexte TDD, le test illustré est parfaitement logique.

Doc Brown
la source
Bel angle que je n'avais pas envisagé!
Liath
1
Cela me montre pourquoi une telle forme rigide de TDD est une perte de temps: le test devrait avoir une valeur après que le code soit écrit, ou vous écrivez simplement chaque ligne de code deux fois, une fois comme assertion et une fois comme code. Je dirais que le constructeur lui-même n'est pas un morceau de logique à tester; la règle commerciale "les personnes nées avant 1900 ne doivent pas être représentables" est testable, et le constructeur est l'endroit où cette règle se trouve être mise en œuvre, mais quand le test d'un constructeur vide ajouterait-il de la valeur au projet?
IMSoP
Est-ce vraiment tdd par le livre? Je créerais une instance et appellerais immédiatement sa méthode dans un code. Ensuite, j'écrirais un test pour cette méthode, et en faisant cela, je devrais également créer une instance pour cette méthode, de sorte que le constructeur et la méthode seront couverts dans ce test. Sauf dans le constructeur, il y a une certaine logique, mais cette partie est couverte par Liath.
Rafał Łużyński
@ RafałŁużyński: TDD « par le livre » est sur l' écriture de tests d' abord . Cela signifie en fait de toujours écrire un test qui échoue en premier (et non pas de compiler aussi un échec). Vous écrivez donc d'abord un test appelant le constructeur même lorsqu'il n'y a pas de constructeur . Ensuite, vous essayez de compiler (qui échoue), puis vous implémentez un constructeur vide, compilez, exécutez le test, result = green. Ensuite, vous écrivez le premier test qui échoue et vous l'exécutez - result = red, puis vous ajoutez la fonctionnalité pour rendre le test "vert" à nouveau, et ainsi de suite.
Doc Brown
Bien sûr. Je ne voulais pas d'abord écrire l'implémentation, puis tester. J'écris simplement "l'utilisation" de ce code dans un niveau supérieur, puis teste ce code, puis je l'implémente. Je fais habituellement "Outside TDD".
Rafał Łużyński