Я хочу протестировать ту установку определенного свойства (или в более общем плане, выполняя некоторый код) генерирует определенное событие на моем объекте. В этом отношении моя проблема подобна Поблочному тестированию, что событие генерируется в C#, но мне нужно много этих тестов, и я ненавижу шаблон. Таким образом, я ищу более общее решение, с помощью отражения.
Идеально, я хотел бы сделать что-то вроде этого:
[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);
}
Как Вы видите, моя проблема заключается в создании a Delegate
из соответствующего типа для этого события. Делегат должен сделать, ничто кроме не вызывает incrementer
.
Из-за всего синтаксического сиропа в C# мое понятие того, как делегаты и события действительно работают, является немного туманным. Это - также первый раз, когда я балуюсь отражением. Какова недостающая часть?
Недавно я написал серию сообщений в блоге о последовательностях событий модульного тестирования для объектов, которые публикуют как синхронные, так и асинхронные события. В сообщениях описывается подход и структура модульного тестирования, а также предоставляется полный исходный код с тестами.
Я описываю реализацию «монитора событий», который позволяет писать более чистые модульные тесты последовательности событий, то есть избавляться от всего беспорядочного шаблонного кода.
Используя монитор событий, описанный в моей статье, тесты могут быть написаны так:
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 выполняет все тяжелый подъем и запустит тест (действие) и подтвердит, что события возникают в ожидаемой последовательности (ожидаемая последовательность). Он также распечатывает приятные диагностические сообщения при неудачном тестировании. Отражение и IL используются под капотом для работы динамической подписки на события, но все это красиво инкапсулировано, поэтому для написания тестов событий требуется только код, подобный приведенному выше.
В сообщениях много деталей, описывающих проблемы и подходы, а также исходный код:
http://gojisoft.com/blog/2010/04/22/event-sequence-unit-testing-part -1 /
С лямбдами вы можете сделать это с очень небольшим кодом. Просто назначьте событию лямбду и установите значение в обработчике. Нет необходимости в рефлексии, и вы получаете строго типизированный рефакторинг
[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);
}
}
}
Как насчет этого:
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);
}
Решение в предложенном вами стиле, которое охватывает ВСЕ случаи, будет чрезвычайно трудно реализовать. Но если вы готовы принять, что типы делегатов с параметрами ref и out или возвращаемыми значениями не будут охвачены, вы должны быть в состоянии использовать DynamicMethod.
Во время проектирования создайте класс для хранения счета, назовем его CallCounter.
В AssertRaisesEvent:
создайте DynamicMethod в классе счетчика
new DynamicMethod(string. Empty, typeof(void), типы параметров, извлеченные из eventInfo, typeof(CallCounter))
получите MethodBuilder динамического метода и используйте reflection.Emit для добавления опкодов для увеличения поля
Единственный шаг, в котором я не уверен на 100%, это получение типов параметров из EventInfo. Вы используете свойство EventHandlerType, а дальше? Ну, на этой странице есть пример, показывающий, что вы просто берете MethodInfo для метода Invoke делегата (я предполагаю, что имя "Invoke" гарантировано где-то в стандарте), затем GetParameters и затем вытаскиваете все значения ParameterType, проверяя по пути, что нет параметров ref/out.