2010-03-11 2 views
0

Je regarde une classe simple Je dois gérer des sections critiques et des verrous, et je voudrais couvrir cela avec des cas de test. Est-ce que cela a du sens, et comment on s'y prendrait? C'est difficile parce que la seule façon de vérifier le fonctionnement de la classe est de mettre en place des scénarios de threads très compliqués, et même là, il n'y a pas de moyen de tester la fuite d'une section critique dans Win32. Existe-t-il un moyen plus direct de s'assurer que cela fonctionne correctement?Tests unitaires Refcounted Section Critique Classe

Voici le code:

CriticalSection.hpp:

#pragma once 
#include <windows.h> 
#include <boost/shared_ptr.hpp> 

namespace WindowsAPI { namespace Threading { 

    class CriticalSectionImpl; 
    class CriticalLock; 
    class CriticalAttemptedLock; 

    class CriticalSection 
    { 
     friend class CriticalLock; 
     friend class CriticalAttemptedLock; 
     boost::shared_ptr<CriticalSectionImpl> impl; 
     void Enter(); 
     bool TryEnter(); 
     void Leave(); 
    public: 
     CriticalSection(); 
    }; 

    class CriticalLock 
    { 
     CriticalSection &ref; 
    public: 
     CriticalLock(CriticalSection& sectionToLock) : ref(sectionToLock) { ref.Enter(); }; 
     ~CriticalLock() { ref.Leave(); }; 
    }; 

    class CriticalAttemptedLock 
    { 
     CriticalSection &ref; 
     bool valid; 
    public: 
     CriticalAttemptedLock(CriticalSection& sectionToLock) : ref(sectionToLock), valid(ref.TryEnter()) {}; 
     bool LockHeld() { return valid; }; 
     ~CriticalAttemptedLock() { if (valid) ref.Leave(); }; 
    }; 

}} 

CriticalSection.cpp:

#include "CriticalSection.hpp" 

namespace WindowsAPI { namespace Threading { 

class CriticalSectionImpl 
{ 
    friend class CriticalSection; 
    CRITICAL_SECTION sectionStructure; 
    CriticalSectionImpl() { InitializeCriticalSection(&sectionStructure); }; 
    void Enter() { EnterCriticalSection(&sectionStructure); }; 
    bool TryEnter() { if (TryEnterCriticalSection(&sectionStructure)) return true; else return false; }; 
    void Leave() { LeaveCriticalSection(&sectionStructure); }; 
public: 
    ~CriticalSectionImpl() { DeleteCriticalSection(&sectionStructure); }; 
}; 

void CriticalSection::Enter() { impl->Enter(); }; 
bool CriticalSection::TryEnter() { return impl->TryEnter(); }; 
void CriticalSection::Leave() { impl->Leave(); }; 
CriticalSection::CriticalSection() : impl(new CriticalSectionImpl) {} ; 

}} 

Répondre

4

Voici trois options et personnellement je suis en faveur de la dernière ...

  • Vous pouvez créer un Interface 'usine de section critique' qui peut être transmise à votre constructeur. Cela aurait des fonctions qui ont enveloppé les fonctions de niveau API que vous devez utiliser. Vous pourriez alors se moquer de cette interface et passer le code au code lors du test et vous pouvez être sûr que les bonnes fonctions de l'API sont appelées. Vous auriez généralement un constructeur qui n'a pas pris cette interface et qui s'est initialisé avec une instance statique de l'usine qui a appelé directement l'API. La création normale des objets ne serait pas affectée (car vous les utilisez avec une implémentation par défaut) mais vous pouvez l'instrumenter en cours de test. Ceci est la route d'injection de dépendance standard et vous permet de parameterise from above. L'inconvénient de tout cela est que vous avez une couche d'indirection et que vous devez stocker un pointeur vers l'usine dans chaque instance (donc vous perdez probablement dans l'espace et le temps).
  • Vous pouvez également essayer de rayer l'API de dessous ... Il y a longtemps, j'ai cherché à tester ce type d'utilisation de l'API de bas niveau avec l'accrochage à l'API; l'idée étant que si j'accroissais les appels réels de l'API Win32, je pourrais développer une «fausse couche API» qui serait utilisée de la même manière que des objets Mock plus normaux mais qui s'appuierait sur «paramétrer d'en bas» plutôt que de paramétrer ci-dessus. Bien que cela ait fonctionné et que j'ai eu un long chemin à parcourir dans le projet, il était très complexe de s'assurer que vous vous moquiez seulement du code testé. La bonne chose à propos de cette approche était que je pouvais faire échouer les appels API dans des conditions contrôlées dans mon test; Cela m'a permis de tester des chemins d'échecs qui étaient par ailleurs TRÈS difficiles à exercer.
  • La troisième approche consiste à accepter que du code n'est pas testable avec des ressources raisonnables et que l'injection de dépendance n'est pas toujours appropriée. Rendez le code aussi simple que possible, collez-le, écrivez des tests pour les bits que vous pouvez et passez à autre chose. C'est ce que j'ai tendance à faire dans des situations comme celle-ci.

Cependant ....

Je suis douteux de votre choix de conception. Premièrement, il y a trop de choses dans la classe (IMHO). Le comptage de référence et le verrouillage sont orthogonaux. Je les séparais pour avoir une classe simple qui s'occupait de la gestion des sections critiques et ensuite je construisais dessus. J'ai vraiment besoin de compter les références ... Deuxièmement, il y a le comptage des références et la conception de vos fonctions de verrouillage; plutôt que de renvoyer un objet qui libère le verrou dans son interpréteur, pourquoi ne pas simplement avoir un objet que vous créez sur la pile pour créer un verrou de portée. Cela éliminerait une grande partie de la complexité. En fait, vous pourriez finir avec une classe de section critique qui est aussi simple que cela:

CCriticalSection::CCriticalSection() 
{ 
    ::InitializeCriticalSection(&m_crit); 
} 

CCriticalSection::~CCriticalSection() 
{ 
    ::DeleteCriticalSection(&m_crit); 
} 

#if(_WIN32_WINNT >= 0x0400) 
bool CCriticalSection::TryEnter() 
{ 
    return ToBool(::TryEnterCriticalSection(&m_crit)); 
} 
#endif 

void CCriticalSection::Enter() 
{ 
    ::EnterCriticalSection(&m_crit); 
} 

void CCriticalSection::Leave() 
{ 
    ::LeaveCriticalSection(&m_crit); 
} 

ce qui correspond à mon idée de ce genre de code étant assez simple globe oculaire plutôt que d'introduire des tests complexes ...

Vous pourriez alors avoir une classe de verrouillage scope tels que:

CCriticalSection::Owner::Owner(
    ICriticalSection &crit) 
    : m_crit(crit) 
{ 
    m_crit.Enter(); 
} 

CCriticalSection::Owner::~Owner() 
{ 
    m_crit.Leave(); 
} 

Vous utiliseriez comme ça

void MyClass::DoThing() 
{ 
    ICriticalSection::Owner lock(m_criticalSection); 

    // We're locked whilst 'lock' is in scope... 
} 

Bien sûr mon code n'utilise pas TryEnter() ou faire quelque chose complexe, mais rien n'empêche vos classes RAII simples d'en faire plus; Cependant, à mon humble avis, je pense que TryEnter() est réellement nécessaire très rarement.

+0

Quelques problèmes avec ceci. #1. Les structures CRITICAL_SECTION ne peuvent pas être déplacées ou copiées. C'est la raison pour laquelle le refcounting en premier lieu. Par conséquent, une certaine forme de recomptage est essentielle car je ne veux pas que les clients aient à se soucier de la gestion de la mémoire de la classe. # 2: J'aime beaucoup mieux la manipulation de la serrure. Merci beaucoup :) # 3: Je vais regarder en quelque sorte se moquant des fonctions de l'API - ce qui est ironique parce que la moitié du point de l'objet section critique est de pouvoir tester le code en l'utilisant. –

+0

Je n'ai jamais trouvé # 1 d'être un problème; c'est peut-être la façon dont j'utilise mes serrures. J'ajoute simplement une instance de 'CCriticalSection' à une classe qui doit être capable de verrouiller des zones de lui-même, puis d'utiliser la classe RAII 'owner' pour gérer la durée de vie du verrou. Pour les objets qui ont leurs propres verrous j'ai rarement une copie ctor ou une opération d'affectation; ça n'a jamais de sens, donc je n'ai pas de problème et je n'ai pas besoin du ref ... –

+0

Je suppose que je pourrais juste rendre l'objet section critique non-discutable mais pour l'instant je suis dans un scénario où j'ai vraiment besoin l'objet section critique à avoir la sémantique "' shared_ptr' ". Si les sections critiques n'avaient pas besoin de démontage spécial, j'utiliserais simplement 'shared_ptr' et j'aurais fini avec, mais malheureusement ils le font :( –