Как использовать модульные тесты в проектах со многими уровнями косвенности

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

public bool IsOverdraft)
{
    balanceProvider.IsOverdraft();
}

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

  • Если balanceProvider.IsOverdraft() возвращает true, то IsOverdraft должен возвращать true
  • Если balanceProvider.IsOverdraft() возвращает false, то IsOverdraft должен возвращать false
  • Если balanceProvider выдает исключение, то IsOverdraft должен удалить одно и то же исключение

Чтобы усугубить ситуацию, используемая система фраз (NMock2) приняла имена методов как строковые литералы, как показано ниже:

NMock2.Expect.Once.On(mockBalanceProvider)
    .Method("IsOverdraft")
    .Will(NMock2.Return.Value(false));

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

Каков идеальный способ справиться с этой ситуацией?

A) Сохраняйте меньшие уровни слоев, чтобы эти переадресационные вызовы больше не выполнялись.

B) Не проверяйте эти методы пересылки, так как они не содержат бизнес-логики. Для целей охвата все они отмечены атрибутом ExcludeFromCodeCoverage.

C) Проверяйте только, если вызывается правильный метод, без проверки возвращаемых значений, исключений и т.д.

D) Подсуньте его и продолжайте писать эти тесты;)

Ответ 1

Либо B, либо C. Проблема с такими общими требованиями ( "каждый метод должен иметь unit test, каждая строка кода должна быть покрыта" ) - иногда выгоды, которые они предоставляют, не стоят затрат. Если это то, что вы придумали, я предлагаю пересмотреть этот подход. "У нас должно быть 95% охвата кода" может быть привлекательным на бумаге, но на практике он быстро порождает проблемы, подобные тем, которые у вас есть.

Кроме того, код, который вы тестируете, я бы назвал тривиальным кодом. Имея 3 теста на это, скорее всего, избыток. Для этой единственной строки кода вам нужно будет поддерживать еще 40. Если ваше программное обеспечение не критично (что может объяснять потребность в высоком покрытии), я бы пропустил эти тесты.

Один из (IMHO) самых прагматических советов по этой теме был предоставлен Кентом Бекем некоторое время назад на этом самом сайте, и я немного расширил эти мысли с помощью в моих сообщениях в блоге - Что вы должны проверить?

Ответ 2

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

Если я пишу тест, но тест заканчивается просто "дублированием" реализации или хуже... если , сложнее понять тест, чем фактическая реализация then действительно такой тест не должен существовать. Никто не интересуется чтением таких тестов. Тесты не должны содержать сведения о реализации. Тест - это , что " должно происходить не ", как " это будет сделано. Поскольку вы отметили свой вопрос с помощью" TDD", я бы добавил, что TDD - это практика проектирования. Поэтому, если я уже знаю, что на 100% уверен заранее, какой будет дизайн того, что я собираюсь реализовать, тогда нет смысла использовать TDD и писать модульные тесты (Но я всегда буду иметь во всех случаях приемочный тест высокого уровня, который будет охватывать этот код). Это часто случается, когда вещь для дизайна действительно проста, как в вашем примере. TDD не касается тестирования и покрытия кода, а действительно помогает нам разработать наш код и документировать наш код. Нет смысла использовать инструмент проектирования или инструмент документации для проектирования/документирования простых/очевидных вещей.

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

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

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

Ответ 3

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

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

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

Ответ 4

Несколько вещей, чтобы добавить к обсуждению здесь.

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

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

[Test]
public void IsOverdraftDelegatesToBalanceProvider()
{
    var result = RandomBool();
    providerMock.Setup(p=>p.IsOverdraft()).Returns(result);
    Assert.That(myObject.IsOverDraft(), Is.EqualTo(result);
}

Не создавайте ненужные слои косвенности. В основном, модульные тесты скажут вам, нужна ли вам косвенная привязка. Большинство потребностей косвенности могут быть решены с помощью принципа инверсии зависимостей, или "пара абстракций, а не конкреций". Некоторые слои необходимы по другим причинам (я делаю реализацию WCF ServiceContract тонким проходом через слой. Я также не проверяю, что это происходит). Если вы видите бесполезный слой косвенности, 1) убедитесь, что он действительно бесполезен, затем 2) удалите его. Кодовый беспорядок имеет огромные затраты с течением времени. Resharper делает это смехотворно простым и безопасным.

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

Ответ 5

Я бы сказал D) Сосать его и продолжать писать эти тесты;) и попытаться посмотреть, можете ли вы заменить NMock MOQ.

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