Статьи

Модульное тестирование Succintly: стратегии для модульных тестов

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

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

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

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

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

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

Одним из преимуществ запуска проекта по набору требований является то, что вы получаете возможность создать архитектуру (или выбрать стороннюю архитектуру) как часть процесса разработки. Сторонние платформы, которые позволяют вам использовать архитектуры, такие как инверсия управления (и связанная с ними концепция внедрения зависимостей), а также формальные архитектуры, такие как Model-View-Controller (MVC) и Model-View-ViewModel (MVVM), облегчают модульное тестирование по той простой причине, что модульная архитектура обычно проще для модульного тестирования. Эти архитектуры выделяют:

  • Презентация (просмотр).
  • Модель (отвечает за постоянство и представление данных).
  • Контроллер (где должны выполняться вычисления).

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

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

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

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


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

Один из них — «Разработка через тестирование», подытоженная следующим рабочим процессом:

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

  • Если требуются зависимости от других объектов, которые еще не реализованы (объекты передаются в качестве параметров методу или возвращаются методом), реализуйте их как пустые интерфейсы.
  • Если свойства отсутствуют, реализуйте заглушки для свойств, которые необходимы для проверки результатов.
  • Напишите какие-либо требования к настройке или проверке.
  • Напишите тесты. Причины написания любых заглушек перед написанием теста: во-первых, использовать IntelliSense при написании теста; во-вторых, установить, что код все еще компилируется; и в-третьих, чтобы гарантировать, что тестируемый метод, его параметры, интерфейсы и свойства синхронизированы в отношении именования.
  • Запустите тесты, убедившись, что они не пройдены.
  • Код реализации.
  • Запустите тесты, убедившись, что они успешны.

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private void btnCalculate_Click(object sender, System.EventArgs e)
{
  double Principal, AnnualRate, InterestEarned;
  double FutureValue, RatePerPeriod;
  int NumberOfPeriods, CompoundType;
 
  Principal = Double.Parse(txtPrincipal.Text);
  AnnualRate = Double.Parse(txtInterest.Text) / 100;
 
  if (rdoMonthly.Checked)
    CompoundType = 12;
  else if (rdoQuarterly.Checked)
    CompoundType = 4;
  else if (rdoSemiannually.Checked)
    CompoundType = 2;
  else
    CompoundType = 1;
 
  NumberOfPeriods = Int32.Parse(txtPeriods.Text);
  double i = AnnualRate / CompoundType;
  int n = CompoundType * NumberOfPeriods;
 
  RatePerPeriod = AnnualRate / NumberOfPeriods;
  FutureValue = Principal * Math.Pow(1 + i, n);
  InterestEarned = FutureValue — Principal;
 
  txtInterestEarned.Text = InterestEarned.ToString(«C»);
  txtAmountEarned.Text = FutureValue.ToString(«C»);
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public enum CompoundType
{
  Annually = 1,
  SemiAnnually = 2,
  Quarterly = 4,
  Monthly = 12
}
 
private double CompoundInterestCalculation(
  double principal,
  double annualRate,
  CompoundType compoundType,
  int periods)
{
  double annualRateDecimal = annualRate / 100.0;
  double i = annualRateDecimal / (int)compoundType;
  int n = (int)compoundType * periods;
  double ratePerPeriod = annualRateDecimal / periods;
  double futureValue = principal * Math.Pow(1 + i, n);
  double interestEaned = futureValue — principal;
 
  return interestEaned;
}

который тогда позволил бы написать простой тест:

1
2
3
4
5
6
[TestMethod]
public void CompoundInterestTest()
{
  double interest = CompoundInterestCalculation(2500, 7.55, CompoundType.Monthly, 4);
  Assert.AreEqual(878.21, interest, 0.01);
}

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

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

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

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

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

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

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

Экономически эффективный процесс модульного тестирования требует баланса между стратегиями разработки, основанной на тестировании, Code First, Test Second и «Test Some Other Way». Всегда следует учитывать экономическую эффективность модульного тестирования, а также такие факторы, как опыт разработчиков в команде. Как менеджер, вы, возможно, не захотите слышать, что подход, основанный на тестировании, является хорошей идеей, если ваша команда достаточно экологична и вам нужен процесс, чтобы привить дисциплину и подход.