magnusarinell
magnusarinell

Reputation: 1157

Failing a controller unit test on ModelState.AddModelError

I am unit testing an MVC controller. While I can assert the model state after the test has run, I would prefer it if I could make the test fail when the error is being added to the ModelState. My idea was to mock ModelState but it has no setter.

Does anyone know how to listen for ModelState.AddModelError in a unit test?

Upvotes: 1

Views: 543

Answers (2)

magnusarinell
magnusarinell

Reputation: 1157

Fixed it. First attempt using:

var f = typeof(ViewDataDictionary).GetField("_modelState", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.SetField);
var modelStateDictionary = MockRepository.GenerateMock<ModelStateDictionaryMock>();
f.SetValue(controller.ViewData, modelStateDictionary);

with

public abstract class ModelStateDictionaryMock : ModelStateDictionary
{
    public abstract int Count { get; }

    public abstract bool IsReadOnly { get; }

    public abstract bool IsValid { get; }

    public abstract ICollection<string> Keys { get; }

    public abstract ICollection<ModelState> Values { get; }

    public abstract ModelState this[string key] { get; set; }

    public abstract void Add(KeyValuePair<string, ModelState> item);

    public abstract void Add(string key, ModelState value);

    public abstract void AddModelError(string key, Exception exception);

    public abstract void AddModelError(string key, string errorMessage);

    public abstract void Clear();

    public abstract bool Contains(KeyValuePair<string, ModelState> item);

    public abstract bool ContainsKey(string key);

    public abstract void CopyTo(KeyValuePair<string, ModelState>[] array, int arrayIndex);

    public abstract IEnumerator<KeyValuePair<string, ModelState>> GetEnumerator();

    public abstract bool IsValidField(string key);

    public abstract void Merge(ModelStateDictionary dictionary);

    public abstract bool Remove(KeyValuePair<string, ModelState> item);

    public abstract bool Remove(string key);

    public abstract void SetModelValue(string key, ValueProviderResult value);

    public abstract bool TryGetValue(string key, out ModelState value);
}

However that didn't work since doing a mock in this way didnt work. So I solved it using Microsoft Fakes instead:

System.Web.Mvc.Fakes.ShimModelStateDictionary.AllInstances.AddModelErrorStringString =
    (dictionary, key, errorMessage) =>
    {
        Assert.Fail(errorMessage);
    };

Upvotes: 1

Luke
Luke

Reputation: 23680

Wrap a class around your ModelState class:

public class ModelStateWrapper : IValidationDictionary
{
    private ModelStateDictionary ModelStateDictionary;

    public ModelStateWrapper(ModelStateDictionary modelStateDictionary)
    {
        this.ModelStateDictionary = modelStateDictionary;
    }

    /// <summary>
    /// Adds a validation error to the model state dictionary
    /// </summary>
    /// <param name="key">The key for the field</param>
    /// <param name="errorMessage">The error message</param>
    public void AddError(string key, string errorMessage)
    {
        this.ModelStateDictionary.AddModelError(key, errorMessage);
    }

    /// <summary>
    /// Determines whether there are error messages in the model state dictionary
    /// </summary>
    public bool IsValid
    {
        get
        {
            return this.ModelStateDictionary.IsValid;
        }
    }
}

The interface can look like this:

public interface IValidationDictionary
{
    void AddError(string key, string errorMessage);
    bool IsValid { get; }
}

Then within your class, assign the ModelState within the constructor of the controller:

public HomeController : IController
{
    private IValidationDictionary validationDictionary;

    public HomeController()
    {
        this.validationDictionary = new ModelStateWrapper(this.ModelState);
    }

    public ActionResult Index()
    {
        this.myService.CallSomething(this.validationDictionary)
    }
}

When you pass the instance of this.validationDictionary to a service, it will augment the original ModelState from your controller.

Upvotes: 1

Related Questions