что такое moc файл

Инструмент moc читает заголовочный файл C++. Если он находит одно или более объявлений классов, которые содержат макрос Q_OBJECT, то он порождает файл исходного кода C++, содержащий мета-объектный код для этих классов. Кроме всего прочего, мета-объектный код требуется механизму сигналов и слотов, информации о типе времени выполнения и системы динамических свойств.

Файл исходного кода C++, сгенерированный moc, должен компилироваться и компоноваться с помощью реализации класса.

Если вы используете qmake для создания своих make-файлов, в правила сборки будет включен вызов moc когда это необходимо, поэтому вам не нужно использовать moc непосредственно. Дополнительную информацию по moc смотрите в статье Почему Qt не использует шаблон для сигналов и слотов?

Использование

Обычно moc используется с входным файлом, содержащим такое объявление класса:

В дополнение к сигналам и слотам, показанным выше, moc также реализует свойства объекта как в следующем примере. Макрос Q_PROPERTY() объявляет свойство объекта, в то время как Q_ENUMS() объявляет список перечислимых типов внутри класса используемый внутри системы свойств.

В следующем примере мы объявляем свойство перечислимого типа Priority, которое также называется priority и имеет функцию получения значения priority() и функцию установки значения setPriority().

Макрос Q_FLAGS() объявляет перечисления, которые используются как флаги, т.е. соединены через ИЛИ. Другой макрос, Q_CLASSINFO(), позволяет вам прикреплять дополнительные пары имя/значение к мета-объекту класса:

Производимый moc вывод должен компилироваться и компоноваться, также как и другой C++ код в вашей программе; в противном случае, сборка завершится ошибкой на последнем этапе компоновки. Если вы используете qmake, это делается автоматически. При каждом запуске qmake производится анализ заголовочных файлов проекта и генерируются make-правила для запуска moc для тех файлов, которые содержат макрос Q_OBJECT.

Написание make-правил для вызывания moc

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

Мы рекомендуем использовать для создания ваших make-файлов инструмент генерации make-файлов qmake. Этот инструмент генерирует make-файл, который выполнит всю необходимую обработку moc.

Если вы сами хотите создать make-файлы, то вот несколько советов о том как включить обработку moc.

Для объявлений класса Q_OBJECT в заголовочных файлах, здесь есть правило make-файла если используете только GNU make:

Если вы хотите написать переносимый код, вы можете использовать индивидуальные правила следующего вида:

Оба примера предполагают, что $(DEFINES) и $(INCPATH) расширяются для опций путей определений и включений, которые передаются компилятору C++. Это требуется moc для предварительной обработки исходных файлов.

В то время как мы предпочитаем именовать наши файлы исходного кода C++ .cpp, вы можете использовать любые другие расширения, например, .C, .cc, .CC, .cxx и .c++, если вы предпочитаете.

Для объявлений класса Q_OBJECT в реализации файлов (.cpp) мы предлагаем такое правило make-файла:

Этим гарантируется, что make запустит moc перед компиляцией foo.cpp. Затем вы можете поместить

в конце foo.cpp, где полностью известны все объявленные в этом файле классы.

Опции командной строки

Опции командной строки, поддерживаемые moc:

ОпцияОписание
-oЗаписывает вывод в файл вместо записи в стандартный вывод.
-f[ ]Задает генерацию оператора #include в вывод. Установлено по умолчанию для заголовочных файлов, расширения которых начинаются с H или h. Эта опция полезна, если у вас есть заголовочные файлы, в которых не соблюдаются стандартные соглашения именования. Часть является необязательной.
-iНе генерировать оператор #include в выводе. Её можно использовать для запуска moc с файлом C++, содержащим одно или более объявлений класса. Вы можете затем использовать #include чтобы включить мета-объектный код в файл .cpp.
-nwНе генерировать никаких предупреждений. (Не рекомендуется.)
-p

Вы можете явно указать moc не производить синтаксический разбор частей заголовочного файла. moc определяет идентификатор препроцессора Q_MOC_RUN. Любой код, окружённый конструкцией

будет пропущен moc'ом.

Выявление ошибок

moc предупредит вас о количестве опасных или недопустимых конструкций в объявлениях класса Q_OBJECT.

Если вы получили ошибки компоновки на завершающей фазе сборки вашей программы, скажем что YourClass::className() не определен или что в YourClass отсутствует vtable, то что-то сделано неправильно. Очень часто забывают скомпилировать или включить с помощью #include сгенерированный moc код C++, или (в первом случае) включить этот объектный файл в команду компоновки. Если вы используете qmake, попробуйте перезапустить его для обновления вашего make-файла. Будет сделано все, что нужно.

Ограничения

Другое ограничение заключается в том, что moc не расширяет макросы, поэтому вы не можете, например, использовать макрос для объявления сигнала/слота или использовать его для объявления базового класса для QObject.

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

Множественное наследование требует указания QObject первым

Если вы используете множественное наследование, то moc предполагает что первый класс, от которого идет наследование, является подклассом QObject. Также убедитесь, что только первый класс, от которого идет наследование, является QObject.

Виртуальное наследование с помощью QObject не поддерживается.

Указатели на функцию не могут быть параметрами сигналов и слотов

В большинстве случаев, когда вы обдумываете использование указателей на функцию в качестве параметров сигналов или слотов, мы думаем что наследование является более подходящим вариантом. Вот пример недопустимого синтаксиса:

Вы можете обойти это ограничение, например так:

Иногда будет даже лучше заменить указатель на функцию наследованием и виртуальными функциями.

Перечисления и переименования типов (Typedefs) должны быть полностью уточнены для параметров сигналов и слотов

Когда проверяются сигнатуры аргументов, QObject::connect() сравнивает типы данных посимвольно. Соответственно, Alignment и Qt::Alignment будут рассматриваться как два отдельных типа. Чтобы обойти это ограничение убедитесь, что полностью уточнили типы данных когда объявляли сигналы и слоты и когда создавали соединения. Например:

Тип макросов не может использоваться в качестве параметров для сигналов и слотов

Так как moc не расширяет директивы #define, тип макросов, который получает аргумент, не будет работать в сигналах и слотах. Вот некорректный пример:

Макрос без параметров будет работать.

Вложенные классы не могут содержать сигналы или слоты

Вот пример проблемной конструкции:

Возвращаемыми типами сигналов/слотов не могут быть ссылки

Сигналы и слоты могут возвращать типы, но возвращаемые ссылки на сигналы или на слоты будут трактоваться как возвращаемое пустое значение (void).

Только сигналы и слоты могут появляться в разделах класса signals и slots

moc будет жаловаться, если вы попытаетесь поместить в разделы класса signals или slots вместо сигналов и слотов другие конструкции.

Источник

Развенчание мифов о мета-объектном компиляторе Qt

Я часто встречаю критику фреймворка Qt, в которой ему пеняют использованием мета-объектного компилятора (утилиты moc). Как один из разработчиков moc, я решил написать данную статью с целью развенчать некоторые связанные с этим мифы.

Вступление

Moc — это один из инструментов разработчика и часть библиотеки Qt. Его задача — поддерживать расширение языка С++, необходимое для интроспекции и рефлексии в Qt (сюда относятся сигналы, слоты и QML). Для более детального объяснение вы можете почитать о том, как работают сигналы и слоты в Qt.

Необходимость использования moc является одним из главных объектов критики Qt. Это даже привело к появлению форков Qt, принципиально отказавшихся от moc (например, CopperSpice). Но всё-же большинство приписываемых moc так называемых недостатков не обоснованы.

Moc переписывает ваш код перед тем, как передать его компилятору

Это распространённое заблуждение. Moc не модифицирует и не переписывает ваш код. Он просто парсит часть кода для того, чтобы сгенерировать дополнительные С++ файлы, которые потом будут компилироваться независимо. Это не очень большое отличие, но всё-же важное техническое недопонимание.

Moc просто автоматически генерирует шаблонный код, который можно было бы долго и нудно писать вручную, если бы moc не существовало. Если вы мазохист, то и сейчас вполне возможно взять и написать самостоятельно все таблицы для интроспекции и реализации сигналов. Ну или положиться на надёжный инструмент, который сделает это за вас.

Используя Qt, вы не пишете на настоящем С++

Я слышал этот довод много раз, но он попросту неверен. Макросы, используемые moc для аннотации кода — это стандартные макросы С++. Они должны быть корректно распознаны любым инструментом, способным анализировать код на С++. Когда вы добавляете в код Q_OBJECT, то просто дописываете объявление нескольких функций. Когда вы пишете «signals:», то просто добавляете макрос, который превратится в «public:». Многие другие макросы Qt вообще ни во что не раскрываются. Moc просто находит их и генерирует код эмиттеров сигналов и таблиц интроспекции.

Тот факт, что ваш код теперь может быть прочитан ещё одним инструментом, не делает его «менее соответствующим стандарту С++». Вы же не считаете код, написанный с расчётом на использование gettext или doxygen каким-то «менее правильным С++»?

Moc усложняет процесс сборки кода

Если вы используете любую промышленную систему сборки кода, вроде CMake или qmake, то получаете нативную поддержку Qt. Даже с какой-то собственной системой сборки речь идёт о всего-лишь одном дополнительном запуске команды обработки заголовочных файлов. Все известные мне системы сборки позволяют добавлять шаги по запуску дополнительных команд перед запуском компилятора, поскольку многие проекты в том или ином виде используют генерацию кода при сборке проекта. Вспомните, например, инструменты вроде yacc/bison, gperf, llvm/TableGen.

Moc делает отладку сложнее

Поскольку moc генерирует код на чистом С++, то отладчики не должны иметь никаких проблем с ним. Мы стараемся поддерживать сгенерированный код в таком состоянии, чтобы он не вызывал предупреждений компиляторов или инструментов статического или динамического анализа кода. Да, иногда при отладке вы будете видеть в колстеке следы сгенерированного moc кода. В некоторых редких случаях вы можете получить ошибку, связанную с кодом, созданным moc, но обычно причины достаточно легко обнаружить. Код, сгенерированный moc, достаточно человекочитаем. Также его, вероятно, понимать и отлаживать даже проще, чем те печально известные ошибки некоторых библиотек, построенных на продвинутом использовании шаблонов.

Отказ от moc улучшает производительность на этапе выполнения кода

Это прямая цитата с главной страницы CopperSpice, и, вероятно, самая большая их ложь. Код, генерируемый moc, старается избегать динамических аллокаций и уменьшить количество реаллокаций памяти. Генерируемые moc таблицы являют собой константные массивы и хранятся в read-only сегменте данных. CopperSpice же регистрирует свои QMetaObject (информацию о сигналах, слотах и свойствах) на рантайме.

Milian Wolff проделал некоторые сравнения производительности Qt и CopperSpice в его докладе на CppCon2015. Вот скриншот одного из его слайдов (меньше — лучше).

что такое moc файл. Смотреть фото что такое moc файл. Смотреть картинку что такое moc файл. Картинка про что такое moc файл. Фото что такое moc файл

Также нужно отметить, что код на Qt даже с учётом запуска moc компилируется быстрее, чем код на CopperSpice.

Устаревшие мифы

Некоторая критика когда-то была справедливой, но более таковой не является.

До выхода Qt5 утилита moc действительно не раскрывала макросы. Но начиная с Qt 5.0 moc полностью поддерживает макросы во всех вышеперечисленных местах, так что это больше совершенно не является проблемой.

Перечисления (enums) и переопределения типов (typedefs) должны строго соответствовать при использовании их в качестве параметров сигналов и слотов

Это является проблемой только если вы всё ещё хотите использовать синтаксис соединений, основанный на строках (поскольку там действительно используется прямое сравнение названий типов). С выходом Qt5 и новым синтаксисом это больше не препятствие.

Q_PROPERTY не позволяет использовать запятые в типах

Q_PROPERTY это макрос с одним аргументом, который ни во что не раскрывается и служит лишь для помощи moc. Но, поскольку это всё ещё макрос, запятая в, например, QMap разделяет аргументы макроса и вызывает ошибку компиляции. Когда я увидел, как CopperSpice использует этот аргумент против Qt, то потратил 5 минут на то, чтобы исправить это с использованием variadic-макросов из стандарта С++11.

Другая критика

Шаблоны, вложенные классы или классы, используемые множественное наследование, не могут быть QObject-ами

Хотя это и является правдой, но эти возможности просто пока не поддерживаются QObject, хотя и вполне могут быть реализованы в moc, если мы этого захотим. Проект Qt в данный момент не считает данные фичи приоритетными.

Я однажды добавил поддержку шаблонных QObjects в moc, но это изменение не вошло в основную ветку разработки, поскольку никто больше не выразил интереса к данной функциональности.

Ещё нужно отметить поддержку шаблонных и вложенных классов в moc-ng.

Множественное наследование уже само по себе является очень неоднозначным. Чаще всего оно указывает на проблемы с архитектурой приложения и многие современные языки напрямую его запрещают. Вы всё ещё можете использовать множественное наследование с Qt, если если QObject является первым базовым классом в цепочке наследования. Это небольшое ограничение позволяет нам применять полезные оптимизации. Когда-нибудь задумывались почему qobject_cast настолько быстрее, чем dynamic_cast?

Выводы

Я не думаю, что moc — это проблема. Макросы Qt действительно помогают реализовать необходимую Qt функциональность. Сравнивая их с подходом CopperSpice мы можем заметить в последнем значительную избыточность служебного кода, а также недружелюбный синтаксис макросов (не говоря уже о потерях производительности на рантайме). Синтаксис сигналов и слотов, который существует в Qt с 90-ых годов — одна из фундаментальных вещей, обеспечивших успех фреймворка.

Вам может быть также интересно изучить некоторые эксперименты, связанные с moc, вроде moc-ng (это moc, переписанный с использованием библиотек clang). Также есть вот это исследование замены moc с помощью инструментов рефлексии С++. Ну и библиотека Verdigris, с макросами, создающими QMetaObject без moc.

Источник

Моки и явные контракты

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

Ниже представлен вольный перевод статьи, в которой José Valim — создатель языка Elixir — высказал своё мнение на проблему использования моков, с которым я полностью согласен.

Несколько дней назад я поделился своими мыслями по поводу моков в Twitter:

что такое moc файл. Смотреть фото что такое moc файл. Смотреть картинку что такое moc файл. Картинка про что такое moc файл. Фото что такое moc файл

Мок — полезный инструмент в тестировании, но имеющиеся тестовые библиотеки и фреймворки зачастую приводят к злоупотреблению этим инструментом. Ниже мы рассмотрим лучший способ использования моков.

Что такое мок?

Воспользуемся определением из англоязычной википедии: мок — настраиваемый объект, который имитирует поведение реального объекта. Я сделаю акцент на этом позже, но для меня мок — это всегда существительное, а не глагол [для наглядности, глагол mock везде будет переводиться как "замокать" — прим. перев.].

На примере внешнего API

Давайте рассмотрим стандартный пример из реальной жизни: внешнее API.

Представьте, что вы хотите использовать Twitter API в веб-приложении на фреймворке Phoenix или Rails. В приложение приходит запрос, который перенаправляется в контроллер, который, в свою очередь, делает запрос к внешнему API. Вызов внешнего API происходит прямо в контроллере:

Далее вы используете такой же подход в других частях приложения и добиваетесь прохождения юнит и интеграционных тестов. Время двигаться дальше?

Кроме того, так как мок, показанный выше, изменяет модуль глобально, вы больше не сможете запустить эти тесты в Elixir параллельно.

Решение

В Elixir все приложения имеют конфигурационные файлы и механизм для их чтения. Используем этот механизм, чтобы настроить клиент Twitter'a для различных окружений. Код контроллера теперь будет выглядеть следующим образом:

Соответствующие настройки для различных окружений:

Сейчас мы можем выбрать лучшую стратегию получения данных из Twitter для каждого из окружений. Sandbox может быть полезен, если Twitter предоставляет какой-нибудь sandbox для разработки. Наша замоканная версия HTTPClient позволяла избежать реальных HTTP-запросов. Реализация этой же функциональности в данном случае:

Код получился простым и чистым, а сильной внешней зависимости от HTTPClient больше нет. MyApp.Twitter.InMemory является моком, то есть существительным, и для его создания вам не нужны никакие библиотеки!

Необходимость явных контрактов

Мок предназначен для замены реального объекта, а значит будет эффективен только в том случае, когда поведение реального объекта определено явно. Иначе, вы можете оказаться в ситуации, когда мок начнет становиться все сложнее, увеличивая зависимость между тестируемыми компонентами. Без явного контракта заметить это будет сложно.

Мы уже имеем три реализации Twitter API и лучше сделать их контракты явными. В Elixir описать явный контракт можно с помощью behaviour:

Теперь добавьте @behaviour MyApp.Twitter в каждый модуль, который реализует этот контракт, и Elixir поможет вам создать ожидаемый API.

В Elixir мы полагаемся на такие behaviours постоянно: когда используем Plug, когда работаем с базой данных в Ecto, когда тестируем Phoenix channels и так далее.

Тестирование границ

Сначала, когда явные контракты отсутствовали, границы приложения выглядели так:

Поэтому изменение HTTPClient могло приводить к падению интеграционных тестов. Сейчас приложение зависит от контракта и только одна реализация этого контракта работает с HTTP:

Сложность тестирования больших систем заключается в определение четких границ между компонентами. Слишком высокий уровень изоляции при отсутствии интеграционных тестов сделает тесты хрупкими, а большинство проблем будут обнаруживаться только на production. С другой стороны, низкий уровень изоляции увеличит время прохождения тестов и сделает тесты сложными в сопровождении. Единственного верного решения нет, и уровень изоляции будет изменяться в зависимости от уверенности команды и прочих факторов.

Лично я бы протестировал MyApp.Twitter.HTTP на реальном Twitter API, запуская эти тесты по-необходимости во время разработки и каждый раз при сборке проекта. Система тегов в ExUnit — библиотеке для тестирования в Elixir — реализует такое поведение:

Исключим тесты с Twitter API:

При необходимости включим их в общий тестовый прогон:

Также можно запустить их отдельно:

Вместо создания мока HTTPClient можно поднять dummy-сервер, который будет эмулировать Twitter API. bypass — один из проектов, который может в этом помочь. Все возможные варианты вы должны обсудить со своей командой.

Примечания

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

Создание "тестируемого" кода

Получается, предложенное решение делает production-код более "тестируемым", но создает необходимость ходить в конфигурацию приложения на каждый вызов функции? Наличие ненужного оверхэда, чтобы сделать что-то "тестируемым", не похоже на хорошее решение.

Я бы сказал, что речь идет не о создании "тестируемого" кода, а об улучшении дизайна [от англ. design of your code — прим. перев.].

Тест — это пользователь вашего API, как и любой другой код, который вы пишите. Одна из идей TDD заключается в том, что тесты — это код и ничем не отличаются от кода. Если вы говорите: "Я не хочу делать мой код тестируемым", это означает "Я не хочу уменьшать зависимость между компонентами" или "Я не хочу думать о контракте (интерфейсе) этих компонентов".

Нет ничего плохого в нежелании уменьшать зависимость между компонентами. Например, если речь идет о модуле работы с URI [имеется ввиду модуль URI для Elixir — прим. перев.]. Но если мы говорим о чем-то таком же сложном, как внешнее API, определение явного контракта и наличие возможности заменять реализацию этого контракта сделает ваш код удобным и простым в сопровождении.

Кроме того, оверхэд минимален, так как конфигурация Elixir-приложения хранится в ETS, а значит вычитывается прямо из памяти.

Локальные моки

Хотя мы и использовали конфигурацию приложения для решения проблемы с внешним API, иногда проще передать зависимость как аргумент. Например, некоторая функция выполняет долгие вычисления, которые вы хотите изолировать в тестах:

Можно избавиться от зависимости, передав её как аргумент. В данном случае будет достаточно передачи анонимной функции:

Тест будет выглядеть следующим образом:

Или, как было описано ранее, можно определить контракт и передать модуль целиком:

Вы также можете представить зависимость в виде data structure и определить контракт с помощью protocol.

Мок — это существительное

Лучше думать о моках как о существительных. Вместо того, чтобы мокать API (мокать — глагол), нужно создать мок (мок — существительное), который реализует необходимый API.

Библиотеки для создания моков

После прочитанного у вас может возникнуть вопрос: "Нужно ли отказываться от библиотек для создания моков?"

Все зависит от ситуации. Если библиотека подталкивает вас на подмену глобальных объектов (или на использование моков в качестве глаголов), изменение статических методов в объектно-ориентированном или замену модулей в функциональном программировании, то есть на нарушение описанных выше правил создания моков, то вам лучше отказаться от неё.

Однако, есть библиотеки для создания моков, которые не подталкивают вас на использование описанных выше анти-паттернов. Такие библиотеки предоставляют "мок-объекты" или "мок-модули", которые передаются в тестируемую систему в качестве аргумента и собирают информацию о количестве вызовов мока и о том, с какими аргументами он был вызван.

Заключение

Одна из задач тестирования системы — нахождение правильных контрактов и границ между компонентами. Использование моков только в случае наличия явного контракта позволит вам:

Явные контракты позволяют увидеть сложность зависимостей в вашем приложении. Сложность присутствует в каждом приложении, поэтому всегда старайтесь делать её настолько явной, насколько это возможно.

Источник

Когда использовать mocks в юнит-тестировании

Эта статья является переводом материала «When to Mock».

Использование моков в модульном тестировании является спорной темой. Автор оригинала заметил, что на протяжении всей своей карьеры в программировании он сначала перешел от моков почти над каждой зависимостью к политике «без моков», а затем к «только моки для внешних зависимостей».

Ни одна из этих практик не является достаточно хорошей. В этой статье Владимир Хориков покажет, какие зависимости следует мокать, а какие использовать как есть в тестах.

Что такое mock (мок, от англ. «пародия», «имитация»)?

Прежде чем перейти к теме того, когда использовать моки, давайте обсудим, что такое мок. Люди часто используют термины тестовый двойник (test double) и мок (mock) как синонимы, но технически это не так:

Мок – это лишь один из видов таких зависимостей.

Согласно Жерару Месарошу, существует 5 видов тестовых двойников:

Такое разнообразие может показаться пугающим, но на самом деле все они могут быть сгруппированы всего в два типа: моки и стабы.

Разница между этими двумя типами сводится к следующему:

Моки помогают имитировать и изучать исходящие (outcoming) взаимодействия. То есть вызовы, совершаемые тестируемой системой (SUT) к ее зависимостям для изменения их состояния.

Стабы помогают имитировать входящие (incoming) взаимодействия. То есть вызовы, совершаемые SUT к ее зависимостям для получения входных данных.

Извлечение данных из БД является входящим (incoming) взаимодействием — оно не приводит к побочному эффекту. Соответствующий тестовый двойник является стабом.

Все остальные различия между пятью типами тестовых двойников являются незначительными деталями реализации:

Spies (шпионы) выполняют ту же роль, что и моки. Отличие в том, что spies пишутся вручную, а моки создаются с помощью готовых инструментов. Иногда spies называют рукописными моками.

С другой стороны, разница между стабами, dummy (пустышками) и фейками заключается в том, насколько они умны:

Стаб посложнее. Это полноценная зависимость, которую вы настраиваете для возврата разных значений для разных сценариев.

Обратите внимание на разницу между моками и стабами (помимо исходящих и входящих взаимодействий). Моки помогают эмулировать и изучать взаимодействия между SUT и его зависимостями, в то время как стабы помогают только эмулировать эти взаимодействия. Это важное различие.

Мок-как-инструмент vs. мок-как-тестовый-двойник

Но у термина мок есть и другое значение. Вы также можете ссылаться на классы из библиотек (для создания моков) как на моки. Эти классы помогают вам создавать настоящие моки, но сами по себе они не являются моками как таковыми:

Важно не смешивать мок-как-инструмент с мок-как-тестовый-двойник, потому что вы можете использовать мок-как-инструмент для создания обоих типов тестовых двойников: моков и стабов.

Не проверяйте взаимодействия со стабами

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

В приведенных выше примерах проверка

В то же время вызов GetNumberOfUsers() вообще не является результатом. Это внутренняя деталь реализации, касающаяся того, как SUT собирает данные, необходимые для создания отчета. Следовательно, проверка этого вызова приведет к уязвимости теста. Неважно, как SUT генерирует конечный результат, если этот результат правильный.

Вот пример такого хрупкого теста:

Такая практика проверки того, что не является частью конечного результата, также называется чрезмерной спецификацией.

Совместное использование моков и стабов

Иногда нужно создать тестовый двойник, который проявляет свойства как мока, так и стаба:

Этот тест использует storeMock для двух целей: он возвращает шаблонный ответ и проверяет вызов метода, сделанный SUT.

Однако обратите внимание, что это два разных метода: тест устанавливает ответ от HasEnoughInventory(), но затем проверяет вызов RemoveInventory(). Таким образом, здесь не нарушается правило не проверять взаимодействия со стабами.

Mocks vs. stubs и commands vs. queries

Понятие моков и стабов связано с принципом command-query separation (CQS). Принцип CQS гласит, что каждый метод должен быть либо командой, либо запросом, но не обоими:

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

Посмотрите еще раз на два теста из предыдущих примеров:

Когда мокать?

Разобравшись со всеми этими определениями, давайте поговорим о том, когда вам следует использовать моки.

Очевидно, что вы не хотите мокать саму тестируемую систему (SUT), поэтому вопрос «Когда мокать?» сводится к следующему: «Какие типы зависимостей вы должны заменять на моки, а какие использовать в тестах?»

Вот все типы зависимостей модульного тестирования, которые автор оригинала перечислил в своей предыдущей статье:

Совместная зависимость соответствует изменяемой внепроцессорной зависимости (mutable out-of-process dependency) в подавляющем большинстве случаев, поэтому автор оригинала использует здесь эти два понятия как синонимы. (Ознакомьтесь с предыдущим постом Владимира Хорикова, чтобы узнать больше: Unit Testing Dependencies: The Complete Guide.)

Существуют две школы модульного тестирования с собственными взглядами на то, какие типы зависимостей следует заменять на моки:

Лондонская школа (также известная как школа mockist) выступает за замену всех изменяемых зависимостей на моки.

Классическая школа (также известная как школа Детройта) выступает за замену только общих (изменяемых внепроцессорных) зависимостей.

Обе школы ошибаются в своем отношении к мокам, хотя классическая школа меньше, чем лондонская.

Моки и неизменяемые внепроцессорные зависимости.

А как насчет иммутабельных внепроцессорных (immutable out-of-process) зависимостей? Разве их не стоит мокать, по крайней мере, по мнению одной из школ? Неизменяемые внепроцессорные зависимости (например, служба API только для чтения) следует заменить тестовым двойником, но этот тестовый двойник будет стабом, а не моком.

Это опять же из-за различий между моками и стабами:

Взаимодействия с иммутабельными внепроцессорными зависимостями по определению являются входящими и, следовательно, не должны проверяться в тестах, а только заменяться шаблонными ответами (обе школы с этим согласны).

Не мокайте все изменяемые зависимости

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

Внутрисистемные коммуникации являются деталями реализации, поскольку взаимодействие, через которое проходят ваши доменные классы для выполнения операции, не является частью их наблюдаемого поведения. Это взаимодействие не имеет непосредственной связи с целью клиента. Таким образом, связь с таким взаимодействием приводит к хрупким тестам.

Это свойство межсистемных коммуникаций проистекает из того, как отдельные приложения развиваются вместе. Один из основных принципов такой эволюции – обеспечение обратной совместимости. Независимо от рефакторинга, который вы выполняете внутри своего приложения, паттерн взаимодействия, который оно использует для взаимодействия с внешними приложениями, должен всегда оставаться на месте, чтобы внешние приложения могли его понять. Например, сообщения, отправляемые вашим приложением по шине, должны сохранять свою структуру и так далее.

Использование моков закрепляет контракт взаимодействия между тестируемой системой и зависимостью. Это именно то, что вам нужно при проверке связи между вашей системой и внешними приложениями. И наоборот, использование моков для проверки связи между классами внутри вашей системы связывает ваши тесты с деталями реализации, делая их хрупкими. Внутрисистемные связи соответствуют изменяемым внутрипроцессорным зависимостям:

Итак, лондонская школа ошибается, потому что она поощряет использование моков для всех изменяемых зависимостей и не проводит различия между внутрисистемными (внутрипроцессорными) и межсистемными (внепроцессорными) коммуникациями.

В результате тесты проверяют связь между классами точно так же, как они проверяют связь между вашим приложением и внешними системами. Это неизбирательное использование моков является причиной того, что следование лондонской школе часто приводит к хрупким тестам — тестам, которые связаны с деталями реализации.

Не мокайте все внепроцессорные зависимости

Классическая школа лучше справляется с этим вопросом, потому что она выступает за замену только внепроцессорных зависимостей, таких как служба SMTP, шина сообщений и т. д. Но классическая школа также не идеальна по отношению к межсистемным коммуникациям. Эта школа также поощряет чрезмерное использование моков, хотя и не так сильно, как лондонская школа.

Не все внепроцессорные зависимости следует мокать. Если внепроцессорная зависимость доступна только через ваше приложение, то связь с такой зависимостью не является частью наблюдаемого поведения вашей системы. Внепроцессорная зависимость, которая не может наблюдаться извне, по сути, действует как часть вашего приложения. Связи с такой зависимостью становятся деталями реализации: они не должны оставаться неизменными после рефакторинга и, следовательно, не должны проверяться с помощью моков.

Помните, что требование всегда сохранять схему взаимодействия между вашим приложением и внешними системами проистекает из необходимости поддерживать обратную совместимость. Вы должны поддерживать способ взаимодействия вашего приложения с внешними системами. Это потому, что вы не можете изменять эти внешние системы одновременно с вашим приложением; они могут следовать другому циклу развертывания, или вы можете просто не контролировать их.

Но когда ваше приложение действует как прокси-сервер для внешней системы, и ни один клиент не может получить к ней прямой доступ, требование обратной совместимости исчезает. Теперь вы можете развернуть свое приложение вместе с этой внешней системой, и это не повлияет на клиентов. Схема взаимодействия с такой системой становится деталью реализации.

Хорошим примером здесь является БД приложения: БД, которая используется только вашим приложением. Ни одна внешняя система не имеет доступа к этой БД. Таким образом, вы можете изменить схему взаимодействия между вашей системой и БД приложения любым удобным вам способом, если это не ломает существующую функциональность. Поскольку эта БД полностью скрыта от глаз клиентов, вы даже можете заменить ее совершенно другим механизмом хранения, и никто этого не заметит.

Использование моков для внепроцессорных зависимостей, которые вы полностью контролируете, также приводит к хрупким тестам. Вы не хотите, чтобы ваши тесты становились красными каждый раз, когда вы разбиваете таблицу в БД или изменяете тип одного из параметров в хранимой процедуре. БД и ваше приложение должны рассматриваться как одна система.

Это различие разделяет внепроцессорные зависимости на две подкатегории:

Управляемые (managed) зависимости — внепроцессорные зависимости, над которыми вы имеете полный контроль. Эти зависимости доступны только через ваше приложение; взаимодействия с ними не видны внешнему миру. Типичным примером является БД приложения. Внешние системы не имеют прямого доступа к вашей БД; они делают это через API, предоставляемый вашим приложением.

Только неуправляемые зависимости должны быть заменены моками. Используйте реальные экземпляры управляемых зависимостей в тестах.

Резюме

Существует пять вариантов тестовых двойников — dummy (манекен), стаб, spy (шпион), мок и фейк, которые можно сгруппировать всего в два типа: моки и стабы.

Spies функционально такие же, как и моки; dummy и фейки выполняют ту же роль, что и стабы.

Различия между моками и стабами:

Моки помогают имитировать и изучать исходящие взаимодействия: вызовы от SUT к его зависимостям, которые изменяют состояние этих зависимостей.

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

Проверка взаимодействия со стабами всегда приводит к хрупким тестам.

Тестовые двойники, заменяющие команды CQS, являются моками. Тестовые двойники, заменяющие запросы CQS, являются стабами.

Внепроцессорные зависимости можно разделить на 2 подкатегории: управляемые и неуправляемые зависимости.

Используйте реальные экземпляры управляемых зависимостей в интеграционных тестах; замените неуправляемые зависимости моками.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *