Модульное тестирование событий в С# (по порядку)
У меня есть код, который вызывает события PropertyChanged
, и я хотел бы иметь возможность unit test правильно воссоздать события.
Код, который поднимает события, похож на
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. Правильно ли мой метод повышения прикованных событий?
Ответы
Ответ 1
Все, что вы сделали, правильно, если вы хотите, чтобы ваш тест спросил: "Какое последнее событие было поднято?"
Ваш код запускает эти два события, в этом порядке
- Изменено свойство (... "My Property"...)
- Изменено свойство (... "MyOtherProperty"...)
Является ли это "правильным" или нет, зависит от цели этих событий.
Если вы хотите проверить количество возникающих событий и порядок их привлечения, вы можете легко расширить существующий тест:
[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]);
}
Ответ 2
Если вы делаете TDD, то тестирование событий может начать генерировать много повторяющегося кода. Я написал монитор событий, который позволяет гораздо чище подходить к написанию модульных тестов для этих ситуаций.
var publisher = new PropertyChangedEventPublisher();
Action test = () =>
{
publisher.X = 1;
publisher.Y = 2;
};
var expectedSequence = new[] { "X", "Y" };
EventMonitor.Assert(test, publisher, expectedSequence);
Пожалуйста, смотрите мой ответ на следующее для более подробной информации.
Модульное тестирование того, что событие вызывается в С# с использованием отражения
Ответ 3
Это очень старый и, вероятно, даже не прочитанный, но с некоторыми классными новыми функциями .net Я создал класс 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);
}
См. gist: https://gist.github.com/Seikilos/6224204
Ответ 4
Ниже приведен слегка измененный код 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"]);
}
Ответ 5
Основываясь на этой статье, я создал этот простой помощник утверждения:
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");
}
Ответ 6
Не пишите тест для каждого участника - это большая работа
(возможно, это решение не идеально подходит для любой ситуации - но оно показывает возможный путь. Возможно, вам придется адаптировать его для вашего случая использования)
Можно использовать отражение в библиотеке, чтобы проверить, все ли ваши члены правильно реагируют на событие изменения вашего свойства:
- Событие 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();
}
}
Тесты вашего класса теперь могут быть записаны как. (может быть, вы хотите разделить тест на "событие есть" и "событие с правильным именем" - вы можете сделать это самостоятельно)
[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");
}
}
}
Ответ 7
Я сделал расширение здесь:
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 execution OnPropertyChanged for _filesViewModel.AllFilesFiltered should be raised
_filesViewModel.NotifyPropertyChangedVerificationSettingUp(nameof(_filesViewModel.AllFilesFiltered));
//Act
_filesViewModel.ApplyRenamingCommand.Execute(null);
// Assert
Assert.True(_filesViewModel.IsNotifyPropertyChangedFired());
}