在 C # 中引发事件的单元测试(按顺序)

我有一些引发 PropertyChanged事件的代码,我希望能够单元测试事件是否正确引发。

引发事件的代码类似于

public class MyClass : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;


protected void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}


public string MyProperty
{
set
{
if (_myProperty != value)
{
_myProperty = value;
NotifyPropertyChanged("MyProperty");
}
}
}
}

在我的单元测试中,我从下面的代码中得到了一个不错的绿色测试,它使用了委托:

[TestMethod]
public void Test_ThatMyEventIsRaised()
{
string actual = null;
MyClass myClass = new MyClass();


myClass.PropertyChanged += delegate(object sender, PropertyChangedEventArgs e)
{
actual = e.PropertyName;
};


myClass.MyProperty = "testing";
Assert.IsNotNull(actual);
Assert.AreEqual("MyProperty", actual);
}

然而,如果我尝试将属性的设置链接在一起,如下所示:

public string MyProperty
{
set
{
if (_myProperty != value)
{
_myProperty = value;
NotifyPropertyChanged("MyProperty");
MyOtherProperty = "SomeValue";
}
}
}


public string MyOtherProperty
{
set
{
if (_myOtherProperty != value)
{
_myOtherProperty = value;
NotifyPropertyChanged("MyOtherProperty");
}
}
}

我对事件的测试失败了——它捕获的事件是 MyOtherProperty 的事件。

我非常确定事件会触发,我的用户界面的反应和它一样,但是我的委托只捕获要触发的最后一个事件。

所以我在想:
1. 我测试事件的方法正确吗?
2. 引发 被锁住了事件的方法正确吗?

94619 次浏览

您所做的一切都是正确的,只要您希望测试询问“最后引发的事件是什么?”

您的代码按照这个顺序触发这两个事件

  • 财产变更(... “我的财产”...)
  • 属性更改(... “ MyOther 属性”...)

这是否“正确”取决于这些事件的目的。

如果您想测试引发的事件数量以及它们的引发顺序,您可以轻松地扩展现有的测试:

[TestMethod]
public void Test_ThatMyEventIsRaised()
{
List<string> receivedEvents = new List<string>();
MyClass myClass = new MyClass();


myClass.PropertyChanged += delegate(object sender, PropertyChangedEventArgs e)
{
receivedEvents.Add(e.PropertyName);
};


myClass.MyProperty = "testing";
Assert.AreEqual(2, receivedEvents.Count);
Assert.AreEqual("MyProperty", receivedEvents[0]);
Assert.AreEqual("MyOtherProperty", receivedEvents[1]);
}

如果您正在进行 TDD,那么事件测试可以开始生成重复代码的 很多。我编写了一个事件监视器,它为这些情况提供了一种更清晰的单元测试编写方法。

var publisher = new PropertyChangedEventPublisher();


Action test = () =>
{
publisher.X = 1;
publisher.Y = 2;
};


var expectedSequence = new[] { "X", "Y" };


EventMonitor.Assert(test, publisher, expectedSequence);

有关详情,请参阅我对以下问题的回答。

使用反射在 C # 中引发事件的单元测试

下面是一个略有改动的 Andrew 代码,它不仅记录引发事件的顺序,而且计算特定事件被调用的次数。虽然它是基于他的代码,但我发现它在我的测试中更有用。

[TestMethod]
public void Test_ThatMyEventIsRaised()
{
Dictionary<string, int> receivedEvents = new Dictionary<string, int>();
MyClass myClass = new MyClass();


myClass.PropertyChanged += delegate(object sender, PropertyChangedEventArgs e)
{
if (receivedEvents.ContainsKey(e.PropertyName))
receivedEvents[e.PropertyName]++;
else
receivedEvents.Add(e.PropertyName, 1);
};


myClass.MyProperty = "testing";
Assert.IsTrue(receivedEvents.ContainsKey("MyProperty"));
Assert.AreEqual(1, receivedEvents["MyProperty"]);
Assert.IsTrue(receivedEvents.ContainsKey("MyOtherProperty"));
Assert.AreEqual(1, receivedEvents["MyOtherProperty"]);
}

这是非常旧的,甚至可能不会被读,但有一些很酷的新。我已经创建了一个 INPC Tracer 类,它允许:

[Test]
public void Test_Notify_Property_Changed_Fired()
{
var p = new Project();


var tracer = new INCPTracer();


// One event
tracer.With(p).CheckThat(() => p.Active = true).RaisedEvent(() => p.Active);


// Two events in exact order
tracer.With(p).CheckThat(() => p.Path = "test").RaisedEvent(() => p.Path).RaisedEvent(() => p.Active);
}

见要点: https://gist.github.com/Seikilos/6224204

基于本文,我创建了这个简单的断言助手:

private void AssertPropertyChanged<T>(T instance, Action<T> actionPropertySetter, string expectedPropertyName) where T : INotifyPropertyChanged
{
string actual = null;
instance.PropertyChanged += delegate (object sender, PropertyChangedEventArgs e)
{
actual = e.PropertyName;
};
actionPropertySetter.Invoke(instance);
Assert.IsNotNull(actual);
Assert.AreEqual(propertyName, actual);
}

使用这个方法助手,测试变得非常简单。

[TestMethod()]
public void Event_UserName_PropertyChangedWillBeFired()
{
var user = new User();
AssertPropertyChanged(user, (x) => x.UserName = "Bob", "UserName");
}

不要为每个成员编写测试-这是很多工作

(也许这个解决方案并不适用于所有情况,但它显示了一种可能的方法。您可能需要根据您的用例对其进行调整)

可以使用库中的反射来测试成员是否都正确地响应属性更改事件:

  • 在 setter 访问时引发 PropertyChanged 事件
  • 事件引发正确(属性名等于引发事件的参数)

下面的代码可用作库,并演示如何测试下列泛型类

using System.ComponentModel;
using System.Linq;


/// <summary>
/// Check if every property respons to INotifyPropertyChanged with the correct property name
/// </summary>
public static class NotificationTester
{
public static object GetPropertyValue(object src, string propName)
{
return src.GetType().GetProperty(propName).GetValue(src, null);
}


public static bool Verify<T>(T inputClass) where T : INotifyPropertyChanged
{
var properties = inputClass.GetType().GetProperties().Where(x => x.CanWrite);
var index = 0;


var matchedName = 0;
inputClass.PropertyChanged += (o, e) =>
{
if (properties.ElementAt(index).Name == e.PropertyName)
{
matchedName++;
}


index++;
};


foreach (var item in properties)
{
// use setter of property
item.SetValue(inputClass, GetPropertyValue(inputClass, item.Name));
}


return matchedName == properties.Count();
}
}

现在可以将类的测试编写为。(也许你想把测试分成“ event is there”和“ event  用正確的名稱引發”-你可以自己做)

[TestMethod]
public void EveryWriteablePropertyImplementsINotifyPropertyChangedCorrect()
{
var viewModel = new TestMyClassWithINotifyPropertyChangedInterface();
Assert.AreEqual(true, NotificationTester.Verify(viewModel));
}

同学们

using System.ComponentModel;


public class TestMyClassWithINotifyPropertyChangedInterface : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;


protected void NotifyPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}


private int id;


public int Id
{
get { return id; }
set { id = value;
NotifyPropertyChanged("Id");
}
}
}

我已经延期了:

public static class NotifyPropertyChangedExtensions
{
private static bool _isFired = false;
private static string _propertyName;


public static void NotifyPropertyChangedVerificationSettingUp(this INotifyPropertyChanged notifyPropertyChanged,
string propertyName)
{
_isFired = false;
_propertyName = propertyName;
notifyPropertyChanged.PropertyChanged += OnPropertyChanged;
}


private static void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == _propertyName)
{
_isFired = true;
}
}


public static bool IsNotifyPropertyChangedFired(this INotifyPropertyChanged notifyPropertyChanged)
{
_propertyName = null;
notifyPropertyChanged.PropertyChanged -= OnPropertyChanged;
return _isFired;
}
}

这里有一个用法:

   [Fact]
public void FilesRenameViewModel_Rename_Apply_Execute_Verify_NotifyPropertyChanged_If_Succeeded_Through_Extension_Test()
{
//  Arrange
_filesViewModel.FolderPath = ConstFolderFakeName;
_filesViewModel.OldNameToReplace = "Testing";
//After the command's execution OnPropertyChanged for _filesViewModel.AllFilesFiltered should be raised
_filesViewModel.NotifyPropertyChangedVerificationSettingUp(nameof(_filesViewModel.AllFilesFiltered));
//Act
_filesViewModel.ApplyRenamingCommand.Execute(null);
// Assert
Assert.True(_filesViewModel.IsNotifyPropertyChangedFired());


}