Bagaimana cara memalsukan ModelState.IsValid menggunakan kerangka kerja Moq?

91

Saya memeriksa ModelState.IsValiddalam metode tindakan pengontrol saya yang membuat Karyawan seperti ini:

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

    // Etc.
}

Saya ingin mengejeknya dalam metode pengujian unit saya menggunakan Moq Framework. Saya mencoba mengejeknya seperti ini:

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

Tapi ini menimbulkan pengecualian dalam kasus uji unit saya. Adakah yang bisa membantu saya di sini?

Mazen
sumber

Jawaban:

142

Anda tidak perlu mengejeknya. Jika Anda sudah memiliki pengontrol, Anda dapat menambahkan kesalahan status model saat menginisialisasi pengujian Anda:

// 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();
Darin Dimitrov
sumber
bagaimana kita mengatur ModelState.IsValid untuk mencapai kasus yang sebenarnya? ModelState tidak memiliki penyetel, dan karenanya kami tidak dapat melakukan hal berikut: _controllerUnderTest.ModelState.IsValid = true. Tanpa itu, itu tidak akan memukul karyawan
Karan
4
@Newton, ini benar secara default. Anda tidak perlu menentukan apa pun untuk mencapai kasus yang sebenarnya. Jika Anda ingin mendapatkan kasus palsu Anda cukup menambahkan kesalahan modelstate seperti yang ditunjukkan dalam jawaban saya.
Darin Dimitrov
Solusi IMHO Lebih baik adalah dengan menggunakan konveyor mvc. Dengan cara ini Anda mendapatkan perilaku yang lebih realistis dari pengontrol Anda, Anda harus mengirimkan validasi model ke destiny - validasi atribut. Posting di bawah ini menjelaskan ini ( stackoverflow.com/a/5580363/572612 )
Vladimir Shmidt
13

Satu-satunya masalah yang saya miliki dengan solusi di atas adalah bahwa itu tidak benar-benar menguji model jika saya menetapkan atribut. Saya mengatur pengontrol saya dengan cara ini.

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;
    }

Objek modelBinder adalah objek yang menguji validitas model. Dengan cara ini saya bisa mengatur nilai objek dan mengujinya.

uadrive
sumber
1
Sangat bagus, inilah yang saya cari. Saya tidak tahu berapa banyak orang yang memposting pertanyaan lama seperti ini, tetapi saya sangat menghargai Anda. Terima kasih.
W. Jackson
Sepertinya solusi yang bagus, masih di 2016 :)
Matt
2
Bukankah lebih baik menguji model secara terpisah dengan sesuatu seperti ini? stackoverflow.com/a/4331964/3198973
RubberDuck
2
Meskipun ini adalah solusi yang cerdas, saya setuju dengan @RubberDuck. Agar ini menjadi pengujian unit yang sebenarnya dan terisolasi, validasi model harus menjadi pengujiannya sendiri, sedangkan pengujian pengontrol harus memiliki pengujiannya sendiri. Jika model berubah melanggar validasi ModelBinder, pengujian pengontrol Anda akan gagal, yang merupakan positif palsu karena logika pengontrol tidak rusak. Untuk menguji ModelStateDictionary yang tidak valid, cukup tambahkan kesalahan ModelState palsu untuk pemeriksaan ModelState.IsValid gagal.
xDaevax
2

Jawaban uadrive membantu saya, tetapi masih ada beberapa celah. Tanpa data apa pun di input ke new NameValueCollectionValueProvider(), pengikat model akan mengikat pengontrol ke model kosong, bukan ke modelobjek.

Tidak apa-apa - cukup buat model Anda NameValueCollectionmenjadi serial, lalu teruskan ke NameValueCollectionValueProviderkonstruktor. Yah, kurang tepat. Sayangnya, ini tidak berfungsi dalam kasus saya karena model saya berisi koleksi, dan tidak berfungsi NameValueCollectionValueProviderdengan baik dengan koleksi.

Tapi JsonValueProviderFactorydatang untuk menyelamatkannya di sini. Ini dapat digunakan DefaultModelBinderselama Anda menentukan jenis konten "application/json"dan meneruskan objek JSON serial Anda ke dalam aliran input permintaan Anda (Harap diperhatikan, karena aliran input ini adalah aliran memori, tidak apa-apa membiarkannya tidak dibuang, sebagai memori aliran tidak berpegang pada sumber daya eksternal):

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;
}
Rob Lyndon
sumber