2010-06-30 8 views
6

J'ai un objet de service non trivial développé avec TDD. Cela a commencé par une tâche simple: Pour un objet de la file, construisez une tentative de traitement asynchrone. J'ai donc écrit un test autour de ma méthode constructAttempt():TDD - Refactoring en boîte noire?

void constructAttempt() {...} 

Il existe de nombreux scénarios possibles qui doivent être pris en considération, j'ai donc des tests douzaine pour cette méthode. Puis j'ai implémenté ce que j'avais vraiment besoin de faire: Analyser toute la file d'attente et construire un lot de tentatives. Ainsi, le code ressemble plus:

public void go() { 
    for (QueuedItem item : getQueuedItems()) { 
     constructAttempt(item); 
    } 
} 

donc j'ai ajouté un nouveau test ou deux pour cette méthode go().


Enfin j'ai découvert je avais besoin de pré-traitement qui peut parfois affecter constructAttempt(). Maintenant, le code ressemble plus:

public void go() { 
    preprocess(); 
    for (QueuedItem item : getQueuedItems()) { 
     constructAttempt(item); 
    } 
} 

J'ai quelques doutes sur ce que je dois faire maintenant. Dois-je conserver le code tel quel, avec constructAttempt(), preprocess() et go() testé de façon indépendante? Pourquoi oui/pourquoi pas? Je risque de ne pas couvrir les effets secondaires du prétraitement et de l'encapsulation de rupture.

Ou dois-je refactoriser toute ma suite de tests pour appeler seulement go() (qui est la seule méthode publique)? Pourquoi oui/pourquoi pas? Cela rendrait les tests un peu plus obscurs, mais d'un autre côté, il prendrait en compte toutes les interactions possibles. Il deviendrait en fait un test de boîte noire utilisant uniquement l'API publique, ce qui peut ne pas être en accord avec TDD.

+0

Si vous refactorisez votre suite de tests pour tester uniquement la méthode go() ... comment identifierz-vous si les bugs détectés sont en préprocess() ou en constructAttempt ?? –

Répondre

6

La méthode go ne fait qu'organiser plusieurs interactions et n'est pas très intéressante en soi. Si vous écrivez vos tests contre go au lieu de vos méthodes subordonnées, les tests sont probablement compliqués car vous devrez tenir compte de l'explosion combinatoire des interactions entre preprocess et constructAttempt (et peut-être même getQueuedItems, mais cela semble relativement simple). Au lieu de cela, vous devez écrire des tests pour les méthodes subordonnées - et les tests pour constructAttempt doivent prendre en compte tous les effets potentiels du prétraitement. Si vous ne pouvez pas simuler ces effets secondaires (en manipulant la file d'attente sous-jacente ou un double de test) refactoriser votre classe jusqu'à ce que vous le pouvez.

1

Je dis que votre suite de tests appelle seulement go() car c'est la seule API publique. Ce qui signifie qu'une fois que vous avez couvert tous les scénarios de la méthode go (ce qui inclut le prétraitement et la file d'attente), cela n'a plus d'importance si vous modifiez l'implémentation interne. Votre classe reste correcte en ce qui concerne l'utilisation publique. J'espère que vous utiliserez l'inversion de dépendance pour les classes utilisées pour le prétraitement/la mise en file d'attente - de cette façon, vous pourrez tester de manière indépendante le prétraitement, puis le mocker dans votre test go().

3

@Jeff l'a à peu près juste. Ce que vous avez réellement, c'est deux responsabilités qui se produisent dans cet objet. Vous voudrez peut-être tirer les objets en file d'attente dans leur propre classe.Poussez le preprocess et constructAttempt dans des articles individuels. IMHO lorsque vous avez une classe qui traite des éléments individuels et une liste d'éléments que vous avez une odeur de code. Les responsabilités sont le conteneur de liste agit sur les éléments.

public void go() { 
    for (QueuedItem item : getQueuedItems()) { 
     item.preprocess(); 
     item.constructAttempt(); 
    } 
} 

Note: Ceci est similaire à travailler avec un modèle d'objet de commande

[EDIT 1a] Cela rend vraiment facile à tester avec moquerie. La méthode go nécessite uniquement des tests avec un seul élément de file d'attente ou aucun élément de file d'attente. Aussi, chaque item peut maintenant avoir ses tests individuels séparés de l'explosion combinatoire du go.

[EDIT 1b] Vous pourriez même être en mesure de plier le preprocess dans l'article:

public void go() { 
    for (QueuedItem asyncCommunication: getQueuedItems()) { 
     asyncCommunication.attempt(); 
    } 
} 

Maintenant vous avez un vrai command pattern.