Alexander Radchenko:
Есть философский вопрос по поводу юнит тестов и C++. Довольно типичная ситуаций когда есть функция которую надо протестировать, на вход она принимает объект некоторого типа и вызывает у него функцию если выполняются определенные условия.
Хочется написать тест, который проверит что функция действительно вызвалась.
В умных книжках рекомендуют делать вот так.
https://gist.github.com/anonymous/6787337
Собствено это наименее intrusive метод, который не меняет поведедение существующего кода.
Но при этом он добавляет virtual, который в целом отрицательно влияет на производительность.
Мое мнение что цена достаточно мала чтобы этим можно было пренебречь. Скоростной код не должен активно использовать OOP в любом случае, и должен быть ближе к data driven C со структурами который легко тестировать.
Интересует мнение остальных ? Насколько сильно ваш C++ код покрыт тестами ? На какие компромисы вы готовы идти чтобы.
увеличить покрытие ?
P.S. Книжка которую я имел в виду.
http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052
Нарезка слайдов на ту же тему.
http://www.slideshare.net/apolci/working-with-legacy-code
Ruslan Abdikeev:
про data driven и OOP я промолчу, а в остальном - да, тупо виртуальные функции и https://code.google.com/p/googlemock/
покрытие довольно высокое.
благодаря C++ gmock код выглядит условно проще чем тот, что выше.
https://code.google.com/p/chromium/codesearch#chromium/src/chrome/browser/ui/autofill/account_chooser_model_unittest.cc&q=account_chooser&sq=package:chromium&l=94
EXPECT_CALL(*delegate(), AccountChoiceChanged()).Times(1); и т.п.
Alexander Radchenko:
Хм, интересно.
Выглядит действительно проще.
Надо глянуть на внутренности.
у нас сейчас CppUnitLite2, если gmock с ним совместим то может будет попробовать заменить.
Ruslan Abdikeev:
если virtual реально тормозит - я бы пересмотрел композицию классов.
никто не отменял взлёт солнца вручную, но ради тестов я бы послал.
Simon Kozlov:
А как это работает, на пальцах?
Почему это в начале теста?
Ruslan Abdikeev:
ну, генерируется класс который перехватывает и считает все вызовы, смотрит переданные в методы аргументы и т.п.
Simon Kozlov:
А перехватывает через наследование?
Ruslan Abdikeev:
ну да.
при желании можно без наследования - но тогда понятное дело надо все вокруг тоже переписать.
строго говоря сам по себе класс вообще мало что делает -- он в основном только жалуется, что на протяжении теста в нём вызывали методы, а никаких EXPECT на методы не сделали.
EXPECT создает автоматический объект подключенный к сгенерированному.
этот автоматический объект уже проверяет что всё нормально, параметры правильные, правильное количество раз и т.п.
Simon Kozlov:
Ага, круто.
А AccountChoiceChanged это название метода?
А почему со скобочками записано?
Ruslan Abdikeev:
потому что нет параметров.
это не вызов ;)
если бы он принимал int, я бы написал.
EXPECT_CALL(...., foo(42))
и он бы послал если бы вдруг не 42.
Simon Kozlov:
И он проверит, что позвалось именно 42?
Ruslan Abdikeev:
да.
Simon Kozlov:
Все методы специально объявлять...
Но в общем дело конечно наживное.
Ruslan Abdikeev:
ну, идея в том, что если у тебя в интерфейсе 100500 методов, наверное gmock тебе уже не поможет, да.
а так -
EXPECT_CALL(turtle, GetY())
.WillOnce(Return(100))
.WillOnce(Return(200))
.WillRepeatedly(Return(300));
или ты в том смысле что руками писать?
gmock_gen.py.
"сгенерированный" - это я в прямом смысле этого слова.
хочешь - сам пишешь, и тогда препроцессор.
хочешь - сгенерируют по .h.
Simon Kozlov:
Только в тесте, да?
Т.е. в отлаживаемом релизном коде этого всего нет.
Ruslan Abdikeev:
нет конечно.
он тупо сделает то же самое что ты руками - объявит класс с тонной MOCK_METHODxxx.
Alexander Radchenko:
Выглядит круто, да.
Еще вопрос. Как вы обычно тестируете C++ код в котором используется pimpl ?
Вынос pimpl декларации в отдельный header и подключение его в тесте ?
Ruslan Abdikeev:
ммм, я не уверен что понимаю.
надо протестировать то, что внутри pimpl или то, что обещает интерфейс?
интерфейс - см выше.
pimpl - надо его и тестировать.
Alexander Radchenko:
что внутри.
сейчас набросаю код.
в общем случае это проблема доступа если реализация pimpl скрыта в cpp файле.
Ruslan Abdikeev:
ну вот натурально его и тестируешь, нет?
я не думаю что понимаю проблему.
пример сестра пример!
если скрыта, то понятное дело не протестируешь никак, кроме как тупо интерфейсно.
можно всякие секретные методы для получения всякого говна, но думаю получится херня.
а зачем её скрывать?
(я опять же промолчу про смертельное комбо "у нас тормозят виртуальные функции" и "у нас скрытый pimpl")
Alexander Radchenko:
чтобы пользовательский код видел только открытый API, и не мог полагаться на то как все реализовано внутри.
т.е. хочется чтобы для пользовательского кода реализация была черным ящиком, а для тестов белым.
Ruslan Abdikeev:
а, я понял.
не, ничего магического не придумали -- тупо неследуешься от реализации pimpl, руками подключаешь и т.п.
там можно мышкой пощёлкать чтобы позырить кто есть кто.
там вот этот AutofillDialogControllerImpl - как можно понять из названия - это внутренности.
но их пишут как если бы они были чем-то конечным.
но такое довольно редко бывает, ибо всем лень.
TDD по определению означает, что смысл дизайна - не в достижении цели, а в удобстве написания юнит-тестов.
Alexander Radchenko:
т.е. вынос интерфейса Scene::Impl в отдельный header и тестирование его как отдельной сущности независимой от Scene ?
Ruslan Abdikeev:
ну да.
если народ тупой -- создай каталог "internal" или там "detail".
можешь натурально в namespace перенести (не Scene::Impl, а detail::SceneImpl).
тогда уже очевидно точно надо битой.
если народ тупой категорически - можно триггер на presubmit вхерачить.
Alexander Radchenko:
понятно, detail namespace и отдельный header я думаю будет достаточно.