Question

I have lots of properties in my viewmodels that have very little logic in them but they have the RaisePropertyChanged() method in them to refresh the GUI. i.e.

private bool _StatesIsSelected;
public bool StatesIsSelected
{
    get { return _StatesIsSelected; }
    set
    {
        _StatesIsSelected = value;
        RaisePropertyChanged("StatesIsSelected");
    }
}

I am starting to wonder if I should have unit tests confirming that the RaisePropertyChanged() method has been called. If I forgot to put it in the property the GUI wouldn't get refreshed and the application would have a bug...so it should have a unit test. But how do you test that?

So to sum it up....Am I being to militant about having unit tests for this logic? And if I am not being to militant...what is there a good way to test for this?

Était-ce utile?

La solution

Are you being militant? That is not really an easy question to answer. We do test the majority of our property changed events, of which there are a lot, and I'm not really sure how much value there is in those tests. By that I mean if we removed them and stopped writing them in the future would we start seeing more bugs, or even any that wouldn't be pretty obvious as soon as you used the client? To be honest the answer is probably no. Conversely they are easy tests to write and certainly don't hurt.

Anyway, yes there is a very nice way to do this (had to make a few minor tweeks, so can't guarantee the code will compile, but should make the concepts clear):

public static class PropertyChangedTestHelperFactory
{
    /// <summary>
    /// Factory method for creating <see cref="PropertyChangedTestHelper{TTarget}"/> instances.
    /// </summary>
    /// <param name="target">
    /// The target.
    /// </param>
    /// <typeparam name="TTarget">
    /// The target type.
    /// </typeparam>
    /// <returns>
    /// The <see cref="PropertyChangedTestHelper{TTarget}"/>
    /// </returns>
    public static PropertyChangedTestHelper<TTarget> CreatePropertyChangedHelper<TTarget>(
        this TTarget target)
        where TTarget : INotifyPropertyChanged
    {
        return new PropertyChangedTestHelper<TTarget>(target);
    }
}

public sealed class PropertyChangedTestHelper<TTarget> : IDisposable
    where TTarget : INotifyPropertyChanged
{
    /// <summary>
    /// This list contains the expected property names that should occur in property change notifications
    /// </summary>
    private readonly Queue<string> propertyNames = new Queue<string>(); 

    /// <summary>
    /// The target of the helper
    /// </summary>
    private readonly TTarget target;

    /// <summary>
    /// Initialises a new instance of the <see cref="StrictPropertyChangedTestHelper{TTarget}"/> class.
    /// </summary>
    /// <param name="target">The target.</param>
    public PropertyChangedTestHelper(TTarget target)
    {
        this.target = target;
        this.target.PropertyChanged += this.HandleTargetPropertyChanged;
    }

    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    public void Dispose()
    {
        this.target.PropertyChanged -= this.HandleTargetPropertyChanged;

        if (this.propertyNames.Count != 0)
        {
            Assert.Fail("Property change notification {0} was not raised", this.propertyNames.Peek());
        }
    }

    /// <summary>
    /// Sets an expectation that a refresh change notification will be raised.
    /// </summary>
    public void ExpectRefresh()
    {
        this.propertyNames.Enqueue(string.Empty);
    }

    /// <summary>
    /// Sets an expectation that a property change notification will be raised.
    /// </summary>
    /// <typeparam name="TProperty">The type of the property.</typeparam>
    /// <param name="propertyExpression">The property expression.</param>
    public void Expect<TProperty>(Expression<Func<TTarget, TProperty>> propertyExpression)
    {
        this.propertyNames.Enqueue(((MemberExpression)propertyExpression.Body).Member.Name);
    }

    /// <summary>
    /// Handles the target property changed event.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="e">The <see cref="PropertyChangedEventArgs"/> instance containing the event data.</param>
    private void HandleTargetPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (this.propertyNames.Count == 0)
        {
            Assert.Fail("Unexpected property change notification {0}", e.PropertyName);
        }

        var expected = this.propertyNames.Dequeue();
        var propertyName = (e.PropertyName ?? string.Empty).Trim();
        if (propertyName != expected)
        {
            Assert.Fail("Out of order property change notification, expected '{0}', actual '{1}'", expected, propertyName);
        }
    }
}

Usage:

[TestMethod]
public void StatesIsSelected_RaisesIsValidChangeNotification()
{
    // Arrange
    var target = new SomeViewModel();

    using (var helper = target.CreatePropertyChangedHelper())
    {
        helper.Expect(item => item.StatesIsSelected);

        // Act
        target.StatesIsSelected = true;

        // Assert
    }
}

When the helper is disposed the expectations are interrogated and the test will fail if they are not all met in the order they were defined.

We also have a Weak version that only requires that the expectations are met, not that they are met exactly (i.e. other property change events could be raised) and that is not order dependent.

FYI - if I were you I'd think about ditching MVVMLight and moving to Caliburn.Micro, its in a different league.

Autres conseils

you can test it easily:

void TestMethod()
{
   Container container = new Container();
   bool isRaised = false;
   container.PropertyChanged += (o,e) => { 
       if(e.PropertyName == "StatesIsSelected")
           isRaised = true;
       };
   container.StatesIsSelected = true;
   Assert.True(isRaised);
}

It think it would be useful to write the test for the ViewModel base class but not for all properties which raise a change, thats just too extreme

If you're targeting .NET Framework >= 4.5 you can inherit from ViewModelBase and write helper method:

public class ViewModelBaseExtended : ViewModelBase
{

    protected void TryRaisePropertyChanged<T>(ref T oldValue, T newValue,
                                             [CallerMemberName] string propertyName = "")
    {
        if (oldValue == null || !oldValue.Equals(newValue))
        {
            oldValue = newValue;
            RaisePropertyChanged(propertyName);
        }
    }
}

and your property code would look like that:

private bool _StatesIsSelected;
public bool StatesIsSelected
{
    get { return _StatesIsSelected; }
    set
    {
        TryRaisePropertyChanged(ref _StatesIsSelected, value);
    }
}

Now you would only have to assert property value in your unit tests.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top