2010-03-11 4 views
0

Я смотрю на простой класс, я должен управлять критическими разделами и замками, и я хотел бы осветить это тестовыми примерами. Имеет ли это смысл, и как это сделать? Это сложно, потому что единственный способ проверить работу класса - установить очень сложные сценарии потоковой передачи, и даже тогда нет хорошего способа протестировать утечку критической секции в 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) {} ; 

}} 

ответ

4

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

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

Однако ....

я сомнителен вашего выбора дизайна. Во-первых, в классе слишком много происходит (IMHO). Счетчик ссылок и блокировка являются ортогональными. Я бы разделил их на части, чтобы у меня был простой класс, который занимался критическим разделением, а затем построил его. Я нашел, что мне действительно нужен подсчет ссылок ... Во-вторых, есть подсчет ссылок и дизайн ваших функций блокировки; вместо того, чтобы возвращать объект, который освобождает блокировку в своем 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 делать больше; хотя, IMHO, я думаю, TryEnter() действительно требуется ОЧЕНЬ редко.

+0

Немногие проблемы с этим. # 1. Структуры CRITICAL_SECTION не могут быть перемещены или скопированы. В этом причина пересчета в первую очередь. Поэтому некоторая форма пересчета здесь важна, потому что я не хочу, чтобы клиентам приходилось беспокоиться о управлении памятью класса. # 2: Мне нравится, как ваше управление блокировкой намного лучше. Большое спасибо :) # 3: Я собираюсь взглянуть на некоторые издевательства над функциями API, что иронично, потому что половина точки объекта критического раздела должна быть в состоянии проверить код, используя его. –

+0

Я никогда не встречал # 1, чтобы быть проблемой; возможно, именно так я использую свои блокировки. Я просто добавляю экземпляр 'CCriticalSection' к классу, который должен иметь возможность блокировать области, а затем использовать класс владельца RAII для управления временем жизни блокировки. Для объектов, у которых есть свои блокировки, я редко имею копию ctor или присваивание op; это просто никогда не имеет смысла, поэтому у меня нет проблем и мне не нужен счетчик ссылок ... –

+0

Я полагаю, что я мог бы просто сделать объект критического раздела не подлежащим копированию, но пока я в сценарии, где мне действительно нужно объект критического раздела имеет семантику «shared_ptr». Если критические разделы не нуждались в специальном разрыве, я бы просто использовал 'shared_ptr' и делал с ним, но, к сожалению, они делают :( –

Смежные вопросы