Модульное тестирование Пересчитано Класс критической секции - PullRequest
0 голосов
/ 11 марта 2010

Я смотрю на простой класс, мне нужно управлять критическими секциями и блокировками, и я хотел бы покрыть это тестовыми примерами. Имеет ли это смысл, и как можно это сделать? Это сложно, потому что единственный способ проверить, работает ли класс, - это установить очень сложные сценарии потоков, и даже тогда нет хорошего способа проверить наличие утечки критического раздела в Win32. Есть ли более прямой способ убедиться, что он работает правильно?

Вот код:

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

}}

1 Ответ

4 голосов
/ 11 марта 2010

Вот три варианта, и лично я предпочитаю последний ...

  • Вы можете создать интерфейс «фабрики критических секций», который можно передать вашему конструктору. Это будет иметь функции, которые обернуты функции уровня API, которые вам нужно использовать. Затем вы могли бы смоделировать этот интерфейс и передать макет коду во время тестирования, и вы можете быть уверены, что вызываются правильные функции API. Обычно у вас также есть конструктор, который не принимает этот интерфейс и который вместо этого инициализирует себя статическим экземпляром фабрики, который вызывается непосредственно к API. Нормальное создание объектов не будет затронуто (так как у вас есть реализация по умолчанию), но вы можете использовать инструмент в процессе тестирования. Это стандартный путь внедрения зависимостей, и вы можете параметризовать сверху . Недостатком всего этого является то, что у вас есть слой косвенности, и вам нужно хранить указатель на фабрику в каждом случае (так что вы, вероятно, проигрываете как в пространстве, так и во времени).
  • В качестве альтернативы вы можете попытаться смоделировать API снизу ... Давным-давно я изучал этот вид низкоуровневого использования API с перехватом API; Идея заключалась в том, что, если бы я перехватил реальные вызовы Win32 API, я мог бы разработать «ложный уровень API», который бы использовался так же, как и более обычные Mock Objects, но полагался бы на «параметризацию снизу», а не параметризацию сверху. Хотя это сработало, и я проделал довольно длинный путь в проекте, было очень сложно убедиться, что вы только издеваетесь над тестируемым кодом. Хорошая вещь в этом подходе состояла в том, что я мог вызвать сбой вызовов API в контролируемых условиях в моем тесте; это позволило мне проверить пути неудач, которые в противном случае были ОЧЕНЬ трудными для осуществления.
  • Третий подход заключается в том, чтобы признать, что некоторый код не может быть протестирован с разумными ресурсами и что внедрение зависимостей не всегда подходит. Сделайте код настолько простым, насколько это возможно, оцените его, напишите тесты для битов, которые вы можете, и двигайтесь дальше. Это то, что я склонен делать в подобных ситуациях.

Однако ....

Я сомневаюсь в вашем выборе дизайна. Во-первых, в классе происходит слишком много (ИМХО). Подсчет ссылок и блокировка являются ортогональными. Я разделил их на части, чтобы у меня был простой класс, который выполнял управление критическими секциями, а затем на его основе я обнаружил, что мне действительно нужен подсчет ссылок ... Во-вторых, есть подсчет ссылок и дизайн ваших функций блокировки; вместо того, чтобы возвращать объект, который снимает блокировку в его dtor, почему бы просто не иметь объект, который вы создаете в стеке для создания блокировки с областью действия. Это позволило бы устранить большую часть сложности. Фактически, вы можете получить класс критической секции, который так прост:

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

Что согласуется с моим представлением о том, что этот вид кода достаточно прост, чтобы смотреть в глаза, а не вводить сложное тестирование ...

Тогда вы могли бы иметь класс блокировки с областью видимости, такой как:

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

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

Вы бы использовали это так

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

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

Конечно, мой код не использует TryEnter() и не делает ничего сложного, но ничто не мешает вашим простым классам RAII делать больше; хотя, ИМХО, я думаю TryEnter() на самом деле требуется ОЧЕНЬ редко.

...