Я боролся со все более раздражающей проблемой, касающейся наших модульных тестов, которые мы внедряем в моей команде. Мы пытаемся добавить модульные тесты в унаследованный код, который не был хорошо разработан, и, хотя у нас не было никаких проблем с фактическим добавлением тестов, мы начинаем бороться с тем, как тесты получаются.
В качестве примера проблемы, скажем, у вас есть метод, который вызывает 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());
}
Как остальные из вас справились с этим, или нет великолепного «простого» способа справиться с этим?
Обновление - я ценю все отзывы. К сожалению, и это не удивительно, на самом деле не существует отличного решения, шаблона или практики, которой можно следовать при модульном тестировании, если тестируемый код плохой. Я отметил ответ, который лучше всего уловил эту простую истину.
источник
Ответы:
Исправьте код, чтобы он был лучше разработан. Если у ваших тестов есть эти проблемы, то ваш код будет иметь худшие проблемы, когда вы пытаетесь что-то изменить.
Если вы не можете, то, возможно, вам нужно быть менее идеальным. Проверка на соответствие предварительным и последующим условиям метода. Кого волнует, если вы используете другие 5 методов? Предположительно, у них есть свои собственные модульные тесты, дающие понять (что), что вызвало сбой при неудачном тестировании.
«У модульных тестов должна быть только одна причина провалиться» - хорошее руководство, но, по моему опыту, нецелесообразно. Трудно писать тесты, не пишите. Хрупкие испытания не верят.
источник
Разбиение больших методов на более сфокусированные маленькие методы - определенно лучшая практика. Вы видите это как боль при проверке поведения юнит-теста, но вы испытываете боль и другими способами.
Тем не менее, это ересь, но я лично фанат создания реалистичных временных сред тестирования. То есть вместо того, чтобы высмеивать все, что скрыто внутри этих других методов, убедитесь, что существует простая настройка временной среды (в комплекте с частными базами данных и схемами - SQLite может помочь здесь), которая позволяет вам запускать все эти вещи. Ответственность за знание того, как создать / разрушить эту тестовую среду, лежит на коде, который требует этого, чтобы при его изменении вам не приходилось изменять весь код модульного теста, который зависел от его существования.
Но я отмечаю, что это ересь с моей стороны. Люди, которые увлекаются юнит-тестированием, отстаивают «чистые» юнит-тесты и называют то, что я описал, «интеграционными тестами». Я лично не беспокоюсь об этом различии.
источник
Я хотел бы рассмотреть возможность смягчения макетов и просто сформулировать тесты, которые могут включать методы, к которым он обращается.
Не проверяй как , проверяй что . Это результат, который имеет значение, включая суб-методы, если это необходимо.
С другой стороны, вы можете сформулировать тест, выполнить его одним большим методом, выполнить рефакторинг и получить дерево методов после рефакторинга. Вам не нужно проверять каждого из них в отдельности. Это конечный результат, который имеет значение.
Если под-методы затрудняют тестирование некоторых аспектов, рассмотрите возможность их разделения на отдельные классы, чтобы вы могли смоделировать их там более аккуратно, без использования тестируемого класса. Трудно сказать, тестируете ли вы какую-либо конкретную реализацию в вашем тестовом примере.
источник