Единичное тестирование того, что событие поднято на С#, используя отражение
Я хочу проверить, что установка определенного свойства (или, более того, выполнение какого-то кода) вызывает определенное событие на моем объекте. В этом отношении моя проблема аналогична Групповому тестированию, что событие поднято на С#, но мне нужно много этих тестов, и я ненавижу шаблон. Поэтому я ищу более общее решение, используя отражение.
В идеале я хотел бы сделать что-то вроде этого:
[TestMethod]
public void TestWidth() {
MyClass myObject = new MyClass();
AssertRaisesEvent(() => { myObject.Width = 42; }, myObject, "WidthChanged");
}
Для реализации AssertRaisesEvent
я зашел так далеко:
private void AssertRaisesEvent(Action action, object obj, string eventName)
{
EventInfo eventInfo = obj.GetType().GetEvent(eventName);
int raisedCount = 0;
Action incrementer = () => { ++raisedCount; };
Delegate handler = /* what goes here? */;
eventInfo.AddEventHandler(obj, handler);
action.Invoke();
eventInfo.RemoveEventHandler(obj, handler);
Assert.AreEqual(1, raisedCount);
}
Как вы можете видеть, моя проблема заключается в создании Delegate
соответствующего типа для этого события. Делегат ничего не должен делать, кроме вызова incrementer
.
Из-за синтаксического сиропа в С# мое представление о том, как работают делегаты и события, немного туманно. Это также первый раз, когда я размышляю. Какая недостающая часть?
Ответы
Ответ 1
Недавно я написал серию постов в блоге о последовательностях событий модульного тестирования для объектов, которые публикуют синхронные и асинхронные события. В этих публикациях описывается подход и структура модульного тестирования, а также предоставляется полный исходный код с тестами.
Я описываю реализацию "монитора событий", который позволяет писать более точные записи модульных тестов последовательности событий, то есть избавляться от всего грязного стандартного кода.
Используя монитор событий, описанный в моей статье, тесты можно записать так:
var publisher = new AsyncEventPublisher();
Action test = () =>
{
publisher.RaiseA();
publisher.RaiseB();
publisher.RaiseC();
};
var expectedSequence = new[] { "EventA", "EventB", "EventC" };
EventMonitor.Assert(publisher, test, expectedSequence);
Или для типа, который реализует INotifyPropertyChanged:
var publisher = new PropertyChangedEventPublisher();
Action test = () =>
{
publisher.X = 1;
publisher.Y = 2;
};
var expectedSequence = new[] { "X", "Y" };
EventMonitor.Assert(publisher, test, expectedSequence);
А для случая в оригинальном вопросе:
MyClass myObject = new MyClass();
EventMonitor.Assert(myObject, () => { myObject.Width = 42; }, "Width");
EventMonitor выполняет всю тяжелую работу и запускает тест (действие) и утверждает, что события генерируются в ожидаемой последовательности (Ожидаемая последовательность). Он также выводит на экран хорошие диагностические сообщения о сбое теста. Reflection и IL используются под капотом, чтобы заставить работать динамическую подписку на события, но все это хорошо инкапсулировано, поэтому для написания тестов событий требуется только код, подобный приведенному выше.
Там много подробностей в постах, описывающих проблемы и подходы, а также исходный код:
http://gojisoft.com/blog/2010/04/22/event-sequence-unit-testing-part-1/
Ответ 2
С lambdas вы можете сделать это с очень маленьким кодом. Просто назначьте лямбда событию и установите значение в обработчике. Не нужно отражать, и вы получаете строго типизированный рефакторинг.
[TestFixture]
public class TestClass
{
[Test]
public void TestEventRaised()
{
// arrange
var called = false;
var test = new ObjectUnderTest();
test.WidthChanged += (sender, args) => called = true;
// act
test.Width = 42;
// assert
Assert.IsTrue(called);
}
private class ObjectUnderTest
{
private int _width;
public event EventHandler WidthChanged;
public int Width
{
get { return _width; }
set
{
_width = value; OnWidthChanged();
}
}
private void OnWidthChanged()
{
var handler = WidthChanged;
if (handler != null)
handler(this, EventArgs.Empty);
}
}
}
Ответ 3
Решение в стиле, который вы предлагаете, который охватывает ВСЕ случаи, будет чрезвычайно сложно реализовать. Но если вы согласны с тем, что типы делегатов с параметрами ref и out или возвращаемые значения не будут покрыты, вы сможете использовать DynamicMethod.
Во время разработки создайте класс для хранения счетчика, позвоните ему CallCounter.
В AssertRaisesEvent:
- создайте экземпляр вашего CallCounterclass, сохранив его в строго типизированной переменной
- инициализировать счетчик до нуля
-
построить DynamicMethod в вашем классе счетчиков
new DynamicMethod(string.Empty, typeof(void), parameter types extracted from the eventInfo, typeof(CallCounter))
-
получить DynamicMethod MethodBuilder и использовать reflection.Emit для добавления кодов операций для увеличения поля
- ldarg.0 (этот указатель)
- ldc_I4_1 (постоянный)
- ldarg.0 (этот указатель)
- ldfld (чтение текущего значения счетчика)
- добавить
- stfld (поместить обновленный счетчик обратно в переменную-член)
- вызывать двухпараметрическую перегрузку CreateDelegate, первым параметром является тип события, взятый из eventInfo, второй параметр - ваш экземпляр CallCounter
- передать полученный делегат event event.AddEventHandler(у вас есть это)
Теперь вы готовы выполнить тестовый пример (у вас это есть).
- наконец, прочитайте счет обычным способом.
Единственный шаг, на котором я не уверен на 100%, как вы это делаете, это получение типов параметров из EventInfo. Вы используете свойство EventHandlerType, а затем? Ну, вот пример на этой странице, показывающий, что вы просто захватываете метод MethodInfo для метода Invoke делегата (я думаю, что имя "Invoke" гарантировано где-то в стандарте), а затем GetParameters, а затем вытащите все значения параметра ParameterType, проверяя, что на пути нет параметров ref/out.
Ответ 4
Как насчет этого:
private void AssertRaisesEvent(Action action, object obj, string eventName)
{
EventInfo eventInfo = obj.GetType().GetEvent(eventName);
int raisedCount = 0;
EventHandler handler = new EventHandler((sender, eventArgs) => { ++raisedCount; });
eventInfo.AddEventHandler(obj, handler );
action.Invoke();
eventInfo.RemoveEventHandler(obj, handler);
Assert.AreEqual(1, raisedCount);
}
Ответ 5
Это старая ветка, но я думаю, что это все еще актуальная проблема, для которой я не нашел простого решения. На работе мне нужно было что-то сделать для тестирования событий, и поскольку существующего решения не было, я решил реализовать небольшую библиотеку. Поскольку я вполне доволен своим решением, я решил разместить его на GitHub. Может быть, вы тоже найдете это полезным:
https://github.com/f-tischler/EventTesting
Вот как это выглядит в действии:
using EventTesting;
// ARRANGE
var myObj = new MyClass();
var hook = EventHook.For(myObj)
.HookOnly((o, h) => o.WidthChanged+= h);
// ACT
myObj.Width = 42;
// ASSERT
hook.Verify(Called.Once());