2017-04-25 4 views
2

Je code depuis environ 12 ans, mais je ne me suis jamais habitué à TDD dans tout ce temps. Eh bien, les choses sont sur le point de changer, mais puisque j'apprends tout seul, j'espère que vous pourriez m'aider.Meilleures pratiques pour cette tentative TDD

Je poste un exemple de jeu pour une classe de poitrine TRÈS SIMPLE. Lorsque le joueur attrape un coffre, il enregistre l'heure actuelle à laquelle il a été obtenu. Cette poitrine prend un certain temps à s'ouvrir, donc, j'ai besoin, pour des raisons d'interface utilisateur, de montrer le temps qu'il faut pour ouvrir. Chaque coffre a un type, et ce type est lié à une valeur de base de données de combien de temps il faudrait pour ouvrir.

Il s'agit d'un "test-pas-juste-choses-faites-rapide-esprit". Considérez que ChestsDatabase et DateManager sont des singletons contenant les valeurs liées à la base de données et l'heure système actuelle enveloppée dans une classe.

public class Chest { 
    private readonly int _type; 
    private readonly float _timeObtained; 

    public Chest(int type, float timeObtained) { 
     _type = type; 
     _timeObtained = timeObtained; 
    } 

    public bool IsOpened() { 
     return GetRemainingTime() <= 0; 
    } 

    // It depends heavily on this concrete Singleton class 
    public float GetRemainingTime() { 
     return ChestsDatabase.Instance.GetTimeToOpen(_type) - GetPassedTime(); 
    } 

    // It depends heavily on this concrete Singleton class 
    private float GetPassedTime() { 
     return DateManager.Instance.GetCurrentTime() - _timeObtained; 
    } 
} 

Bien sûr, je aurais pu faire dans une dépendance de la mode d'injection et de se débarrasser des singletons:

public class Chest { 
    private readonly ChestsDatabase _chestsDatabase; 
    private readonly DateManager _dateManager; 
    private readonly int _type; 
    private readonly float _timeObtained; 

    public Chest(ChestsDatabase chestsDatabase, DateManager dateManager, int type, float timeObtained) { 
     _chestsDatabase = chestsDatabase; 
     _dateManager = dateManager; 
     _type = type; 
     _timeObtained = timeObtained; 
    } 

    public bool IsOpened() { 
     return GetRemainingTime() <= 0; 
    } 

    public float GetRemainingTime() { 
     return _chestsDatabase.GetTimeToOpen(_type) - GetPassedTime(); 
    } 

    private float GetPassedTime() { 
     return _dateManager.GetCurrentTime() - _timeObtained; 
    } 
} 

Et si j'utilise des interfaces pour exprimer la même logique? Cela va être beaucoup plus "TDD-friendly", non? (En supposant que je l'ai fait d'abord les tests, bien sûr)

public class Chest { 
    private readonly IChestsDatabase _chestsDatabase; 
    private readonly IDateManager _dateManager; 
    private readonly int _type; 
    private readonly float _timeObtained; 

    public Chest(IChestsDatabase chestsDatabase, IDateManager dateManager, int type, float timeObtained) { 
     _chestsDatabase = chestsDatabase; 
     _dateManager = dateManager; 
     _type = type; 
     _timeObtained = timeObtained; 
    } 

    public bool IsOpened() { 
     return GetRemainingTime() <= 0; 
    } 

    public float GetRemainingTime() { 
     return _chestsDatabase.GetTimeToOpen(_type) - GetPassedTime(); 
    } 

    private float GetPassedTime() { 
     return _dateManager.GetCurrentTime() - _timeObtained; 
    } 
} 

Mais comment diable suis-je censé tester quelque chose comme ça? Serait-ce comme ça?

[Test] 
    public void SomeTimeHavePassedAndReturnsRightValue() 
    { 
     var mockDatabase = new MockChestDatabase(); 
     mockDatabase.ForType(0, 5); // if Type is 0, then takes 5 seconds to open 
     var mockManager = new MockDateManager(); 
     var chest = new Chest(mockDatabase, mockManager, 0, 6); // Got a type 0 chest at second 6 
     mockManager.SetCurrentTime(8); // Now it is second 8 
     Assert.AreEqual(3, chest.GetRemainingTime()); // Got the chest at second 6, now it is second 8, so it passed 2 seconds. We need 5 seconds to open this chest, so the remainingTime is 3 
    } 

Est-ce logiquement correct? Est-ce que je manque quelque chose? Parce que cela semble si grand, si compliqué, si ... faux. J'ai dû créer 2 classes supplémentaires ~ juste ~ pour ces tests.

Voyons voir un cadre moqueur:

[Test] 
    public void SomeTimeHavePassedAndReturnsRightValue() 
    { 
     var mockDatabase = Substitute.For<IChestsDatabase>(); 
     mockDatabase.GetTimeToOpen(0).Returns(5); 
     var mockManager = Substitute.For<IDateManager>(); 
     var chest = new Chest(mockDatabase, mockManager, 0, 6); 
     mockManager.GetCurrentTime().Returns(8); 
     Assert.AreEqual(3, chest.GetRemainingTime()); 
    } 

je me suis débarrassé de deux classes avec le cadre, mais encore, je sens qu'il ya quelque chose de mal. Y a-t-il un moyen plus simple dans ma logique? Dans ce cas, utiliseriez-vous un cadre moqueur ou des classes implémentées? Est-ce que vous vous débarrasseriez complètement des tests ou insisteriez-vous sur l'une de mes solutions? Ou comment rendre cette solution meilleure?

J'espère que vous pouvez m'aider dans mon voyage TDD. Merci.

Répondre

3

Pour votre conception actuelle votre dernière tentative est logiquement juste et proche de ce que je considérerais comme un cas de test optimal.

Je recommande d'extraire les variables fictives au champ. Je réorganiserais également les lignes de test pour établir une distinction claire entre la configuration, l'exécution et la vérification. L'extraction du type de poitrine à la constante rend également le test plus facile à comprendre.

private IChestsDatabase mockDatabase = Substitute.For<IChestsDatabase>(); 
private IDateManager mockManager = Substitute.For<IDateManager>(); 
private const int DefaultChestType = 0; 

[Test] 
public void RemainingTimeIsTimeToOpenMinusTimeAlreadyPassed() 
{ 
    mockDatabase.GetTimeToOpen(DefaultChestType).Returns(5); 
    mockManager.GetCurrentTime().Returns(6+2); 
    var chest = new Chest(mockDatabase, mockManager, DefaultChestType, 6); 

    var remainingTime = chest.GetRemainingTime(); 

    Assert.AreEqual(5-2, remainingTime); 
} 

Maintenant, pour un commentaire plus général. Le principal avantage de TDD est qu'il vous donne un retour sur votre conception. Vos sentiments que le code de test est grand, compliqué et faux sont une rétroaction importante. Pensez-y comme design pressure. Les tests vont s'améliorer à la fois avec le refactoring des tests, ainsi que lorsque le design s'améliore.

Pour votre code, je considérerais ces questions de conception:

  1. Les responsabilités sont attribuées correctement? En particulier, est-ce la responsabilité de Chest de connaître les temps passés et restants?
  2. Y a-t-il un concept qui manque dans la conception? Peut-être que chaque coffre a un verrou, et il y a un verrou de base de temps. Et si nous passions le TimeToOpen au lieu de Type to Chest lors de la construction? Pensez-y comme passant une aiguille au lieu de passer une botte de foin, dans laquelle l'aiguille est encore à trouver. Pour référence, voir this post

Pour une bonne discussion sur la façon dont les tests peuvent fournir une rétroaction de conception, reportez-vous au logiciel orienté objet croissance guidée par des tests par Steve Freeman et Nat Pryce.

Pour un bon ensemble de pratiques pour écrire des tests lisibles en C#, je recommande The Art of Unit Testing par Roy Osherove.

2

Il y a quelques points importants qui sont nécessaires pour être pris en considération lors de l'écriture des tests unitaires comme indiqué

  1. projet séparé pour les tests unitaires.

  2. Une classe pour l'écriture de tests unitaires de fonctions dans une classe du code principal .

  3. Conditions de couverture dans les fonctions.
  4. Test Driven Development (TDD)

Si vous voulez vraiment en savoir plus (avec des exemples), jetez un oeil à ce tutoriel

Tests unitaires C# - meilleures pratiques https://www.youtube.com/watch?v=grf4L3AKSrs