Pull to refresh

CxxMock — Mock-объекты в C++

Reading time 5 min
Views 19K
Если вы верите в Agile и разработка через тестирование для вас является нормой, а не какой-то непонятной практикой, но наверное столкнулись с такой нехорошей проблемой как организацией тестирования объектов которые используют другие объекты через интерфейсы на C++.

Если для .NET есть замечательная библиотека Rhino.Mocks, которой достаточно «скормить» интерфейс и вы получаете возможность программирования поведения методов интерфейса прямо в модульном тесте. То для С++ все сильно сложнее, так как нет замечательного рефлекшена который позволяет строить код во время исполнения. И приходится писать объекты-заглушки вручную. И в случае изменения интерфейса приходится не только обновлять все классы в приложении но обновлять весь набор «одноразовых» классов заглушек реализующих интерфейс которые применяются в тестах.

Решение


Я очень ленивый, я не люблю писать код который за меня может сделать какая-то автогенерилка на python или perl. А также мне нравится концепция: код должен не должен содержать «лишних» директив #define насколько, насколько это возможно, и по возможности чтобы все наборы тестов были упакованы в классы. Именно поэтому среди всех библиотек обеспечения модульного тестирования для C++ я предпочитаю CxxTest который использует концепцию генератора оглавления всех тестовых наборов.

То же самое относится и mock-объектам, я не люблю переписывать всякие одноразовые классы которые реализуют заглушки интерфейсов применяемых в тестах. И ведь зачем их переписывать, если можно применить тот же прием что и Rhino.Mocks для .Net но только на уровне компиляции?

Сказано, сделано! Представляю CxxMock (зеркало на GitHub).

Ремарка 1: Библиотека написана давно, достаточно успешно применяется на моих проектах.

Ремарка 2: Другие решения существуют, например тот же googlemock но он идеологически не совместим с CxxTest.

Ремарка 3: CxxMock использует, по возможности, такую же систему сигнатур методов определения ожиданий что и Rhino.Mocks, поэтому если у вас есть проект на C# и С++, то не будет проблем в переучивании.

Быстрый старт


1. Добавляем шаг авто-генерации заголовочного файла содержащего реализации всех интерфейсов которые нужны в тестах.

python cxxmockgen.py <IMyCoolInterface.h>  <header.h> <header.h>.... >generated_mocks.h


2. Используем в тестах, также как это делается в Rhino.Mocks для C#

#include "generated_mocks.h" // подключаем созданный заголовочный файл.

class TestMyMockObjects : public CxxTest::TestSuite
{
...
...
    
    void testQuickStart() 
    {
        //создам экземпляр репозитория mock-объектов
        CxxMock::Repository mocks;

        //получаем объект реализующий нужный интерфейс
        IMyCoolInterface* mock = mocks.create<IMyCoolInterface>();

        //программируем ОЖИДАНИЕ поведения :
        // КОГДА метод IMyCoolInterface::method() будет вызван с параметром 10 ТО вернуть 5
        TS_EXPECT_CALL( mock->method(10) ).returns( 5 );

        //программируем ОЖИДАНИЕ поведения для методов которые ничего не возвращают 
        TS_EXPECT_CALL_VOID( mock->voidMethod() );

        //указываем подсистеме CxxMock что мы закончили записывать поведение и 
        //теперь нужно его воспроизвести и сравнить фактически вызванные методы с ожидаемыми.
        mocks.replay();

        //начали  выполнять стандартный код теста с вызовами методов тестируемого объекта
    
        // выполняем какой-то код который вызовет IMyCoolInterface::method() с параметром 10 .
        // тут приведен просто явный вызов , как фрагмент само-теста
        TS_ASSERT_EQUALS( 5, mock->method(10) );

        //выполняем какой-то код который вызовет IMyCoolInterface::voidMethod() 
        // тут приведен просто явный вызов , как фрагмент само-теста
        mock->voidMethod();
    
        //закончили выполнять основной тест
    
        //проверяем что все вызовы которые мы ожидали были вызваны.
        mocks.verify();
    }
} 


здесь применяются два макроса с совместимой с CxxTest сигнатурой

  • TS_EXPECT_CALL — программирование ожидания вызова для метода возвращающего значение, и
  • TS_EXPECT_CALL_VOID — программирование ожидания вызова для void метода не возвращающего значение


А что еще может?


CxxMock поддерживает не все возможности Rhino.Mocks, но те которые больше всего нужны он поддерживает. Здесь я покажу как их использовать.

Установка возвращаемого значения:

 TS_EXPECT_CALL( object->method() )
          .returns( retvalue );


Снять проверку на количество вызовов, по умолчанию программируется только один вызов. Бывает полезно если нам не интересен этот вызов, например в случаях стандартного оповещения об изменениях.

 TS_EXPECT_CALL( object->method() )
         .repeat().any();

Явно указать что ждать только один вызов. Это поведение по умолчанию. Если задано ожидание вызова, то CxxMock будет проверять что был всего один вызов.

 TS_EXPECT_CALL( object->method() )
         .repeat().once();

Явно указать сколько вызовов метода ждать. Если вызовов будет меньше или больше, то произойдет отказ проверки

 TS_EXPECT_CALL( object->method() )
         .repeat().times(5);

Не проверять аргументы. Полезно применять если мы накрываем тестами готовую систему и не хочется разбираться как она работает. нам важно только что вызовы были или просто хотим пропустить эту проверку…

 TS_EXPECT_CALL( object->method() )
        .ignoreArguments();

Пример общего случая программирования ожидания метода который будет возвращать всегда число 10 независимо от аргументов и независимо от количества вызовов.

 TS_EXPECT_CALL( object->method("value1", 5) )
        .ignoreArguments()
        .returns( 10 )
        .repeat().any();

Чего CxxTest не может ?


У любой даже самой умной программы всегда есть ограничения. Так как CxxMock имеет свой генератор кода на основе вашего кода, то возникает вопрос: А будет ли это работать именно с Вашим кодом?

Ответ: Может будет, а может и нет. CxxMock поддерживает пространства имен, в том числе вложенные, однако очень требовательно относится к сигнатурам методов.

Например такая сигнатура методов интерфейса не поддерживается:

class NotSupportedInterface
{
public:
    virtual void canNotSetPointer2(int *arg) = 0;
    virtual void canNotSetPointer3(int&arg) = 0;

    virtual void canNotSetReference2(int &arg) = 0;
    virtual void canNotSetReference3(int&arg) = 0;

    virtual ~NotSupportedInterface(){}
};

А вот такие поддерживаются

class Interface
{
public:
    virtual void setValue( Type arg ) = 0;	
    virtual Type getValue() = 0;
	
    virtual Type& canGetReference() = 0;
    virtual Type* canGetPointer() = 0;
    
    virtual void canSetPointer( Type* arg ) = 0;
    virtual void canSetReference(Type& arg) = 0;

    virtual void canParseCompressed(Type arg1, Type arg2) = 0;
    virtual Type canParseReference(const Type& arg) = 0;
    virtual void canSetConstParam(const Type* arg) = 0;

    virtual void voidMethod() = 0;
    virtual ~Interface(){}
};


Основное правило для аргументов: модификатор + тип + пробел + имя. При этом тип должен быть задан одним словом.

А также CxxMock не может:
  1. CxxMock НЕ РАБОТАЕТ с абстрактными классами и шаблонами.
  2. CxxMock Не имеет методов проверки аргументов по условию, аргументы всегда проверяются на равенство.
  3. CxxMock Не поддерживает произвольный порядок ожиданий вызовов методов, порядок жестко задан.
  4. CxxMock Не поддерживает сложные типы например: Type*&, Type<A,B> — используйте typedef для получения нормального имени, проще будет в отладке.
  5. CxxMock Не поддерживает настройку действий на вызов (метод .do() ), возможно в будущем появится, а сейчас используйте ручную реализацию mock-объекта.

Заключение


Если вы верите в Agile и для модульного тестирования проекта на С++ применяете библиотеку CxxTest, но тратите время на поддержку созданных вручную объектов-заглушек для проверки объектов использующих интерфейсы, то CxxMock может вам сильно упростить задачу.

За счет минимального использования директив #define, средства IDE позволят вам всегда найти все места где используется тот или иной метод интерфейса без лишних затрат на поддерживание «одноразовых» объектов.

Ссылки


  1. Основной сайт CxxMock
  2. Зеркало на SourceForge
  3. Зеркало на GitHub
  4. CxxTest
  5. Rhino.Mocks
  6. GoogleMock
Tags:
Hubs:
+15
Comments 6
Comments Comments 6

Articles