Есть ли способ сломать эту зависимость для модульного тестирования? - PullRequest
4 голосов
/ 09 марта 2011

Мой класс A зависит от класса B. Вот код

//declaration
class A
{
  public:
    A(B *b);
    ~A();
    void m1();
  private:
    B *ptr_b;
};

//implementation
A::A(B *b)
{
  ptr_b = b;
}

A::~A()
{
   delete ptr_b;
}

void A::m1()
{
   ptr_b->m2();
}

Я хочу разорвать эту зависимость (для модульного тестирования) следующим решением. Вот код

 class FakeB : public B 
  {    
     public:
       FakeB();
       ~FakeB();
       virtual void m2() = 0; 
  };

 class StubB : public FakeB 
 {   
   public:
      StubB();
      ~StubB();
      void m2(); 
 }

Но когда я создаю экземпляр класса A и вызываю его метод m1 () со следующим кодом

A *ptr_a = new A(new StubB);
ptr_a->m1();

Метод m1 () вызывает метод B m2 (), потому что m2 () B не является виртуальным. класс B является устаревшим кодом из другого модуля, я не хочу менять его код но я также не хочу менять код класса А.

Любое решение, чтобы сломать эту зависимость?

Ответы [ 3 ]

6 голосов
/ 09 марта 2011

Во-первых, это плохой дизайн, имеющий delete ptr_b; в деструкторе класса A, поскольку в конструкторе A. нет new B(). Это означает, что каждый раз, когда создается экземпляр A, вы передаете право собственности наB возражает против A, оставляя вас с потенциальным риском дубликата delete для человека, использующего A, который не знает внутренностей.

Во-вторых, если вы хотите дать A "заглушку" (или "макет"", или" фальшивый ") объект вместо" реального B ", B и FakeB нужен общий интерфейс, содержащий все методы из B, которые нужны A как виртуальные методы:

class FakeB : public InterfaceB 

и

class B : public InterfaceB 

, поэтому все функции-члены A могут использовать параметры типа InterfaceB * вместо B *.Тогда внедрение объекта FakeB в A становится очевидным делом.

К сожалению, это означает, что вам нужно изменить B (по крайней мере, немного).Если это не вариант, всегда есть возможность обернуть B некоторым классом WrapperB (это в основном та же идея, что и в классическом шаблоне адаптера ):

class WrapperB: public InterfaceB 
{
    B _b;
 public:
    WrapperB(/* some parameters */) : _b(/* same parameters */){}

    // Here you need to implement all methods of
    // InterfaceB and delegate them to the original method calls
    // of _b. You should give them the same name and signature as
    // the corresponding (non-virtual) methods in B.
    // For example, if there is a method m2 in B, 
    // there should be a pure virtual method m2 in InterfaceB, and
    // an implementation here like this:
    virtual void m2(){ _b.m2(); }
};

WrapperB будет содержать только очень простой и простой код делегирования метода, для которого вы можете опустить модульные тесты.И вы должны использовать WrapperB вместо B, когда вы собираетесь использовать его вместе с A. Но вы получаете отлично тестируемый модуль class A.

Другой (возможно, даже лучше)вариант заключается в построении класса WrapperB таким образом, что вы вставляете ссылку на объект B извне в него:

class WrapperB: public InterfaceB 
{
    B& _b;
 public:
    WrapperB(B& b) :_b(b){}

    // implement InterfaceB methods as above
    virtual void m2(){ _b.m2(); }

}

Вы можете использовать его так же, как это:

B b;
A a(WrapperB(b));

FakeB fb;
A a_for_test(fb);
2 голосов
/ 09 марта 2011

Мерхаба Онур

Другая идея состоит в том, чтобы использовать некоторые символы препроцессора для переключения кода класса А между обычным режимом и режимом модульного тестирования. Например:

Файл A.hpp

#ifndef UNIT_TESTING
# include "B.hpp" // contains "normal" class B
#else
# include "Testable_B.hpp" // contains "fake" class B, dedicated for unit testing.
#endif

UNIT_TESTING будет символом препроцессора, который вы включите только при построении модульного теста.

В случае, если файл Testable_B.hpp содержит класс с другим именем, отличным от «B» (например, Testable_B), вам также необходимо добавить эти директивы в определение класса A. Недостаток заключается в том, что если потребуется больше таких изменений , это может привести к путанице в определении класса.

Еще один способ - использовать typedef:

#ifndef UNIT_TESTING
# include "B.hpp" // contains "normal" class B
#else
# include "Testable_B.hpp" // contains "fake" class B, dedicated for unit testing.
  typedef Testable_B B;
#endif

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

1 голос
/ 09 марта 2011

Возможность разорвать зависимость - изменить путь включения в вашем make-файле и включить вашу версию класса B. Я не могу сказать, работает ли это в вашей схеме модульного тестирования.

...