2010-11-05 2 views
2

В моем приложении ASP.Net MVC я использую IoC для облегчения модульного тестирования. Структура моего приложения - тип структуры Controller -> Service Class -> Repository. Для проведения модульного тестирования у меня есть класс InMemoryRepository, который наследует мой IRepository, который вместо выхода в базу данных использует внутренний член List<T>. Когда я строю свои модульные тесты, я просто передаю экземпляр внутреннего репозитория вместо моего репозитория EF.Должен ли я использовать макет объектов при модульном тестировании?

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

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

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

Однако я не могу понять, как решить эту проблему полностью с помощью mocks и все еще иметь действующие тесты. Например, один из моих модульных тестов: если я вызываю _service.GetDocumentById(5), он получает правильный документ из репозитория. Единственный способ - это действительный модульный тест (насколько я понимаю), если у меня есть 2 или 3 документа, и мой метод GetdocumentById() правильно извлекает тот, у которого есть идентификатор 5.

Как мне получить mockinged repository с вызовом AsQueryable, и как бы я удостоверился, что я не замаскирую какие-либо проблемы, которые я создаю с помощью своих операторов Linq, путем hardcoding операторов return при настройке издевающегося репозитория? Лучше ли проводить тестирование модуля класса обслуживания с помощью InMemoryRepository, но изменить тесты моего контроллера, чтобы использовать издеваемые объекты обслуживания?


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

A Repository - это хранилище данных для одного типа объектов, поэтому, если моему классу службы документов нужны объекты документа, он создает IRepository<Document>.

Контроллеры передаются IRepositoryFactory. IRepositoryFactory - это класс, который, как предполагается, упрощает создание репозиториев без непосредственного обращения к репозиторию в контроллер или если диспетчер беспокоится о том, какие классы услуг требуют, какие репозитории. У меня есть InMemoryRepositoryFactory, который дает классы обслуживания InMemoryRepository<Entity> экземпляров, и эта же идея касается моего EFRepositoryFactory.

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

Так, например

public class DocumentController : Controller 
{ 
    private DocumentService _documentService; 

    public DocumentController(IRepositoryFactory factory) 
    { 
     _documentService = new DocumentService(factory); 
    } 
    ... 
} 

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

+2

«Лучше ли проводить тестирование единицы измерения класса обслуживания с помощью« InMemoryRepository », но изменить тесты модуля-контроллера, чтобы использовать насмешливые объекты обслуживания?» Да, точно. –

+0

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

ответ

3

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

public class DocumentController : Controller 
{ 
    private IDocumentService _documentService; 

    // The controller doesn't construct the service itself 
    public DocumentController(IDocumentService documentService) 
    { 
     _documentService = documentService; 
    } 
    ... 
} 

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

(И см Misko Hevry's article about constructors doing real work в течение длительного обсуждения преимуществ реструктуризации кода, как это.)

+0

Спасибо. Это имеет большой смысл, так же как и ссылка, которую вы предоставили! – KallDrexx

1

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

2

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

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

В то время как ваша забота действительно, я лично не буду беспокоиться о сбое InMemoryRepository. Это тестовые объекты, и вы должны держать эти объекты тестов максимально простыми. Это не позволяет вам писать тесты для ваших тестовых объектов. В большинстве случаев я предполагаю, что они верны (однако иногда я использую самообследование в таком классе, написав утверждения Assert). Тест не удастся, если такой объект ошибочно работает. Это не оптимально, но вы, как правило, достаточно быстро узнаете, что проблема в моем опыте. Чтобы быть продуктивным, вам придется рисовать линию где-то.

Ошибки в контроллере, вызванные обслуживанием, являются еще одной чашкой чая IMO. Хотя вы могли бы издеваться над этим сервисом, это сделало бы тестирование более сложным и менее надежным. Было бы лучше НЕ тестировать сервис вообще. Проверяйте только контроллер! Контроллер будет звонить в службу, и если ваш сервис не будет хорошо себя вести, ваши контрольные тесты обнаружат. Таким образом, вы проверяете объекты верхнего уровня в своем приложении. Покрытие кода поможет вам определить части вашего кода, который вы не тестируете. Конечно, это невозможно во всех сценариях, но это часто работает хорошо. Когда служба работает с издеваемым репозиторием (или подразделением), это будет работать очень хорошо.


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

Прежде всего, я стараюсь минимизировать инверсию зависимостей только для того, что мне нужно для выполнения моих модульных тестов. Вызовы к системным часам, базе данных, серверу 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()). Опять же, вам придется рисовать линию где-то.

Надеюсь, это поможет.

Смежные вопросы