Если вы верите в 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 не может:
- CxxMock НЕ РАБОТАЕТ с абстрактными классами и шаблонами.
- CxxMock Не имеет методов проверки аргументов по условию, аргументы всегда проверяются на равенство.
- CxxMock Не поддерживает произвольный порядок ожиданий вызовов методов, порядок жестко задан.
- CxxMock Не поддерживает сложные типы например: Type*&, Type<A,B> — используйте typedef для получения нормального имени, проще будет в отладке.
- CxxMock Не поддерживает настройку действий на вызов (метод .do() ), возможно в будущем появится, а сейчас используйте ручную реализацию mock-объекта.
Заключение
Если вы верите в Agile и для модульного тестирования проекта на С++ применяете библиотеку CxxTest, но тратите время на поддержку созданных вручную объектов-заглушек для проверки объектов использующих интерфейсы, то CxxMock может вам сильно упростить задачу.
За счет минимального использования директив #define, средства IDE позволят вам всегда найти все места где используется тот или иной метод интерфейса без лишних затрат на поддерживание «одноразовых» объектов.
Ссылки
Автор: sbase