asp.net-mvc 단위테스트 - MVC 유효성 검사에 대한 단위 테스트




유닛 시나리오 (11)

MVA 2 Preview 1에서 DataAnnotation 유효성 검사를 사용할 때 엔티티의 유효성을 검사 할 때 컨트롤러 동작이 ModelState에 올바른 오류를 넣고 있는지 테스트 할 수 있습니까?

설명 할 몇 가지 코드. 첫째, 액션 :

    [HttpPost]
    public ActionResult Index(BlogPost b)
    {
        if(ModelState.IsValid)
        {
            _blogService.Insert(b);
            return(View("Success", b));
        }
        return View(b);
    }

그리고 여기에 통과해야한다고 생각되는 실패한 단위 테스트가 있습니다 (MbUnit & Moq 사용).

[Test]
public void When_processing_invalid_post_HomeControllerModelState_should_have_at_least_one_error()
{
    // arrange
    var mockRepository = new Mock<IBlogPostSVC>();
    var homeController = new HomeController(mockRepository.Object);

    // act
    var p = new BlogPost { Title = "test" };            // date and content should be required
    homeController.Index(p);

    // assert
    Assert.IsTrue(!homeController.ModelState.IsValid);
}

나는이 질문에 덧붙여 검증을 테스트해야 하는가? 그리고 내가 이런 식으로 테스트해야 하는가?


Answers

나는 ARM이 최상의 대답을 가지고 있다는 것에 동의한다 : 내장 된 검증이 아닌 컨트롤러의 동작을 테스트하라.

그러나 Model / ViewModel에 올바른 유효성 검사 속성이 정의되어 있는지 테스트 할 수도 있습니다. ViewModel이 다음과 같다고 가정 해 보겠습니다.

public class PersonViewModel
{
    [Required]
    public string FirstName { get; set; }
}

이 단위 테스트는 [Required] 속성의 존재 여부를 테스트합니다.

[TestMethod]
public void FirstName_should_be_required()
{
    var propertyInfo = typeof(PersonViewModel).GetProperty("FirstName");

    var attribute = propertyInfo.GetCustomAttributes(typeof(RequiredAttribute), false)
                                .FirstOrDefault();

    Assert.IsNotNull(attribute);
}

@ giles-smith의 답은 필자가 선호하는 방법이지만 구현을 단순화 할 수 있습니다.

    public static void ValidateViewModel(this Controller controller, object viewModelToValidate)
    {
        var validationContext = new ValidationContext(viewModelToValidate, null, null);
        var validationResults = new List<ValidationResult>();
        Validator.TryValidateObject(viewModelToValidate, validationContext, validationResults, true);
        foreach (var validationResult in validationResults)
        {
            controller.ModelState.AddModelError(validationResult.MemberNames.FirstOrDefault() ?? string.Empty, validationResult.ErrorMessage);
        }
    }

BlogPost 를 전달하는 대신 actions 매개 변수를 FormCollection 으로 선언 할 수도 있습니다. 그런 다음 BlogPost 직접 만들고 UpdateModel(model, formCollection.ToValueProvider()); 호출 할 수 있습니다 UpdateModel(model, formCollection.ToValueProvider()); .

이렇게하면 FormCollection 필드에 대한 유효성 검사가 트리거됩니다.

    [HttpPost]
    public ActionResult Index(FormCollection form)
    {
        var b = new BlogPost();
        TryUpdateModel(model, form.ToValueProvider());

        if (ModelState.IsValid)
        {
            _blogService.Insert(b);
            return (View("Success", b));
        }
        return View(b);
    }

비어있는 상태로 유지하려는보기 양식의 모든 필드에 대한 테스트가 널값을 추가하는지 확인하십시오.

코드를 몇 줄 추가하는 대신이 방법을 사용하면 런타임시 코드를 호출하는 방식과 비슷한 방식으로 유닛 테스트를 수행 할 수 있습니다. 또한 누군가가 int 속성에 바인딩 된 컨트롤에서 "abc"를 입력하면 어떻게 될지 테스트 할 수 있습니다.


@ giles-smith의 답변과 의견을 바탕으로 Web API의 경우 :

    public static void ValidateViewModel<TViewModel, TController>(this TController controller, TViewModel viewModelToValidate) 
        where TController : ApiController
    {
        var validationContext = new ValidationContext(viewModelToValidate, null, null);
        var validationResults = new List<ValidationResult>();
        Validator.TryValidateObject(viewModelToValidate, validationContext, validationResults, true);
        foreach (var validationResult in validationResults)
        {
            controller.ModelState.AddModelError(validationResult.MemberNames.FirstOrDefault() ?? string.Empty, validationResult.ErrorMessage);
        }
    }

위의 답변 편집보기 ...


이것은 DataAnnotations를 포기하기 때문에 질문에 정확히 답하지는 않지만 다른 사람들이 컨트롤러에 대한 테스트를 작성하는 데 도움이 될 수 있으므로 추가 할 것입니다.

System.ComponentModel.DataAnnotations에서 제공하는 유효성 검사를 사용하지 않고 여전히 AddModelError 메서드 및 다른 유효성 검사 메커니즘을 사용하여 ViewData.ModelState 개체를 사용하는 AddModelError 있습니다. 예 :

public ActionResult Create(CompetitionEntry competitionEntry)
{        
    if (competitionEntry.Email == null)
        ViewData.ModelState.AddModelError("CompetitionEntry.Email", "Please enter your e-mail");

    if (ModelState.IsValid)
    {
       // insert code to save data here...
       // ...

       return Redirect("/");
    }
    else
    {
        // return with errors
        var viewModel = new CompetitionEntryViewModel();
        // insert code to populate viewmodel here ...
        // ...


        return View(viewModel);
    }
}

여전히 DataAnnotations 를 사용하지 않고 MVC가 생성하는 Html.ValidationMessageFor() 작업을 활용할 수 있습니다. AddModelError 와 함께 사용하는 키가 유효성 검사 메시지를 위해 뷰에서 기대하는 AddModelError 일치하는지 확인해야합니다.

MVC 프레임 워크에 의해 자동으로 수행되기보다는 유효성 검사가 명시 적으로 일어나기 때문에 컨트롤러가 테스트 가능해진다.


나는 오늘 이것을 연구하고 있었고, 단위 테스트 동안 컨트롤러 액션에 대한 유효성 검사기를 발사하는 최상의 솔루션을 제공하는 것처럼 보이는 Roberto Hernández (MVP) 의이 블로그 게시물 을 발견 했습니다 . 엔터티의 유효성을 검사 할 때 ModelState에 올바른 오류가 표시됩니다.


오래된 게시물 necro에 싫어하지만 내 생각을 추가 할 줄 알았는데 (이후이 문제가 있었고 답변을 찾는 동안이 게시물을 가로 질렀다).

  1. 컨트롤러 테스트에서 유효성을 테스트하지 마십시오. MVC의 검증을 신뢰하거나 직접 작성하십시오 (즉, 다른 코드를 테스트하지 말고 코드를 테스트하십시오)
  2. 유효성 검사를 수행하려는 경우 원하는대로 수행하고, 모델 테스트에서 테스트합니다 (이 작업은 좀 더 복잡한 정규식 유효성 검사를 위해 수행합니다).

여기서 실제로 테스트하고 싶은 것은 유효성 검사가 실패 할 때 컨트롤러가 기대 한 바를 수행한다는 것입니다. 그것은 당신의 코드와 당신의 기대입니다. 그 모든 것을 테스트하고 싶다는 사실을 깨닫게되면 쉽게 테스트 할 수 있습니다.

[test]
public void TestInvalidPostBehavior()
{
    // arrange
    var mockRepository = new Mock<IBlogPostSVC>();
    var homeController = new HomeController(mockRepository.Object);
    var p = new BlogPost();

    homeController.ViewData.ModelState.AddModelError("Key", "ErrorMessage"); // Values of these two strings don't matter.  
    // What I'm doing is setting up the situation: my controller is receiving an invalid model.

    // act
    var result = (ViewResult) homeController.Index(p);

    // assert
    result.ForView("Index")
    Assert.That(result.ViewData.Model, Is.EqualTo(p));
}

ModelBinders를 사용하여 model.IsValid 값을 업데이트 할 수 있습니다.

var form = new FormCollection();
form.Add("Name", "0123456789012345678901234567890123456789");

var model = MvcModelBinder.BindModel<AddItemModel>(controller, form);

ViewResult result = (ViewResult)controller.Add(model);

내 MvcModelBinder.BindModel 메서드는 다음과 같습니다 (기본적으로 MVC 프레임 워크에서 내부적으로 사용되는 동일한 코드).

        public static TModel BindModel<TModel>(Controller controller, IValueProvider valueProvider) where TModel : class
        {
            IModelBinder binder = ModelBinders.Binders.GetBinder(typeof(TModel));
            ModelBindingContext bindingContext = new ModelBindingContext()
            {
                FallbackToEmptyPrefix = true,
                ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(TModel)),
                ModelName = "NotUsedButNotNull",
                ModelState = controller.ModelState,
                PropertyFilter = (name => { return true; }),
                ValueProvider = valueProvider
            };

            return (TModel)binder.BindModel(controller.ControllerContext, bindingContext);
        }

테스트에서 homeController.Index 메서드를 호출하면 유효성 검사를 시작하지 않는 MVC 프레임 워크를 사용하지 않으므로 ModelState.IsValid가 항상 true가됩니다. 우리 코드에서 앰비언트 유효성 검사를 사용하는 대신 컨트롤러에서 직접 헬퍼 Validate 메서드를 호출합니다. 나는 DataAnnotations (우리는 NHibernate.Validators를 사용한다)에 대한 많은 경험이 없었을 것이다. 아마도 다른 누군가가 컨트롤러 내에서 Validate를 호출하는 방법에 대한 지침을 제공 할 수있을 것이다.


유효성 검사에 관심이 있지만 구현 방법에 신경 쓰지 않는다면 DataAnnotations, ModelBinders 또는 ActionFilterAttributes를 사용하여 구현되었는지 여부에 관계없이 가장 높은 수준의 추상화에서 동작 메서드의 유효성 검사 만 신경 쓰면 다음과 같이 Xania.AspNet.Simulator nuget 패키지를 사용할 수 있습니다.

install-package Xania.AspNet.Simulator

-

var action = new BlogController()
    .Action(c => c.Index(new BlogPost()), "POST");
var modelState = action.ValidateRequest();

modelState.IsValid.Should().BeFalse();

내 생각에 당신은 단 classe의 공개 API를 테스트해야합니다.

메소드를 public으로 만들면 단위 테스트를 위해 구현 세부 사항을 노출하는 캡슐화가 중단됩니다.

좋은 공개 API는 클라이언트 코드의 즉각적인 목표를 해결하고 그 목표를 완전히 해결합니다.





asp.net-mvc unit-testing validation asp.net-mvc-2 tdd