Сегодня многие люди продают модульное тестирование как хлеб с маслом для разработки. Это может сработать даже для программ, строго ориентированных на алгоритмы. Однако как бы вы протестировали, например, распределитель памяти (подумайте о malloc () / realloc () / free ()). Нетрудно создать работающий (но абсолютно бесполезный) распределитель памяти, удовлетворяющий заданному интерфейсу. Но как обеспечить надлежащий контекст для функциональности модульного тестирования, которая абсолютно желательна, но не является частью контракта: объединение свободных блоков, повторное использование свободных блоков при следующих выделениях, возврат избыточной свободной памяти в систему, утверждение, что политика выделения (например, first-fit) действительно уважают и т. д.
По моему опыту, утверждения, даже если они сложны и отнимают много времени (например, просмотр всего списка свободных мест для проверки инвариантов), требуют гораздо меньше работы и более надежны, чем модульное тестирование, особенно. при кодировании сложных, зависящих от времени алгоритмов.
Какие-нибудь мысли?





Обе вещи имеют свое место. Используйте модульные тесты, чтобы проверить поведение интерфейсов, как ожидалось, и утверждения, чтобы убедиться, что контракт соблюдается.
Если там есть какая-то логика, ее можно протестировать на единицу.
Если ваша логика включает в себя принятие решений и вызов API ОС / оборудования / системы, подделайте / имитируйте вызовы, зависящие от устройства, и выполните модульное тестирование своей логики, чтобы проверить, принимаются ли правильные решения при заданном наборе предварительных условий. Следуйте триаде Arrange-Act-Assert в своем модульном тесте.
Утверждения не заменяют автоматические модульные тесты. Они не говорят вам, какой сценарий провалился, они не предоставляют обратной связи во время разработки, они не могут использоваться, чтобы доказать, что код, среди прочего, соблюдает все спецификации.
Непонятное обновление: Я не знаю точных вызовов методов .. Думаю, я сам покатаюсь Допустим, ваш код изучает текущие условия, принимает решение и при необходимости обращается к ОС. Допустим, ваши вызовы ОС (у вас может быть намного больше):
void* AllocateMemory(int size);
bool FreeMemory(void* handle);
int MemoryAvailable();
Сначала превратите это в интерфейс I_OS_MemoryFacade. Создайте реализацию этого интерфейса для фактических вызовов ОС. Теперь заставьте свой код использовать этот интерфейс - теперь вы отделили свой код / логику от устройства / ОС. Затем в модульном тесте вы используете фиктивную структуру (ее цель - дать вам фиктивную реализацию указанного интерфейса. Затем вы можете сообщить фиктивной структуре, что ожидать эти вызовы должны быть выполнены, с этими параметрами и вернуть это, когда они В конце теста вы можете попросить макет фреймворка проверить, все ли ожидания оправдались. (например, в этом тесте AllocateMemory должен вызываться трижды с 10, 30, 50 в качестве параметров, за которыми следуют 3 вызова FreeMemory. Проверить если MemoryAvailable возвращает начальное значение.)
Поскольку ваш код зависит от интерфейса, он не знает разницы между реальной реализацией и поддельной / имитацией реализации, которую вы используете для тестирования.
Для получения дополнительной информации по этому поводу погуглите «макеты фреймворков».
Ваш ответ слишком абстрактен, чтобы быть полезным. Не могли бы вы привести пример того, как бы вы провели модульное тестирование распределителя памяти? Давайте сделаем это еще более конкретным: first-fit, который возвращает 50% свободных блоков в систему, когда их вдвое больше, чем выделенной памяти.
Первым шагом было бы, чтобы ваш распределитель памяти получал и высвобождал память из системы через интерфейс, а не напрямую через прямые системные вызовы. Шаг второй - создать фиктивную версию этого интерфейса, которую вы можете использовать в своих тестах. Шаг третий - протестировать и проверить, как код использует интерфейс.
Я расширил свой ответ до «имеет смысл» :)
Лично я нахожу большинство юнит-тестов скорее чьим-то желанием, чем моим. Я думаю, что любой модульный тест должен быть написан так же, как обычная программа, за исключением того факта, что он не делает ничего, кроме тестирования библиотеки / алгоритма или любой части кода.
В моих модульных тестах обычно не используются такие инструменты, как CUnit, CppUnit и подобное программное обеспечение. Создаю свои тесты. Например, не так давно мне нужно было протестировать свежую реализацию контейнера в обычных случаях на утечки памяти. Модульный тест был недостаточно полезен для обеспечения хорошего теста. Вместо этого я создал свой собственный распределитель и заставил его не выделять память после определенного (регулируемого) количества выделений, чтобы увидеть, есть ли в моем приложении утечки памяти в этом случае (а это было :)).
Как это можно сделать с помощью модульного теста? Приложите больше усилий, чтобы ваш код вписался в «шаблон» модульного теста.
Поэтому я настоятельно рекомендую не использовать модульные тесты каждый раз только потому, что это «модно», а только тогда, когда их действительно легко интегрировать с кодом, который вы хотите тестировать.
Код с высокой степенью тестирования имеет тенденцию быть структурированным иначе, чем другой код.
Вы описываете несколько задач, которые должен выполнять распределитель:
Хотя вы можете написать свой код распределения так, чтобы он был очень взаимосвязанным, например, выполняя некоторые из этих вещей внутри одного тела функции, вы также можете разбить каждую задачу на код, который является тестируемым фрагментом. Это почти инверсия того, к чему вы, возможно, привыкли. Я считаю, что тестируемый код имеет тенденцию быть очень прозрачным и состоит из более мелких частей.
Далее, я бы сказал, что в разумных пределах автоматическое тестирование любого вида лучше, чем отсутствие автоматизированного тестирования. Я бы определенно больше сосредоточился на том, чтобы ваши тесты делали что-то полезное, чем беспокоиться о том, правильно ли вы использовали макеты, обеспечили ли вы его должную изоляцию и является ли это истинным модульным тестом. Все это замечательные цели, которые, как мы надеемся, улучшат 99% тестов. С другой стороны, используйте здравый смысл и лучшие инженерные решения для выполнения работы.
Не думаю, что без примеров кода я могу быть более конкретным.
Вы также можете включить тестирование производительности, нагрузочное тестирование и т. д. Это не будут модульные тесты, потому что они будут тестировать все, но они очень полезны в случае распределителя памяти.
Модульное тестирование не исключает подобных тестов. Лучше иметь их обоих.
Я также считаю, что модульные тесты переоценены. В них есть своя полезность, но что действительно повышает качество программы, так это ее пересмотр. С другой стороны, мне очень нравятся утверждения, но они не заменяют модульное тестирование.
Я не говорю о экспертной оценке, а просто перечитайте то, что вы написали, возможно, пройдя через это с помощью отладчика и проверяя, что каждая строка выполняет то, что она должна делать, повышает качество программного обеспечения.
Я бы рекомендовал «высокоуровневые» модульные тесты, которые проверяют фрагмент функциональности, а не вызов крошечного метода. Последние, как правило, делают любое изменение кода чрезвычайно болезненным и дорогостоящим.
Модульное тестирование - это не только проверка работоспособности вашего кода. Это также очень хорошая методология проектирования. Чтобы тесты были полезными, как упоминалось ранее, код должен быть максимально изолирован, например, использовать интерфейсы там, где это необходимо.
Я не всегда сначала пишу тесты, но очень часто, если у меня возникают проблемы с началом чего-либо, я напишу простой тест, поэкспериментирую с дизайном и продолжу. Кроме того, хорошие модульные тесты служат хорошей документацией. На работе, когда мне нужно увидеть, как использовать определенный или аналогичный класс, я смотрю на него модульные тесты.
Просто помните, что модульное тестирование - это интеграционное тестирование нет. У модульного тестирования есть свои ограничения, но в целом я считаю, что это очень хороший инструмент, который нужно уметь правильно использовать.
Таким образом, вы сталкиваетесь с проблемой, что ваш распределитель используется фреймворком тестирования, что может вызвать проблемы с состоянием вашего распределителя, пока вы пытаетесь протестировать. Рассмотрите возможность добавления префиксов к функциям распределителя (см. dlmalloc). Ты пишешь
prefix_malloc();
prefix_free();
а потом
#ifndef USE_PREFIX
#define prefix_malloc malloc
#define prefix_free free
#endif
Теперь настройте вашу систему сборки на компиляцию версии библиотеки с помощью -DUSE_PREFIX. Напишите свои модульные тесты для вызова prefix_malloc и prefix_free. Это позволяет вам отделить состояние вашего распределителя от состояния системного распределителя.
Если вы используете sbrk, а системный распределитель использует sbrk, возможно, у вас могут быть плохие времена, если какой-либо из распределителей предполагает, что он полностью контролирует точку останова. В этом случае вам нужно связать еще один распределитель, который вы можете настроить для использования только mmap, чтобы ваш распределитель мог иметь точку останова.
Не могли бы вы подробнее рассказать, как писать модульные тесты. Вы много их отстаиваете, но я прочитал вопрос, а затем ваш ответ и до сих пор не понимаю, что именно вы предлагаете.