Подтвердить что ты не робот

Единичное тестирование больших блоков кода (сопоставление, перевод и т.д.)

Мы unit test большую часть нашей бизнес-логики, но зациклились на том, как лучше всего проверить некоторые из наших больших задач обслуживания и процедур импорта/экспорта. Например, рассмотрите экспорт данных платежной ведомости из одной системы в стороннюю систему. Чтобы экспортировать данные в формате, который требуется компании, нам нужно нажать ~ 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, у нас есть сотни экспорта и импорта. Другие компании ДЕЙСТВИТЕЛЬНО unit test этот тип вещей? Если да, есть ли ярлыки, чтобы сделать его менее болезненным? Я наполовину соблазнился сказать "ни одного модуля, проверяющего процедуры импорта/экспорта", и просто повторить интеграционное тестирование позже.

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

4b9b3361

Ответ 1

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

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

Ответ 2

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

Инкапсуляция - это старая концепция объектно-ориентированного дизайна, но некоторые люди воспринимают ее настолько, что испытывают страдания. Там другой принцип OO называется Open/Closed Principle, который намного лучше подходит для тестирования. Инкапсуляция по-прежнему ценна, но не за счет расширяемости - фактически, testability - это просто другое слово для принципа Open/Closed.

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

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


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

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

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

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

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

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

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

Ответ 3

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

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

  • Попробуйте разложить автономные алгоритмы и структуры данных, такие как синтаксические анализаторы, средства визуализации, операции поиска, преобразователи, структуры данных специального назначения...
  • Попробуйте выяснить, обрабатываются ли ваши данные в несколько этапов и могут быть построены в виде механизма трубопроводов и фильтров или многоуровневой архитектуры. Попытайтесь найти как можно больше слоев.
  • Отдельные технические (файлы, базы данных) части из логических частей.
  • Если у вас есть многие из этих монстров импорта/экспорта, посмотрите, что у них есть, и измените их и повторно используйте.
  • Ожидайте, что ваш код слишком плотный, т.е. он содержит слишком много разных функций рядом с каждым в слишком маленьком LOC. Посетите различные "изобретения" в своем коде и подумайте, действительно ли они на самом деле сложные объекты, которые стоят того, чтобы иметь собственный класс.
    • Как LOC, так и количество классов, вероятно, будут увеличиваться при рефакторе.
    • Попробуйте сделать код очень простым ( "детским кодом" ) внутри классов и сложным в отношениях между классами.

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


ИЗМЕНИТЬ

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

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

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

Ответ 4

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

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

Как уже упоминалось, serbrech Workign Effectively с кодом Legacy поможет вам в конце концов, я бы настоятельно советовал прочесть его даже для проектов с новыми полями.

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

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

Ответ 5

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

Первая выплачивается за текущую дату:

    var pays = _pays.GetPaysForCurrentDate();

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

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

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

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

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

    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.)

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

Ответ 6

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

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

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

Ответ 7

Я думаю, что у Томаша Зелинского есть часть ответа. Но если вы говорите, что у вас 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()  

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

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

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

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

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

Ответ 8

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

Если есть общие биты функциональности, затем извлеките каждый из них (как отдельные классы) и протестируйте их по отдельности. Естественно, эти небольшие вспомогательные классы имеют разные общедоступные интерфейсы, что должно уменьшить проблему частных 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, но ваш пример, кажется, объединяет все три шага вместе. Это говорит о том, что первым шагом было бы разделить вещи таким образом, чтобы все данные сначала извлекались, а затем переводились, а затем сохранялись. Вы можете, конечно, проверить по крайней мере эти шаги отдельно.

Ответ 9

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

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

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

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

Ответ 10

Вы заглянули в Moq?

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

Moq (произносится как "Mock-you" или просто "Mock" ) - единственная насмешливая библиотека для .NET, разработанный с нуля до в полной мере использовать .NET 3.5 (т. Linq) и С# 3.0 (например, лямбда-выражения) которые делают его наиболее продуктивным, безопасный тип и рефакторинг доступная библиотека.