Рефакторинг классов Уровня служб

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

public class InvoiceCalculator:IInvoiceCalculator
{
   public CalculateInvoice(Invoice invoice)
   {
      foreach (InvoiceLine il in invoice.Lines)
      {
          UpdateLine(il);
      }
      //do a ton of other stuff here
   }

   private UpdateLine(InvoiceLine line)
   {
      line.Amount = line.Qty * line.Rate;
      //do a bunch of other stuff, including calls to other private methods
   }
}

В этом упрощенном случае (это уменьшается от 1 000 классов строки, которые имеют 1 открытый метод и ~30 частных), мой босс заявляет, что я должен смочь протестировать свой CalculateInvoice, и UpdateLine отдельно (UpdateLine на самом деле называет 3 других закрытых метода и выполняет вызовы базы данных также). Но как я сделал бы это? Его предложенный рефакторинг казался немного замысловатым мне:

//Tiny part of original code
public class InvoiceCalculator:IInvoiceCalculator
{
   public ILineUpdater _lineUpdater;

   public InvoiceCalculator (ILineUpdater lineUpdater)
   {
      _lineUpdater = lineUpdater;
   }

   public CalculateInvoice(Invoice invoice)
   {
      foreach (InvoiceLine il in invoice.Lines)
      {
          _lineUpdater.UpdateLine(il);
      }
      //do a ton of other stuff here
   }
}

public class LineUpdater:ILineUpdater
{
   public UpdateLine(InvoiceLine line)
   {
      line.Amount = line.Qty * line.Rate;
      //do a bunch of other stuff
   }
}

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

Спасибо!

Jess

7
задан Beep beep 24 January 2010 в 20:33
поделиться

5 ответов

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

С другой стороны, если метод Updateline () имеет некоторое значение для бизнес-логики, если вы можете представить ситуацию, когда вам нужно будет изменить этот метод независимо от остальной части класса (и, следовательно, тестировать его отдельно), то Вы должны продолжаться с рефакторингом его в отдельный класс LineUpdater.

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

1
ответ дан 7 December 2019 в 07:45
поделиться

Это пример зависти :

line.Amount = line.Qty * line.Rate;

Это, вероятно, должно больше всего, посмотреть:

  var amount = line.CalculateAmount();

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

5
ответ дан 7 December 2019 в 07:45
поделиться

Ну, ваш босс идет более правильно с точки зрения тестирования подразделения:
Теперь он может протестировать CalcoulateInVoice () без тестирования функции UpdateNeline (). Он может пройти макет объекта вместо настоящего объекта LineUpdater и тестировать только CalcoulateinVoice (), не целую кучу кода.
Это правильно? По-разному. Ваш босс хочет сделать реальные единицы-тесты. И тестирование в вашем первом примере не будет тестированием единиц, это будет тестирование в интеграции.

Каковы преимущества единиц-тестов перед тестами интеграции?
1) Установки-тесты позволяют тестировать только один метод или свойство, без него не влияют другие методы / базы данных и так далее.
2) Второе преимущество - тесты единиц, выполняют быстрее (например, вы сказали Updateline использует базу данных), поскольку они не проверяют все вложенные методы. Вложенные методы могут быть вызовы баз данных, поэтому, если у вас есть тысячи тестов, ваши тесты могут работать медленно (несколько минут).
3) Третье преимущество: если ваши методы составляют вызовы базы данных, иногда вам нужно настроить базу данных (заполнить ее данные, необходимые для теста), и его можно не легко - возможно, вам придется написать пару страниц кода Подготовьте базу данных для теста. С помощью модульных тестов вы отделяете вызовы базы данных из тестируемых методов (используя объекты MOCK).

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

Также у меня есть пара предложений для вас:
1) Я не думаю, что имея поле, это хорошая идея. Похоже, что поле количества дополнительно, потому что это значение может быть рассчитано на основе 2 других общественных месторождений. Если вы хотите сделать это в любом случае, я бы сделал это как свойство только для чтения, которая возвращает Qty * Rate.
2) Обычно имея класс, который состоит из 1000 рядов, может означать, что он плохо спроектирован и должен быть повторным образом.

Теперь, я надеюсь, вам лучше понять ситуацию и можете решить. Кроме того, если вы понимаете ситуацию, вы можете поговорить с вашим боссом, и вы можете решить вместе.

1
ответ дан 7 December 2019 в 07:45
поделиться

да, отлично. Не уверен, что в объект InvoiceLine тоже включена какая-то логика, иначе, наверное, понадобилась бы еще и IInvoiceLine. Иногда у меня одни и те же вопросы. С одной стороны, вы хотите сделать все правильно и выполнить юнит-тестирование вашего кода, но когда речь идет о вызовах баз данных и, возможно, о написании файлов, это вызывает много дополнительной работы по настройке первого теста со всеми тестообъектами, которые вступают в игру, когда скоро произойдет запись файлов и создание базы данных io, интерфейсов, утверждений, а также вы хотите проверить, что уровень данных не содержит никаких ошибок. Поэтому тест, который является более "процессом", чем "единицей", часто легче собрать. Если у вас есть проект, который будет сильно изменен (в будущем), и много зависимостей этого кода (может быть, другие программы читают файл или данные из базы данных), может быть неплохо иметь солидный юнит-тест для всех частей вашего кода, и время инвестиций стоит того. Но если проект, как говорит мой последний клиент, "давайте сделаем это вживую и, может быть, немного подправим в следующем году, и в следующем году будет что-то новое", то мне не составит особого труда запустить все юнит-тесты.

Michel

0
ответ дан 7 December 2019 в 07:45
поделиться

Пример вашего начальника мне кажется разумным.

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

  1. Принцип единой ответственности

    Класс должен изменяться только по одной причине.

  2. Оправдывает ли каждый новый класс свое существование

    Были ли классы созданы только ради него, или они инкапсулируют значимую часть логики?

  3. Можете ли вы протестировать каждый фрагмент кода по отдельности?

​​В вашем сценарии, просто глядя на имена, может показаться, что вы отклонились от единственной ответственности - у вас есть IInvoiceCalculator, но этот класс также отвечает за обновление InvoiceLines. Вы не только сильно затруднили тестирование поведения обновления, но и теперь вам нужно изменить свой класс InvoiceCalculator как при изменении бизнес-правил вычислений , так и при изменении правил обновления.

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

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

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

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

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

0
ответ дан 7 December 2019 в 07:45
поделиться
Другие вопросы по тегам:

Похожие вопросы: