- PVSM.RU - https://www.pvsm.ru -
Если вы верите в Agile и разработка через тестирование для вас является нормой, а не какой-то непонятной практикой, но наверное столкнулись с такой нехорошей проблемой как организацией тестирования объектов которые используют другие объекты через интерфейсы на C++.
Если для .NET есть замечательная библиотека Rhino.Mocks [1], которой достаточно «скормить» интерфейс и вы получаете возможность программирования поведения методов интерфейса прямо в модульном тесте. То для С++ все сильно сложнее, так как нет замечательного рефлекшена [2] который позволяет строить код во время исполнения. И приходится писать объекты-заглушки вручную. И в случае изменения интерфейса приходится не только обновлять все классы в приложении но обновлять весь набор «одноразовых» классов заглушек реализующих интерфейс которые применяются в тестах.
Я очень ленивый, я не люблю писать код который за меня может сделать какая-то автогенерилка на python или perl. А также мне нравится концепция: код должен не должен содержать «лишних» директив #define насколько, насколько это возможно, и по возможности чтобы все наборы тестов были упакованы в классы. Именно поэтому среди всех библиотек обеспечения модульного тестирования для C++ я предпочитаю CxxTest [3] который использует концепцию генератора оглавления всех тестовых наборов.
То же самое относится и mock-объектам, я не люблю переписывать всякие одноразовые классы которые реализуют заглушки интерфейсов применяемых в тестах. И ведь зачем их переписывать, если можно применить тот же прием что и Rhino.Mocks для .Net но только на уровне компиляции?
Сказано, сделано! Представляю CxxMock [4] (зеркало на GitHub [5]).
Ремарка 1: Библиотека написана давно, достаточно успешно применяется на моих проектах.
Ремарка 2: Другие решения существуют, например тот же googlemock [6] но он идеологически не совместим с CxxTest.
Ремарка 3: CxxMock [4] использует, по возможности, такую же систему сигнатур методов определения ожиданий что и Rhino.Mocks [1], поэтому если у вас есть проект на 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 сигнатурой
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();
У любой даже самой умной программы всегда есть ограничения. Так как 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 не может:
Если вы верите в Agile и для модульного тестирования проекта на С++ применяете библиотеку CxxTest, но тратите время на поддержку созданных вручную объектов-заглушек для проверки объектов использующих интерфейсы, то CxxMock может вам сильно упростить задачу.
За счет минимального использования директив #define, средства IDE позволят вам всегда найти все места где используется тот или иной метод интерфейса без лишних затрат на поддерживание «одноразовых» объектов.
Автор: sbase
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/83584
Ссылки в тексте:
[1] Rhino.Mocks: http://www.hibernatingrhinos.com/oss/rhino-mocks
[2] рефлекшена: https://ru.wikipedia.org/wiki/%D0%9E%D1%82%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5_%28%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%29
[3] CxxTest: http://cxxtest.com
[4] CxxMock: http://cxxmock.com
[5] GitHub: https://github.com/comm644/cxxmock
[6] googlemock: http://code.google.com/p/googlemock/
[7] Зеркало на SourceForge: http://sourceforge.net/projects/cxxmock/
[8] CxxTest: http://cxxtest.com/
[9] Источник: http://habrahabr.ru/post/250979/
Нажмите здесь для печати.