Reputation: 5097
In an asp.net core application, I have a pair of controller methods that respond to an Edit action. One for GET, which takes a string parameter for the entity id:
public async Task<IActionResult> Edit(string id)
and the other for receiving a POST of updated entity values:
[HttpPost]
[ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditSave(string id)
Inside the postable action method, I call
var bindingSuccess = await TryUpdateModelAsync(vm);
And that works fine.
Now, I'm trying to write a test for this, but am finding that TryUpdateModelAsync
requires lots of things from HttpContext
and the controller to be fleshed out. I've tried mocking those out, but after looking at the source code for TryUpdateModelAsync
, I realize that I'd essentially need to mock everything down to the metadata, which isn't proving to be straightforward.
I'm wondering if perhaps this difficulty is telling me something: TryUpdateModelAsync
makes it hard to test, so I should refactor the controller method to not rely on this helper. Instead, I could add another parameter to the method for my viewmodel and decorate it with [FromBody]
, so the model binding would happen from the post fields when present, but I would be able to pass in a view model when testing. However, I like the TryUpdateModelAsync
method, because it does the busy work of merging the post fields into my view model. The other way I can think to accomplish the merging is write my own Merge method. Okay, no big deal, but I'd prefer not to have to do this for each entity (or reinvent the wheel writing a reflection based merger) and really, I'm wondering if I just missed the boat on how to write a unit test against this. I could fire up a whole TestServer
, like I do for integration tests, but I'm not sure this is the right direction and feels like I would just be complicating my unit tests further. However, maybe it is justified in this scenario?
I've seen answers that worked with previous versions of .net mvc, where all they needed to do was mock an IValueProvider
and attach it to the controller, but in .net core it appears the TryUpdateModelAsync
was reworked and requires more moving parts.
In sum, I see three options:
TryUpdateModelAsync
needs. This might be a dead end, it seems to
be so far.TestServer
and make this test from a little
higher altitude using an HttpClient
[FromBody]
on a view model parameter, then write my own merge
methods for each entity, there by side stepping
TryUpdateModelAsync
altogetherThese all have their drawbacks, so I'm hoping there's a 4th entry to this list that I don't see because I am ignorant.
Upvotes: 1
Views: 1746
Reputation: 247133
After looking at the source for ControllerBase, I noticed that the troublesome method in question relies on the static method ModelBindingHelper.TryUpdateModelAsync
(seriously!!!!? I thought we were more evolved by now.)
This as you have already painfully discovered makes testing your controller some what of a bother.
I'm wondering if perhaps this difficulty is telling me something
well stop wondering. It is. :)
Here is another option you may have over looked. Abstract/Adapt away that difficulty back to the depths from which it came.
public interface IModelBindingHelperAdaptor {
Task<bool> TryUpdateModelAsync<TModel>(ControllerBase controller, TModel model) where TModel : class;
}
an implementation can look like this
public class DefaultModelBindingHelperAdaptor : IModelBindingHelperAdaptor {
public virtual Task<bool> TryUpdateModelAsync<TModel>(ControllerBase controller, TModel model) where TModel : class {
return controller.TryUpdateModelAsync(model);
}
}
Inject the IModelBindingHelperAdaptor
into your controller as a dependency and let it call the demon spawned method.
var bindingSuccess = await modelBindingHelper.TryUpdateModelAsync(this, vm);
You are now free to mock your abstraction without all the tight coupling, which is what I think they should have done in the first place.
Now I assume you already know to setup the necessary things in your startup to allow for the above suggestion to work so it should not be a difficult task for you to get that up and running.
Upvotes: 4