Как один модуль должен протестировать контроллер .NET MVC? - PullRequest
60 голосов
/ 11 января 2012

Я ищу совет относительно эффективного модульного тестирования контроллеров .NET MVC.

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

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

Может кто-нибудь предложить несколько лучших подходовтестированию модуля контроллера или объясните, почему вышеуказанные подходы являются действительными / полезными?

Спасибо!

Ответы [ 6 ]

47 голосов
/ 11 января 2012

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

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

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

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

Ответ на комментарий # 1

При изменении реализации вызовов тестируемого метода для изменения / удаления смоделированного метода нижнего уровня, тогдамодульный тест также должен измениться.Однако это не должно происходить так часто, как вы думаете.

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

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

Возможно, я ошибся, когда использовал термин «изменение реализации».Когда метод действия контроллера и соответствующий модульный тест должны быть изменены, чтобы изменить или удалить смоделированный метод, вы действительно меняете поведение контроллера.Рефакторинг по определению означает изменение реализации без изменения общего поведения и ожидаемых результатов.

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

26 голосов
/ 11 января 2012

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

10 голосов
/ 04 августа 2015

Да, вы должны протестировать весь путь до БД. Время, которое вы вкладываете в имитацию, меньше, а ценность, которую вы получаете от имитации, также очень меньше (80% вероятных ошибок в вашей системе не могут быть выбраны с помощью насмешек).

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

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

В моей карьере было только 2% (или очень редко) ситуаций, когда меня заставляли использовать издевательства / заглушки, поскольку было невозможно создать более реалистичный источник данных. Но во всех других ситуациях интеграционные тесты были возможны.

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

===================================

Отредактировано после комментария ниже: Демо

Интеграционный тест или функциональный тест должен иметь дело с БД / источником напрямую. Никаких издевательств. Так что это шаги. Вы хотите проверить getEmployee (emp_id) . Все эти 5 шагов ниже выполняются в одном методе тестирования.

  1. Drop DB
  2. Создание БД, заполнение ролей и других данных.
  3. Создать запись сотрудника с идентификатором
  4. Используйте этот идентификатор и вызовите getEmployee (emp_id) // это может быть вызов api-url (таким образом, нет необходимости поддерживать строку соединения db в тестовом проекте, и мы могли бы протестировать почти всю среду, просто изменив домен имена)
  5. Теперь Assert () / Проверить, верны ли возвращенные данные

    Это доказывает, что getEmployee () работает. Шаги до 3 требуют, чтобы код использовался только в тестовом проекте. Шаг 4 вызывает код приложения. Я имел в виду, что создание сотрудника (шаг 2) должно выполняться с помощью кода тестового проекта, а не кода приложения. Если существует код приложения для создания сотрудника (например: CreateEmployee () ), его не следует использовать. Точно так же, когда мы тестируем CreateEmployee () , тогда GetEmployee () код приложения не должен использоваться. У нас должен быть тестовый код проекта для извлечения данных из таблицы.

Таким образом, нет насмешек! Причиной удаления и создания БД является предотвращение повреждения данных в БД. При нашем подходе тест пройдет независимо от того, сколько раз мы его запустим.

Специальный совет: на шаге 5 getEmployee () возвращает объект сотрудника. Если позже разработчик удалит или изменит имя поля, тест будет прерван. Что если разработчик добавит новое поле позже? И он / она забывает добавить тест для этого (утверждать)? Тест не поднимет. Решение состоит в том, чтобы добавить проверку количества полей. Например: объект Сотрудник имеет 4 поля (Имя, Фамилия, Обозначение, Пол). Таким образом, число утверждений поля объекта сотрудника равно 4. Поэтому при добавлении нового поля наш тест не пройден из-за количества и напоминает разработчику добавить поле подтверждения для вновь добавленного поля.

И это отличная статья, в которой обсуждаются преимущества интеграционного тестирования над модульным тестированием , потому что "модульное тестирование убивает!"(сказано)

9 голосов
/ 11 января 2012

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

Так что в случае методов Controller, которые возвращают ActionResults, очень полезно проверить значение возвращенного ActionResult.

Взгляните на раздел 'Создание модульных тестов для контроллеров' , где приведены некоторые очень наглядные примеры с использованием Moq.

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

[TestMethod]
public void CreateInvalidContact()
{
    // Arrange
    var contact = new Contact();
    _service.Expect(s => s.CreateContact(contact)).Returns(false);
    var controller = new ContactController(_service.Object);

    // Act
    var result = (ViewResult)controller.Create(contact);

    // Assert
    Assert.AreEqual("Create", result.ViewName);
}
8 голосов
/ 11 января 2012

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

Я предпочитаю интеграционные тесты - я начинаю не с конкретного контроллера, а с URL-адреса и проверяю, что возвращаемая модель имеет правильные значения. С помощью Ivonna тест может выглядеть следующим образом:

var response = new TestSession().Get("/Users/List");
Assert.IsInstanceOf<UserListModel>(response.Model);

var model = (UserListModel) response.Model;
Assert.AreEqual(1, model.Users.Count);

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

1 голос
/ 11 января 2012

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...