Лично я бы разработал систему на основе шаблона Unit of Work, который ссылается на репозитории. Это может упростить задачу и позволить вам выполнять более сложные операции атомарно. Обычно у вас есть IUnitOfWorkFactory
, который предоставляется как зависимость в классах Service. Сервисный класс создаст новую единицу работы, и эта единица работы ссылается на репозитории. Вы можете увидеть пример этого здесь .
Если я правильно понимаю, вы обеспокоены ошибками в одном фрагменте (низкоуровневого) кода, которые не позволяют выполнить множество тестов, что усложняет понимание реальной проблемы. В качестве конкретного примера вы берете InMemoryRepository
.
Хотя ваше беспокойство действительно, я лично не буду беспокоиться о неудаче InMemoryRepository
. Это тестовые объекты, и вы должны сохранять эти тестовые объекты как можно более простыми. Это избавляет вас от необходимости писать тесты для ваших тестовых объектов. Большую часть времени я предполагаю, что они правильные (однако я иногда использую самопроверки в таком классе, написав операторы Assert). Тест не пройдёт, если такой объект будет плохо себя вести. Это не оптимально, но вы, как правило, достаточно быстро выясните, в чем проблема по моему опыту. Чтобы быть продуктивным, вам нужно где-то нарисовать линию.
Ошибки в контроллере, вызванные обслуживанием, - это еще одна чашка чая IMO. Хотя вы можете издеваться над сервисом, это сделает тестирование более трудным и менее заслуживающим доверия. Было бы лучше НЕ проверять сервис вообще. Только тестируйте контроллер! Контроллер вызовет сервис, и если ваш сервис не будет вести себя хорошо, тесты вашего контроллера выяснят это. Таким образом, вы тестируете только объекты верхнего уровня в вашем приложении. Покрытие кода поможет вам определить части кода, которые вы не тестируете. Конечно, это невозможно во всех сценариях, но это часто работает хорошо. Когда служба работает с проверенным хранилищем (или единицей работы), это будет работать очень хорошо.
<ч />
Ваше второе беспокойство состояло в том, что эти зависимости заставляют вас иметь большую настройку теста . У меня есть две вещи, чтобы сказать об этом.
Прежде всего, я стараюсь свести к минимуму инверсию зависимостей только до того, что мне нужно для запуска модульных тестов. Вызовы системных часов, базы данных, SMTP-сервера и файловой системы должны быть сфальсифицированы, чтобы сделать модульные тесты быстрыми и надежными. Другие вещи, которые я стараюсь не переворачивать, потому что чем больше вы издеваетесь, тем менее надежными становятся тесты. Вы тестируете меньше. Минимизация инверсии зависимостей (к тому, что вам нужно для хороших RTM модульных тестов) помогает упростить настройку теста.
Но (второй момент) вам также необходимо написать свои модульные тесты так, чтобы они были удобочитаемыми и поддерживаемыми (сложная часть о модульном тестировании или создании программного обеспечения в целом). Наличие больших тестовых настроек затрудняет их понимание и затрудняет изменение тестового кода, когда класс получает новую зависимость. Я обнаружил, что один из лучших способов сделать тесты более читабельными и обслуживаемыми - это использовать простые фабричные методы в вашем тестовом классе, чтобы централизовать создание типов, которые вам нужны в тесте (я никогда не использую фреймворки для моделирования). Есть две модели, которые я использую. Один из них - простой фабричный метод, например, который создает допустимый тип:
FakeDocumentService CreateValidService()
{
return CreateValidService(CreateInitializedContext());
}
FakeDocumentService CreateValidService(InMemoryUnitOfWork context)
{
return new FakeDocumentSerice(context);
}
Таким образом, тесты могут просто вызывать эти методы, а когда им нужен действительный объект, они просто вызывают один из фабричных методов. Конечно, когда один из этих методов случайно создает недопустимый объект, многие тесты не пройдут. Это трудно предотвратить, но легко исправить. Легко исправить означает, что тесты ремонтопригодны.
Другой шаблон, который я использую, - это использование типа контейнера, который содержит аргументы / свойства фактического объекта, который вы хотите создать. Это становится особенно полезным, когда объект имеет много разных свойств и / или аргументов конструктора. Смешайте это с фабрикой для контейнера и методом построителя для создаваемого объекта, и вы получите очень читаемый тестовый код:
[TestMethod]
public void Operation_WithValidArguments_Succeeds()
{
// Arrange
var validArgs = CreateValidArgs();
var service = BuildNewService(validArgs);
// Act
service.Operation();
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Operation_NegativeAge_ThrowsException()
{
// Arrange
var invalidArgs = CreateValidArgs();
invalidArgs.Age = -1;
var service = BuildNewService(invalidArgs);
// Act
service.Operation();
}
Это позволяет позволить тесту только указать, что имеет значение!Это очень важно, чтобы сделать тесты читабельными!Метод CreateValidArgs()
может создать контейнер с более чем 100 аргументами, которые сделают правильное SUT (тестируемая система).Теперь вы централизовали в одном месте действительную конфигурацию по умолчанию.Я надеюсь, что это имеет смысл.
Ваше третье беспокойство касалось невозможности проверить, работает ли LINQ-запрос ожидаемым образом с данным поставщиком LINQ.Это допустимая проблема, потому что довольно легко писать запросы LINQ (в дерево выражений), которые отлично работают при использовании над объектами в памяти, но не выполняют при запросах к базе данных.Иногда невозможно перевести запрос (потому что вы вызываете метод .NET, который не имеет аналога в базе данных), или у поставщика LINQ есть ограничения (или ошибки).Особенно провайдер LINQ Entity Framework 3.5 сильно отстой.
Однако эту проблему вы не можете решить с помощью модульных тестов по определению.Потому что когда вы вызываете базу данных в своих тестах, это больше не модульный тест.Тем не менее, юнит-тесты никогда полностью не заменяют ручное тестирование: -)
Тем не менее, это серьезная проблема.Помимо модульного тестирования вы можете провести интеграционное тестирование.В этом случае вы запускаете свой код с реальным провайдером и (выделенной) тестовой базой данных.Запустите каждый тест в транзакции базы данных и откатите транзакцию в конце теста ( TransactionScope прекрасно работает с этим!).Однако обратите внимание, что написание поддерживаемых интеграционных тестов еще сложнее, чем написание обслуживаемых модульных тестов.Вы должны убедиться, что модель вашей тестовой базы данных синхронизирована.Каждый интеграционный тест должен вставлять данные, необходимые для этого теста, в базу данных, что часто требует много работы для написания и поддержки.Лучше всего сводить количество интеграционных тестов к минимуму.Иметь достаточно интеграционных тестов, чтобы вы чувствовали себя уверенно при внесении изменений в систему.Например, необходимости вызывать метод сервиса со сложным оператором LINQ в одном тесте часто бывает достаточно, чтобы проверить, способен ли ваш поставщик LINQ создать из него действительный SQL.В большинстве случаев я просто предполагаю, что поставщик LINQ будет вести себя так же, как и поставщик LINQ to Objects (.AsQueryable()
).Опять же, вам придется где-то нарисовать линию.
Надеюсь, это поможет.