Хрупкие юнит-тесты из-за необходимости чрезмерного издевательства

21

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

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

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

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

Вот пример модульного теста, который фиксирует проблему.

В качестве краткого замечания «MergeTests» - это класс модульного тестирования, который наследует от класса, который мы тестируем, и при необходимости переопределяет поведение. Это «шаблон», который мы используем в наших тестах, чтобы позволить нам переопределять вызовы внешних классов / зависимостей.

[TestMethod]
public void VerifyMergeStopsSpinner()
{
    var mockViewModel = new Mock<MergeTests> { CallBase = true };
    var mockMergeInfo = new MergeInfo(Mock.Of<IClaim>(), Mock.Of<IClaim>(), It.IsAny<bool>());

    mockViewModel.Setup(m => m.ClaimView).Returns(Mock.Of<IClaimView>);
    mockViewModel.Setup(
        m =>
        m.TryMergeClaims(It.IsAny<Func<bool>>(), It.IsAny<IClaim>(), It.IsAny<IClaim>(), It.IsAny<bool>(),
                         It.IsAny<bool>()));
    mockViewModel.Setup(m => m.GetSourceClaimAndTargetClaimByMergeState(It.IsAny<MergeState>())).Returns(mockMergeInfo);
    mockViewModel.Setup(m => m.SwitchToOverviewTab());
    mockViewModel.Setup(m => m.IncrementSaveRequiredNotification());
    mockViewModel.Setup(m => m.OnValidateAndSaveAll(It.IsAny<object>()));
    mockViewModel.Setup(m => m.ProcessPendingActions(It.IsAny<string>()));

    mockViewModel.Object.OnMerge(It.IsAny<MergeState>());    

    mockViewModel.Verify(mvm => mvm.StopSpinner(), Times.Once());
}

Как остальные из вас справились с этим, или нет великолепного «простого» способа справиться с этим?

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

PremiumTier
источник
Ух, я вижу только макетную установку, отсутствие инстанцирования SUT или что-то в этом роде, вы тестируете здесь какую-то реальную реализацию? Кто должен вызывать StopSpinner? OnMerge? Вы должны издеваться над любыми зависимостями, которые он может вызывать, но не над самой вещью ..
Joppe
Это немного трудно увидеть, но Mock <MergeTests> - это SUT. Мы устанавливаем флаг CallBase, чтобы гарантировать, что метод «OnMerge» выполняется на реальном объекте, но макетировать методы, вызываемые «OnMerge», которые могли бы вызвать сбой теста из-за проблем с зависимостями и т. Д. Цель теста - последняя строка - чтобы убедиться, что мы остановили счетчик в этом случае.
PremiumTier
MergeTests звучит как другой инструментальный класс, а не что-то, что живет в производстве, отсюда и путаница.
Joppe
1
Если не считать других ваших проблем, мне кажется неправильным, что ваше SUT является пробным <MergeTests>. Зачем вам проверять макет? Почему вы не тестируете сам класс MergeTests?
Эрик Кинг,

Ответы:

18
  1. Исправьте код, чтобы он был лучше разработан. Если у ваших тестов есть эти проблемы, то ваш код будет иметь худшие проблемы, когда вы пытаетесь что-то изменить.

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

«У модульных тестов должна быть только одна причина провалиться» - хорошее руководство, но, по моему опыту, нецелесообразно. Трудно писать тесты, не пишите. Хрупкие испытания не верят.

Telastyn
источник
Я полностью согласен с исправлением дизайна кода, но в менее идеальном мире разработки для большой компании с жесткими временными рамками может быть трудно обосновать необходимость «погашения» технического долга, понесенного прошлыми командами, или принятия неправильных решений в целом. один раз. Во-вторых, большая часть насмешек связана не только с тем, что мы хотим, чтобы тест не удался только по одной причине, а с тем, что исполняемый код не может быть разрешен без предварительной обработки большого количества зависимостей, созданных внутри этого кода. , Извините, что переместил сообщения о цели на этот.
PremiumTier
Если лучший дизайн нереалистичен, я согласен с «Кому интересно, если вы используете другие 5 методов?» Убедитесь, что метод выполняет требуемую функцию, а не как он это делает.
Квеббл
@Kwebble - Понятно, однако цель вопроса состояла в том, чтобы определить, существует ли простой способ проверки поведения метода, когда вам также нужно смоделировать другие поведения, вызываемые в методе, чтобы вообще запустить тест. Я хочу удалить «как», но не знаю как :)
PremiumTier
Там нет волшебной серебряной пули. Там нет "простой способ" проверить плохой код. Либо тестируемый код необходимо реорганизовать, либо сам тестовый код также будет плохим. Либо тест будет плохим, потому что он будет слишком специфичным для внутренних деталей, как вы столкнулись, или, как подсказывает метод , вы можете запускать тесты в рабочей среде, но тогда тесты будут намного медленнее и сложнее. В любом случае, тесты будут сложнее писать, сложнее поддерживать и склонны к ложным негативам.
Стивен Доггарт
8

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

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

Но я отмечаю, что это ересь с моей стороны. Люди, которые увлекаются юнит-тестированием, отстаивают «чистые» юнит-тесты и называют то, что я описал, «интеграционными тестами». Я лично не беспокоюсь об этом различии.

btilly
источник
3

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

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

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

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

Joppe
источник
Проблема в том, что мы должны высмеивать «как», чтобы проверить «что». Это ограничение, накладываемое дизайном кода. Я, конечно, не хочу «издеваться» над тем, как это делает тест хрупким.
PremiumTier
Глядя на названия методов, я думаю, что ваш проверенный класс просто берет на себя слишком много обязанностей. Читайте по принципу единой ответственности. Заимствование у MVC может немного помочь, кажется, ваш класс решает как пользовательский интерфейс, инфраструктуру, так и проблемы бизнеса.
Joppe
Да :( Это был бы тот плохо спроектированный устаревший код, о котором я говорил. Мы работаем над
редизайном