Как вы вводите модульное тестирование в большой, унаследованной (C / C ++) кодовой базе? - PullRequest
72 голосов
/ 14 апреля 2009

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

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

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

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

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

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

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

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

#ifdefs, конечно, добавляет еще одно целое измерение к проблеме.

Мы рассмотрели несколько основ модульных тестов на основе C / C ++, и есть много вариантов, которые выглядят хорошо. Но мы не нашли ничего, что могло бы облегчить переход от «шарика кода без юнит-тестов» к «юнит-тестируемому коду».

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

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

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

Ответы [ 12 ]

48 голосов
/ 14 апреля 2009

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

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

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

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

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

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

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

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

  5. Написать юнит-тесты для новых юнитов.

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

Перебор.

25 голосов
/ 14 апреля 2009

Майкл Фезерс написал Библию об этом, Эффективно работает с устаревшим кодом

8 голосов
/ 14 апреля 2009

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

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

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

7 голосов
/ 16 апреля 2009

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

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

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

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

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

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

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

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

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

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

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

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

5 голосов
/ 16 апреля 2009

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

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

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

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

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

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

4 голосов
/ 14 апреля 2009

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

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

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

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

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

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

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

3 голосов
/ 14 апреля 2009

G'day,

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

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

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

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

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

Это будет долгое, медленное путешествие.

НТН

ура

3 голосов
/ 14 апреля 2009

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

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

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

2 голосов
/ 16 апреля 2009

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

  1. Большая база кодов для рефакторинга
  2. Работа в команде

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

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

2 голосов
/ 14 апреля 2009

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...