Модульное тестирование больших блоков кода (отображения, перевода и т. Д.) - PullRequest
27 голосов
/ 16 января 2010

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

Например, рассмотрим следующее (подмножество ~ 3500 строк кода экспорта):

public void ExportPaychecks()
{
   var pays = _pays.GetPaysForCurrentDate();
   foreach (PayObject pay in pays)
   {
      WriteHeaderRow(pay);
      if (pay.IsFirstCheck)
      {
         WriteDetailRowType1(pay);
      }
   }
}

private void WriteHeaderRow(PayObject pay)
{
   //do lots more stuff
}

private void WriteDetailRowType1(PayObject pay)
{
   //do lots more stuff
}

У нас есть только один открытый метод в этом конкретном классе экспорта - ExportPaychecks (). Это действительно единственное действие, которое имеет смысл для кого-то, вызывающего этот класс ... все остальное является частным (~ 80 частных функций). Мы могли бы сделать их общедоступными для тестирования, но тогда нам нужно было бы смоделировать их для тестирования каждого из них по отдельности (то есть вы не можете тестировать ExportPaychecks в вакууме без насмешки над функцией WriteHeaderRow. Это тоже огромная боль.

Поскольку это единственный экспорт, для одного поставщика перемещение логики в домен не имеет смысла. Логика не имеет значения домена вне этого конкретного класса. В качестве теста мы создали модульные тесты, которые имели почти 100% покрытие кода ... но для этого потребовалось безумное количество тестовых данных, введенных в объекты-заглушки, а также более 7000 строк кода из-за заглушения / насмешек наших многочисленных зависимостей. ,

Как производитель программного обеспечения HRIS, у нас есть сотни экспортов и импортеров. Другие компании действительно проверяют этот тип вещи? Если да, есть ли какие-нибудь способы сделать его менее болезненным? Мне наполовину хочется сказать «нет модульного тестирования процедур импорта / экспорта» и просто реализовать интеграционное тестирование позже.

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

Ответы [ 10 ]

18 голосов
/ 18 января 2010

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

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

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

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


Вот некоторые мысли о том, как выполнить рефакторинг для рассматриваемой проблемы: Каждое приложение ETL должно выполнить , по крайней мере, эти три шага:

  1. Извлечение данных из источника
  2. Преобразование данных
  3. Загрузить данные в пункт назначения

(отсюда и название ETL ). В качестве начала для рефакторинга это дает нам как минимум три класса с различными обязанностями: Extractor, Transformer и Loader. Теперь вместо одного большого класса у вас есть три с более целенаправленными обязанностями. Ничего страшного в этом нет, и уже немного более проверяемого.

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

  • По крайней мере, вам понадобится хорошее представление в памяти каждой «строки» исходных данных. Если источником является реляционная база данных, вы можете использовать ORM, но если нет, то такие классы необходимо смоделировать так, чтобы они правильно защищали инварианты каждой строки (например, если поле не обнуляемо, класс должен гарантировать это путем генерирования исключения, если предпринимается попытка установить нулевое значение). Такие классы имеют четко определенное назначение и могут тестироваться изолированно.
  • То же самое относится и к пункту назначения: для этого вам нужна хорошая объектная модель.
  • Если в источнике выполняется расширенная фильтрация на стороне приложения, вы можете рассмотреть возможность их реализации с использованием шаблона проектирования Specification . Они, как правило, очень хорошо поддаются проверке.
  • На шаге Transform происходит множество действий, но теперь, когда у вас есть хорошие объектные модели как источника, так и получателя, преобразование может быть выполнено Mappers - снова тестируемыми классами.

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

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

7 голосов
/ 20 января 2010

Что-то общее, что пришло мне в голову по поводу рефакторинга :

Рефакторинг не означает, что вы берете свой 3.5k LOC и делите его на n частей. Я бы не рекомендовал публиковать некоторые из ваших 80 методов или подобные вещи. Это больше похоже на вертикальную нарезку кода:

  • Попробуйте выделить автономные алгоритмы и структуры данных, такие как парсеры, средства визуализации, операции поиска, преобразователи, специализированные структуры данных ...
  • Постарайтесь выяснить, обрабатываются ли ваши данные в несколько этапов и могут ли они быть встроены в своего рода конвейерный механизм или механизм фильтрации или многоуровневую архитектуру. Попробуйте найти как можно больше слоев.
  • Отделить технические части (файлы, базы данных) от логических частей.
  • Если у вас много таких монстров импорта / экспорта, посмотрите, что у них общего, и выделите их и используйте повторно.
  • В общем, ожидайте, что ваш код слишком плотный , т. Е. Он содержит слишком много различных функций рядом с каждым в слишком небольшом количестве LOC. Посетите различные «изобретения» в вашем коде и подумайте, не являются ли они на самом деле хитрыми объектами, которые стоит иметь свой собственный класс (ы).
    • И LOC, и количество классов, вероятно, увеличатся при рефакторинге.
    • Постарайтесь сделать ваш код по-настоящему простым ('baby code') внутри классов и сложным в отношениях между классами.

В результате вам не придется писать модульные тесты, которые покрывают весь LOC 3,5 КБ вообще. В одном тесте рассматриваются только его небольшие доли, и у вас будет много небольших тестов, которые не зависят друг от друга.


EDIT

Вот хороший список шаблонов рефакторинга . Среди них одно очень хорошо показывает мое намерение: Разложить условное .

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

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

6 голосов
/ 17 января 2010

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

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

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

http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

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

4 голосов
/ 21 января 2010

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

Проблемой ваших тестов было количество поддельных данных, которые вы должны были создать. Вы можете уменьшить это, создав общий прибор (http://xunitpatterns.com/Shared%20Fixture.html).. Для модульных тестов прибор, который может быть представлением бизнес-объектов в оперативной памяти для экспорта, или в случае интеграционных тестов это может быть фактическим базы данных, инициализированные известными данными. Дело в том, что как бы вы ни генерировали общее устройство в каждом тесте, оно одинаково, поэтому создание новых тестов - это всего лишь небольшая подстройка к существующему устройству для запуска кода, который вы хотите протестировать.

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

3 голосов
/ 16 января 2010

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

Первый получает оплату за текущую дату:

    var pays = _pays.GetPaysForCurrentDate();

Второй безоговорочно обрабатывает результат

    foreach (PayObject pay in pays)
    {
       WriteHeaderRow(pay);
    }

Третий выполняет условную обработку:

    foreach (PayObject pay in pays)
    {
       if (pay.IsFirstCheck)
       {
          WriteDetailRowType1(pay);
       }
    }

Теперь вы можете сделать эти этапы более общими (извините за псевдокод, я не знаю C #):

    var all_pays = _pays.GetAll();

    var pwcdate = filter_pays(all_pays, current_date()) // filter_pays could also be made more generic, able to filter any sequence

    var pwcdate_ann =  annotate_with_header_row(pwcdate);       

    var pwcdate_ann_fc =  filter_first_check_only(pwcdate_annotated);  

    var pwcdate_ann_fc_ann =  annotate_with_detail_row(pwcdate_ann_fc);   // this could be made more generic, able to annotate with arbitrary row passed as parameter

    (Etc.)

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

2 голосов
/ 25 января 2010

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

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

2 голосов
/ 20 января 2010

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

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

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

public void ExportPaychecks(HeaderFormatter h, CheckRowFormatter f)
{
   var pays = _pays.GetPaysForCurrentDate();
   foreach (PayObject pay in pays)
   {
      h.formatHeader(pay);
      f.WriteDetailRow(pay);
   }
}

Абстрактные классы HeaderFormatter и CheckRowFormatter будут определять общий интерфейс для этих типов элементов отчета, а отдельные конкретные подклассы (для различных отчетов) будут содержать логику, например, для удаления дублирующихся строк (или любых других требуется конкретный поставщик).

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


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

2 голосов
/ 16 января 2010

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

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

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

Снова книга от Майкла Фезерса, кажется, должна быть прочитана для вас :) http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

ДОБАВЛЕННЫЙ ПРИМЕР:

Этот пример взят из книги Майкла Фезерса и хорошо иллюстрирует вашу проблему, я думаю:

RuleParser  
public evaluate(string)  
private brachingExpression  
private causalExpression  
private variableExpression  
private valueExpression  
private nextTerm()  
private hasMoreTerms()   
public addVariables()  

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

Хорошо, если вы видите, что это отдельная ответственность и извлекаете класс, например, Tokenizer. этот метод внезапно станет общедоступным в этом новом классе! потому что это его цель. Тогда становится легко проверить это поведение ...

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

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

Надеюсь, это поможет Удачи:)

1 голос
/ 16 января 2010

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

  1. На уровне метода я тестирую все, что субъективно считаю «сложным».Это включает в себя 100% исправлений ошибок, а также все, что вызывает у меня нервозность.

  2. На уровне модуля я тестирую основные варианты использования.Как вы уже заметили, это довольно болезненно, поскольку требует некоторого осмеивания данных.Я достиг этого путем абстрагирования интерфейсов базы данных (то есть, никаких прямых соединений SQL внутри моего модуля отчетности).Для некоторых простых тестов я набрал тестовые данные вручную, для других я написал интерфейс базы данных, который записывает и / или воспроизводит запросы, чтобы я мог загрузить свои тесты с реальными данными.Другими словами, я запускаю один раз в режиме записи, и он не только извлекает реальные данные, но и сохраняет для меня снимок в файле;когда я запускаю в режиме воспроизведения, он обращается к этому файлу вместо реальных таблиц базы данных.(Я уверен, что есть фальшивые фреймворки, которые могут сделать это, но поскольку каждое взаимодействие SQL в моем мире имеет подпись Stored Procedure Call -> Recordset, было довольно просто написать это самому.)

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

0 голосов
/ 16 января 2010

Вы смотрели в Мок?

Цитата с сайта:

Moq (произносится как «издевательство над тобой» или просто «Мок» - единственная издевательская библиотека для .NET разработано с нуля до в полной мере использовать .NET 3.5 (т.е. Деревья выражений Linq) и C # 3.0 особенности (то есть лямбда-выражения) которые делают его наиболее продуктивным, безопасный для типов и дружественный к рефакторингу доступна библиотека для издевательств.

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