วิธีการจำลอง ModelState.IsValid โดยใช้ Moq framework?


92

ฉันกำลังตรวจสอบ ModelState.IsValidวิธีการดำเนินการของคอนโทรลเลอร์ที่สร้างพนักงานแบบนี้:

[HttpPost]
public virtual ActionResult Create(EmployeeForm employeeForm)
{
    if (this.ModelState.IsValid)
    {
        IEmployee employee = this._uiFactoryInstance.Map(employeeForm);
        employee.Save();
    }

    // Etc.
}

ฉันต้องการจำลองในวิธีการทดสอบหน่วยของฉันโดยใช้ Moq Framework ฉันพยายามล้อเลียนแบบนี้:

var modelState = new Mock<ModelStateDictionary>();
modelState.Setup(m => m.IsValid).Returns(true);

แต่สิ่งนี้ทำให้เกิดข้อยกเว้นในกรณีทดสอบหน่วยของฉัน ใครสามารถช่วยฉันที่นี่?

คำตอบ:


143

คุณไม่จำเป็นต้องเยาะเย้ยมัน หากคุณมีคอนโทรลเลอร์อยู่แล้วคุณสามารถเพิ่มข้อผิดพลาดสถานะโมเดลเมื่อเริ่มต้นการทดสอบของคุณ:

// arrange
_controllerUnderTest.ModelState.AddModelError("key", "error message");

// act
// Now call the controller action and it will 
// enter the (!ModelState.IsValid) condition
var actual = _controllerUnderTest.Index();

เราจะตั้งค่า ModelState.IsValid ให้ตีกรณีจริงได้อย่างไร? ModelState ไม่มี setter ดังนั้นเราจึงไม่สามารถทำสิ่งต่อไปนี้: _controllerUnderTest.ModelState.IsValid = true หากไม่มีสิ่งนั้นจะไม่กระทบกับพนักงาน
Karan

4
@ นิวตันก็จริงโดยปริยาย คุณไม่จำเป็นต้องระบุอะไรเพื่อตีกรณีจริง หากคุณต้องการตีกรณีเท็จคุณเพียงแค่เพิ่มข้อผิดพลาด modelstate ดังที่แสดงในคำตอบของฉัน
Darin Dimitrov

IMHO ทางออกที่ดีกว่าคือการใช้สายพานลำเลียง mvc ด้วยวิธีนี้คุณจะได้รับพฤติกรรมที่เป็นจริงมากขึ้นของคอนโทรลเลอร์ของคุณคุณควรส่งมอบการตรวจสอบโมเดลให้กับชะตากรรมนั่นคือการตรวจสอบคุณสมบัติ โพสต์ด้านล่างอธิบายสิ่งนี้ ( stackoverflow.com/a/5580363/572612 )
Vladimir Shmidt

14

ปัญหาเดียวที่ฉันมีกับวิธีแก้ปัญหาข้างต้นคือมันไม่ได้ทดสอบโมเดลหากฉันตั้งค่าแอตทริบิวต์ ฉันตั้งค่าคอนโทรลเลอร์ด้วยวิธีนี้

private HomeController GenerateController(object model)
    {
        HomeController controller = new HomeController()
        {
            RoleService = new MockRoleService(),
            MembershipService = new MockMembershipService()
        };
        MvcMockHelpers.SetFakeAuthenticatedControllerContext(controller);

        // bind errors modelstate to the controller
        var modelBinder = new ModelBindingContext()
        {
            ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
            ValueProvider = new NameValueCollectionValueProvider(new NameValueCollection(), CultureInfo.InvariantCulture)
        };
        var binder = new DefaultModelBinder().BindModel(new ControllerContext(), modelBinder);
        controller.ModelState.Clear();
        controller.ModelState.Merge(modelBinder.ModelState);
        return controller;
    }

วัตถุ modelBinder คือวัตถุที่ทดสอบความถูกต้องของโมเดล ด้วยวิธีนี้ฉันสามารถกำหนดค่าของวัตถุและทดสอบได้


1
ดีมากนี่คือสิ่งที่ฉันกำลังมองหา ฉันไม่รู้ว่ามีกี่คนที่โพสต์คำถามเก่า ๆ แบบนี้ แต่มันมีค่าสำหรับฉัน ขอบคุณ.
W.Jackson

ดูเหมือนจะเป็นทางออกที่ยอดเยี่ยม แต่ในปี 2559 :)
แมตต์

2
ไม่ดีกว่าที่จะทดสอบแบบจำลองแยกกับสิ่งเช่นนี้? stackoverflow.com/a/4331964/3198973
RubberDuck

2
แม้ว่านี่จะเป็นวิธีแก้ปัญหาที่ชาญฉลาด แต่ฉันก็เห็นด้วยกับ @RubberDuck เพื่อให้เป็นการทดสอบหน่วยแยกจริงการตรวจสอบความถูกต้องของแบบจำลองควรเป็นการทดสอบของตัวเองในขณะที่การทดสอบคอนโทรลเลอร์ควรมีการทดสอบของตัวเอง หากโมเดลเปลี่ยนไปละเมิดการตรวจสอบ ModelBinder การทดสอบคอนโทรลเลอร์ของคุณจะล้มเหลวซึ่งเป็นผลบวกที่ผิดพลาดเนื่องจากตรรกะของคอนโทรลเลอร์ไม่เสีย หากต้องการทดสอบ ModelStateDictionary ที่ไม่ถูกต้องเพียงเพิ่มข้อผิดพลาด ModelState ปลอมสำหรับการตรวจสอบ ModelState.IsValid จะล้มเหลว
xDaevax

2

คำตอบของ uadrive ทำให้ฉันมีส่วนร่วม แต่ก็ยังมีช่องว่างอยู่ หากไม่มีข้อมูลใด ๆ ในอินพุตไปยังnew NameValueCollectionValueProvider()ตัวยึดแบบจำลองจะผูกคอนโทรลเลอร์กับโมเดลที่ว่างเปล่าไม่ใช่กับไฟล์modelอ็อบเจ็กต์

ไม่เป็นไร - เพียงแค่ทำให้โมเดลของคุณเป็นอนุกรมNameValueCollectionแล้วส่งผ่านไปยังตัวNameValueCollectionValueProviderสร้าง ก็ไม่เชิง น่าเสียดายที่มันใช้ไม่ได้ในกรณีของฉันเนื่องจากโมเดลของฉันมีคอลเล็กชันและไฟล์NameValueCollectionValueProviderไม่สามารถเล่นกับคอลเลกชันได้ดี

การJsonValueProviderFactoryมาช่วยที่นี่แม้ว่า สามารถใช้ได้DefaultModelBinderตราบเท่าที่คุณระบุประเภทเนื้อหาเป็น"application/json"และส่งออบเจ็กต์ JSON ที่เป็นอนุกรมของคุณไปยังสตรีมอินพุตของคำขอของคุณ (โปรดทราบว่าเนื่องจากสตรีมอินพุตนี้เป็นสตรีมหน่วยความจำจึงไม่ควรปล่อยทิ้งไว้เป็นหน่วยความจำ สตรีมไม่ยึดทรัพยากรภายนอกใด ๆ ):

protected void BindModel<TModel>(Controller controller, TModel viewModel)
{
    var controllerContext = SetUpControllerContext(controller, viewModel);
    var bindingContext = new ModelBindingContext
    {
        ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => viewModel, typeof(TModel)),
        ValueProvider = new JsonValueProviderFactory().GetValueProvider(controllerContext)
    };

    new DefaultModelBinder().BindModel(controller.ControllerContext, bindingContext);
    controller.ModelState.Clear();
    controller.ModelState.Merge(bindingContext.ModelState);
}

private static ControllerContext SetUpControllerContext<TModel>(Controller controller, TModel viewModel)
{
    var controllerContext = A.Fake<ControllerContext>();
    controller.ControllerContext = controllerContext;
    var json = new JavaScriptSerializer().Serialize(viewModel);
    A.CallTo(() => controllerContext.Controller).Returns(controller);
    A.CallTo(() => controllerContext.HttpContext.Request.InputStream).Returns(new MemoryStream(Encoding.UTF8.GetBytes(json)));
    A.CallTo(() => controllerContext.HttpContext.Request.ContentType).Returns("application/json");
    return controllerContext;
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.