Есть ли реальная ценность в модульном тестировании контроллера в ASP.NET MVC?

33

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

Есть ли реальная ценность в модульном тестировании контроллера в ASP.NET MVC?

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

public ActionResult Create(MyModel model)
{
    // start error list
    var errors = new List<string>();

    // check model state based on data annotations
    if(ModelState.IsValid)
    {
        // call a service method
        if(this._myService.CreateNew(model, Request.UserHostAddress, ref errors))
        {
            // all is well, data is saved, 
            // so tell the user they are brilliant
            return View("_Success");
        }
    }

    // add errors to model state
    errors.ForEach(e => ModelState.AddModelError("", e));

    // return view
    return View(model);
}

Большая часть тяжелой работы выполняется либо конвейером MVC, либо моей сервисной библиотекой.

Так что, возможно, вопросы могут быть:

  • Какова будет ценность модульного тестирования этого метода?
  • это не сломалось бы Request.UserHostAddressи ModelStateс NullReferenceException? Должен ли я пытаться издеваться над этим?
  • если бы я рефракторил этот метод в многократно используемый «помощник» (что мне, вероятно, следовало бы, учитывая, сколько раз я делаю это!), было бы полезным тестирование, даже если все, что я действительно проверяю, это в основном «конвейер», который, по-видимому, был проверен с точностью до дюйма его жизни Microsoft?

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

[TestMethod]
public void Test_Home_Index()
{
    var controller = new HomeController();
    var expected = "Index";
    var actual = ((ViewResult)controller.Index()).ViewName;
    Assert.AreEqual(expected, actual);
}

Очевидно, я туплю с этим преувеличенно бессмысленным примером, но есть ли у кого-нибудь еще мудрость, чтобы добавить сюда?

С нетерпением жду этого ... Спасибо.

LiverpoolsNumber9
источник
Я думаю, что RoI (возврат инвестиций) в этом конкретном тесте не стоит усилий, если у вас нет бесконечного времени и денег. Я бы написал тесты, которые Кевин указал для проверки вещей, которые с большей вероятностью могут сломаться, или поможет вам в рефакторинге чего-либо с уверенностью или в обеспечении того, что распространение ошибок происходит, как ожидается. Конвейерные тесты, если это необходимо, могут проводиться на более глобальном / инфраструктурном уровне, а на уровне отдельных методов не будут иметь большого значения. Не сказать, что они не имеют значения, но "мало". Так что, если в вашем случае это дает хороший RoI, сделайте это, иначе, сначала поймайте большую рыбу!
Mrchief

Ответы:

18

Даже для чего-то такого простого, юнит-тест будет служить нескольким целям

  1. Уверенность в том, что было написано, соответствует ожидаемому результату. Может показаться тривиальным убедиться, что он возвращает правильное представление, но результат является объективным свидетельством того, что требование было выполнено
  2. Регрессионное тестирование. Если метод Create нужно изменить, у вас все еще есть модульный тест для ожидаемого результата. Да, выходные данные могут изменяться вместе, и это приводит к хрупкому тесту, но это все еще проверка против неуправляемого контроля изменений

Для этого конкретного действия я бы проверил следующее

  1. Что произойдет, если _myService имеет значение null?
  2. Что произойдет, если _myService.Create выдает исключение, оно генерирует конкретные для обработки?
  3. Возвращает ли успешное _myService.Create представление _Success?
  4. Распространяются ли ошибки до ModelState?

Вы указали проверку Request и Model для NullReferenceException, и я думаю, что ModelState.IsValid позаботится об обработке NullReference для Model.

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

Kevin
источник
Привет, Кевин, спасибо, что нашли время ответить. Я собираюсь оставить это на некоторое время, чтобы посмотреть, придет ли кто-нибудь еще с чем-нибудь, но пока ваш самый логичный / ясный.
LiverpoolsNumber9
Spifty. Рад, что это помогло тебе.
Кевин
3

Мои контроллеры тоже очень маленькие. Большая часть «логики» в контроллерах обрабатывается с использованием атрибутов фильтра (встроенных и рукописных). Так что мой контроллер обычно имеет только несколько заданий:

  • Создание моделей из строк HTTP-запросов, значений форм и т. Д.
  • Выполните некоторые основные проверки
  • Позвоните в мои данные или бизнес-уровень
  • Создать ActionResult

Большая часть привязки модели выполняется автоматически ASP.NET MVC. DataAnnotations обрабатывают большую часть проверки для меня тоже.

Даже с таким небольшим количеством тестов, я все еще обычно пишу их. По сути, я проверяю, что мои репозитории вызываются и что ActionResultвозвращается правильный тип. У меня есть удобный метод для того, ViewResultчтобы убедиться, что верный путь просмотра возвращен, и модель представления выглядит так, как я ожидаю. У меня есть другой для проверки правильного контроллера / действие установлено для RedirectToActionResult. У меня есть другие тесты JsonResultи т. Д.

К сожалению, результатом подкласса Controllerкласса является то, что он предоставляет множество удобных методов, которые используют HttpContextвнутренне. Это затрудняет юнит-тестирование контроллера. По этой причине я обычно помещаю HttpContext-зависимые вызовы за интерфейсом и передаю этот интерфейс конструктору контроллера (я использую веб-расширение Ninject для создания своих контроллеров для меня). Обычно в этом интерфейсе я использую вспомогательные свойства для доступа к сеансу, настройкам конфигурации, IPrinciple и помощникам URL.

Это требует должной осмотрительности, но, думаю, оно того стоит.

Трэвис Паркс
источник
Спасибо, что нашли время ответить, но сразу 2 вопроса. Во-первых, «вспомогательные методы» в модульных тестах очень опасны. Во-вторых, «проверить, как называются мои репозитории» - вы имеете в виду через внедрение зависимостей?
LiverpoolsNumber9
Почему удобные методы опасны? У меня есть BaseControllerTestsкласс, где они все живут. Я копирую свои репозитории. Я подключаю их с помощью Ninject.
Трэвис Паркс
Что произойдет, если вы допустили ошибку или неверное предположение в своем помощнике / помощниках? Другой момент заключался в том, что только интеграционный тест (то есть сквозной) может «проверить», называются ли ваши репозитории. В модульном тесте вы все равно «обновляете» или издеваетесь над своими репозиториями.
LiverpoolsNumber9
Вы передаете репозиторий конструктору. Вы высмеиваете это во время теста. Вы убедитесь, что макет действует так, как ожидалось. Хелперы просто разбирают ActionResults, чтобы проверить переданные URL, модели и т. Д.
Travis Parks
Хорошо, честно - я немного неправильно понял, что вы имели в виду под «проверить, что мои репозитории называются».
LiverpoolsNumber9
2

Очевидно, что некоторые контроллеры намного сложнее, но основаны исключительно на вашем примере:

Что произойдет, если myService выдает исключение?

Как примечание стороны.

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

В вашем примере:

вместо ошибок ref, например, do (string s) => ModelState.AddModelError ("", s).

Майкл
источник
Стоит отметить, что это предполагает, что ваш сервис находится в одном и том же приложении, иначе проблемы с сериализацией вступят в игру.
Майкл
Служба будет в отдельной DLL. Но, в любом случае, вы, вероятно, правы в отношении "реф" С другой стороны, не имеет значения, выдает ли myService исключение. Я не тестирую свой сервис - я бы тестировал методы в нем отдельно. Я говорю о чистом тестировании «модуля» ActionResult с помощью (возможно) поддельного myService.
LiverpoolsNumber9
Есть ли у вас соотношение 1: 1 между вашим сервисом и вашим контроллером? Если нет, используют ли некоторые контроллеры несколько сервисных вызовов? Если так, вы могли бы проверить эти взаимодействия?
Майкл
Нет. В конце концов, сервисные методы принимают входные данные (обычно модель представления или даже просто строки / целые числа), они «делают вещи», а затем возвращают bool / errors, если false. Нет прямой связи между контроллерами и сервисным уровнем. Они полностью отделены.
LiverpoolsNumber9
Да, я понимаю, что я пытаюсь понять реляционную модель между контроллерами и сервисным уровнем - при условии, что у каждого контроллера нет соответствующего метода сервиса, было бы понятно, что некоторым контроллерам может понадобиться использовать более одного метода обслуживания?
Майкл