Как вы вводите модульное тестирование в большую, устаревшую (C/С++) кодовую базу?

У нас есть большое многоплатформенное приложение, написанное на C. (с небольшим, но растущим количеством С++). На протяжении многих лет он эволюционировал со многими функциями, которые можно было бы ожидать в большом приложении C/С++:

  • #ifdef ад
  • Большие файлы, из-за которых трудно изолировать тестируемый код.
  • Функции, которые являются слишком сложными, легко проверяются

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

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

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

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

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

Лично я предпочел бы, чтобы разработчики думали об их внешних зависимостях и грамотно писали свои собственные заглушки. Но это может быть огромным, чтобы заглушить все зависимости для ужасно заросшего, 10 000 строк файла. Возможно, было бы сложно убедить разработчиков, что им нужно поддерживать заглушки для всех внешних зависимостей, но это правильный способ сделать это? (Еще один аргумент, который я слышал, заключается в том, что разработчик подсистемы должен поддерживать заглушки для своей подсистемы. Но мне интересно, могут ли разработчики "форсировать" писать свои собственные заглушки, чтобы улучшить модульное тестирование?)

#ifdefs, конечно, добавьте еще одну целую размерность в проблему.

Мы рассмотрели несколько основанных на C/С++ фреймворков unit test, и есть много вариантов, которые выглядят хорошо. Но мы ничего не нашли, чтобы облегчить переход от "hairball кода без единичных тестов" к "единичному тестируемому коду".

Итак, вот мои вопросы всем, кто прошел через это:

  • Что является хорошей отправной точкой? Мы идем в правильном направлении, или нам не хватает чего-то очевидного?
  • Какие инструменты могут быть полезны для перехода? (предпочтительно свободный/открытый источник, поскольку наш бюджет сейчас примерно равен нулю)

Обратите внимание: наша среда сборки основана на Linux/UNIX, поэтому мы не можем использовать какие-либо инструменты только для Windows.

Ответ 1

мы не нашли ничего, чтобы облегчить переход от "hairball of код без модульных тестов" на "код, проверяемый модулем".

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

Нет простого перехода. У вас большая, сложная, серьезная проблема.

Вы можете решить ее только небольшими шагами. Каждый крошечный шаг включает в себя следующее.

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

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

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

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

  • Напишите модульные тесты для новых блоков.

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

Итерация.

Ответ 3

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

Однако иногда бывает трудно создать модульные тесты (даже тесты характеристик). В этом случае я атакую ​​проблему с помощью приемочных тестов (Fitnesse в этом случае).

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

Ответ 4

Как сказал Джордж, "Эффективная работа с устаревшим кодом - это библия для такого рода вещей".

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

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

Потратьте некоторое время на непрерывную интеграцию, используя круиз-контроль, luntbuild, cdash и т.д. Если ваш код автоматически компилируется каждую ночь и тесты запускаются, разработчики начнут видеть преимущества, если юнит-тесты поймают ошибки до qa.

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

Большинство разработчиков пишут какую-либо форму unit test, иногда небольшую часть кода выброса, которую они не проверяют или не интегрируют в сборку. Внесите их в сборку легко, и разработчики начнут покупать.

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

Единственное место, которое я настаиваю на модульных тестах, - это код платформы. Если #ifdefs заменяет функции/классы более высокого уровня для платформы, они должны быть протестированы на всех платформах с теми же тестами. Это экономит массу времени, добавляя новые платформы.

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

Они завернуты в CTest (часть CMake), которая запускает группу исполняемых файлов единичных тестов сразу и генерирует простой отчет.

Наша ночная сборка автоматизирована с помощью ant и luntbuild (ant cues С++,.net и java builds)

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

Ответ 5

Мы сейчас делаем именно это. Три года назад я присоединился к команде разработчиков по проекту без каких-либо модульных тестов, почти без отзывов о кодах и довольно ad-hoc-процесса сборки.

База кода состоит из набора COM-компонентов (ATL/MFC), кросс-платформенного картриджа данных С++ Oracle и некоторых компонентов Java, все из которых используют кросс-платформенную базовую библиотеку С++. Некоторым из кода почти десять лет.

Первым шагом было добавление некоторых модульных тестов. К сожалению, поведение очень основано на данных, поэтому началось несколько усилий по созданию структуры unit test (первоначально CppUnit, теперь распространяемой на другие модули с JUnit и NUnit), которая использует тестовые данные из базы данных. Большинство начальных тестов были функциональными испытаниями, которые занимались самыми внешними слоями, а не модульными испытаниями. Вероятно, вам придется потратить некоторое усилие (которое вам может потребоваться для бюджета) для внедрения тестового жгута.

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

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

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

Ответ 6

Я работал над проектом Green field с полностью проверенными модулями кода и большими приложениями на С++, которые выросли на протяжении многих лет и со многими разными разработчиками.

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

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

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

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

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

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

Ответ 7

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

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

PS: Рассмотрите возможность создания управляемой командами симуляции, возможно, построенной на Python или Tcl. Это позволит вам легко тестировать script...

Ответ 8

G'day,

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

Затем начните смотреть, как код был выложен. Это логично? Возможно, начните разбивать большие файлы на более мелкие.

Возможно, возьмите копию книги Джона Лакоса "Крупномасштабная разработка программного обеспечения на C++" (санированная ссылка Amazon), чтобы получить некоторые идеи о как это должно быть выложено.

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

Выберите хорошую платформу, мне понравятся CUnit и CPPUnit и оттуда.

Это будет длинный, медленный путь, хотя.

НТН

веселит,

Ответ 9

Его гораздо проще сделать его более модульным. Вы не можете действительно увязать что-то с большим количеством зависимостей. Когда рефактору сложно вычислить. Вы действительно должны взвесить затраты и риски против преимуществ. Является ли этот код тем, что будет многократно использоваться? Или этот код действительно не изменится. Если вы планируете продолжать использовать его, вы, вероятно, захотите реорганизовать.

Похоже, что вы хотите реорганизовать. Вам нужно начать с прорыва самых простых утилит и опираться на них. У вас есть ваш модуль C, который делает вещи. Может быть, например, там есть какой-то код, который всегда форматирует строки определенным образом. Возможно, это может быть выпущено автономным модулем утилиты. У вас есть новый модуль форматирования строк, вы сделали код более удобочитаемым. Это уже улучшение. Вы утверждаете, что находитесь в ситуации 22. На самом деле это не так. Просто перемещая вещи, вы сделали код более читабельным и поддерживаемым.

Теперь вы можете создать unittest для этого разбитого модуля. Вы можете сделать это несколькими способами. Вы можете создать отдельное приложение, которое просто включает в себя ваш код и запускает кучу дел в основной подпрограмме на вашем ПК или может определить статическую функцию под названием "UnitTest", которая выполнит все тестовые примеры и вернет "1", если они пройдут. Это может быть выполнено на цели.

Возможно, вы не можете пойти на 100% с этим подходом, но это начало, и это может заставить вас увидеть другие вещи, которые можно легко разбить на тестируемые утилиты.

Ответ 10

Я думаю, в основном у вас есть две отдельные проблемы:

  • База большого кода для рефакторинга
  • Работа с командой

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

Выполнение такой задачи с командой утомительно. Я сильно сомневаюсь, что "заставить" разработчиков "когда-либо работать. Идеи Iains очень хорошие, но я бы подумал о том, чтобы найти одного или двух программистов, которые могут и хотят" очистить" источники: Refactor, Modualrize, ввести Unit Tests и т.д. Пусть эти люди выполняют эту работу, а другие внедряют новые ошибки, функции aehm. Только люди, которые как, будут работать с этой работой.

Ответ 11

Сделать тесты легко.

Я бы начал с того, что "запускает автоматически" на место. Если вы хотите, чтобы разработчики (включая вас самих) записывали тесты, упрощали их запуск и видели результаты.

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

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

Проверьте, что можно протестировать

Я знаю, что я бегу против общей философии Unit Test здесь, но это то, что я делаю: пишите тесты на те, которые легко тестировать. Я не беспокоюсь насмешкой, я не рефакторинг, чтобы сделать его проверяемым, и если есть пользовательский интерфейс, у меня нет unit test. Но все больше и больше моих подпрограмм библиотеки есть.

Я очень поражен тем, что простые тесты могут найти. Сбор низко висящих фруктов ни в коем случае не бесполезен.

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

(Однако вам нужно сделать это. Не попадайте в ловушку "исправления всего" вокруг вашего процесса сборки.)

Узнайте, как улучшить базу кода

Любая база кода с этой историей кричит об улучшениях, что точно. Однако вы никогда не будете реорганизовывать все это.

Глядя на две части кода с одинаковой функциональностью, большинство людей могут согласиться, какой из них "лучше" в данном аспекте (производительность, читаемость, ремонтопригодность, тестируемость и т.д.). Жесткие части - три:

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

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


Ответ 12

Существует философский аспект.

Вам действительно нужен проверенный, полностью функциональный, аккуратный код? Это ВАША цель? У вас вообще есть какая-то польза от этого?

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

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

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

в конце концов это управление, которое создает рабочее место, а не код.