有没有办法测试MVVMLight中属性的RaiseChanged使用?

时间:2014-04-22 13:33:15

标签: wpf unit-testing mvvm-light

我的viewmodel中有很多属性,它们中的逻辑非常少但是它们中有RaisePropertyChanged()方法来刷新GUI。即。

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

我开始怀疑是否应该通过单元测试确认已调用RaisePropertyChanged()方法。如果我忘了将它放在属性中,GUI就不会刷新,应用程序就会出现错误......所以应该进行单元测试。但是你如何测试呢?

总而言之......我是否因为对这种逻辑进行单元测试而好战?如果我不是激进的......有什么好的方法来测试它?

3 个答案:

答案 0 :(得分:2)

你是激进的吗?这不是一个容易回答的问题。我们测试了大部分属性改变的事件,其中有很多,我不确定那些测试中有多少价值。我的意思是,如果我们删除它们并在将来停止编写它们,我们会开始看到更多的错误,甚至一旦你使用客户端就会发现任何不明显的错误吗?说实话,答案可能是否定的。相反,它们是易于编写的测试,当然也不会受到伤害。

无论如何,是的,有一个非常好的方法来做到这一点(不得不做一些小的tweek,所以不能保证代码将编译,但应该使概念清楚):

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

用法:

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

当帮助者被处置时,期望被询问,如果他们没有按照他们被定义的顺序得到满足,则测试将失败。

我们还有一个版本,只需要满足期望,而不是确切地满足它们(即可以引发其他属性更改事件)并且这不依赖于顺序。

仅供参考 - 如果我是你,我会考虑放弃MVVMLight并转向Caliburn.Micro,它是一个不同的联盟。

答案 1 :(得分:0)

您可以轻松测试:

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

它认为为ViewModel基类编写测试是有用的,但对于所有引发更改的属性都没有用,这太过极了

答案 2 :(得分:0)

如果您定位.NET Framework >= 4.5,则可以继承ViewModelBase并编写帮助方法:

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

您的属性代码如下:

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

现在你只需要在单元测试中断言属性值。