如何测试带特定参数的特定事件是否为多触发事件之一?
问题
我有一个实现了INotifyPropertyChanged接口的对象,直接修改部分属性时会触发变更通知,但修改某一属性可能会导致其他属性作为副作用发生变更,我需要验证这类通知。
示例代码:
public class MyObject : INotifyPropertyChanged { public string MyProperty { get; set; // 为简洁省略通知代码 } public string DependentOnMyProperty { get; // 基于`MyProperty`更新值 } } var obj = new MyObject(); obj.MyProperty = "Value"; // 应触发属性变更通知:MyProperty和DependentOnMyProperty
我想编写单元测试来检测该依赖属性的事件触发情况。我希望借助测试框架的功能,但不确定怎么实现——比如Assert.Raises()似乎只会报告首个触发的事件,忽略后续的。目前我用的测试代码如下:
[Fact] public void ComputedProperty_WhenChangingDependentValue_RaisesPropertyChangedEvent() { var obj = new MyObject(); var hitCounts = new Dictionary<string, int>(); obj.PropertyChanged += (sender, args) => { if (!hitCounts.ContainsKey(args.PropertyName!)) { hitCounts[args.PropertyName] = 0; } hitCounts[args.PropertyName]++; }; obj.MyProperty = "Test Value"; Thread.Sleep(100); // 等待计算属性更新 obj.MyProperty.Should().Be("Test Value"); obj.DependentOnMyProperty.Should().Be("Value derived from Test Value"); hitCounts.Should().ContainKey(nameof(MyObject.DependentOnMyProperty)).WhoseValue.Should().Be(1); }
解决方案
1. 优化现有统计逻辑
你的思路本身没问题,可以简化代码,同时去掉不可靠的Thread.Sleep(如果依赖属性是同步更新的,完全不需要等待;异步场景下也不该用休眠)。
同步场景下的优化版本:
[Fact] public void ComputedProperty_WhenChangingDependentValue_RaisesPropertyChangedEvent() { var obj = new MyObject(); var raisedProperties = new List<string>(); obj.PropertyChanged += (_, args) => raisedProperties.Add(args.PropertyName!); obj.MyProperty = "Test Value"; // 验证属性值正确性 obj.MyProperty.Should().Be("Test Value"); obj.DependentOnMyProperty.Should().Be("Value derived from Test Value"); // 验证通知的顺序和数量 raisedProperties.Should().ContainInOrder(nameof(MyObject.MyProperty), nameof(MyObject.DependentOnMyProperty)); raisedProperties.Count(s => s == nameof(MyObject.DependentOnMyProperty)).Should().Be(1); }
2. 用FluentAssertions的事件监控简化代码
如果使用FluentAssertions,可以直接借助它的事件监控API,不用自己维护集合:
[Fact] public void ComputedProperty_WhenChangingDependentValue_RaisesPropertyChangedEvent() { var obj = new MyObject(); using (obj.MonitorEvents()) { obj.MyProperty = "Test Value"; // 先验证属性值 obj.MyProperty.Should().Be("Test Value"); obj.DependentOnMyProperty.Should().Be("Value derived from Test Value"); // 提取所有捕获到的PropertyChanged事件 var raisedEvents = obj.CapturedEvents.OfType<PropertyChangedEventArgs>().ToList(); // 验证事件触发情况 raisedEvents.Select(e => e.PropertyName) .Should().ContainInOrder( nameof(MyObject.MyProperty), nameof(MyObject.DependentOnMyProperty) ); raisedEvents.Count(e => e.PropertyName == nameof(MyObject.DependentOnMyProperty)) .Should().Be(1); } }
3. 处理异步更新的依赖属性
如果DependentOnMyProperty是异步更新的(比如后台线程计算),绝对不要用Thread.Sleep,改用异步等待机制。比如用FluentAssertions的异步断言:
[Fact] public async Task ComputedProperty_WhenChangingDependentValue_RaisesPropertyChangedEventAsync() { var obj = new MyObject(); var raisedProperties = new List<string>(); obj.PropertyChanged += (_, args) => raisedProperties.Add(args.PropertyName!); obj.MyProperty = "Test Value"; // 等待依赖属性的通知被触发,超时时间2秒 await raisedProperties.Should().Contain(nameof(MyObject.DependentOnMyProperty)) .AfterWaitingUntil(() => raisedProperties.Contains(nameof(MyObject.DependentOnMyProperty))) .WithTimeout(TimeSpan.FromSeconds(2)); // 验证属性值和事件 obj.MyProperty.Should().Be("Test Value"); obj.DependentOnMyProperty.Should().Be("Value derived from Test Value"); }
4. 关于Assert.Raises()的局限
Assert.Raises()本身就是设计用来捕获单个事件的,所以要验证多个事件触发时,必须用捕获所有事件再断言的方式,这也是上面几种方案的核心逻辑。
内容的提问来源于stack exchange,提问作者Jeff Mercado
相关产品推荐
相关产品推荐

