Модульное тестирование того, что событие вызывается в C # с использованием отражения - PullRequest
12 голосов
/ 02 апреля 2010

Я хочу проверить, что установка определенного свойства (или, в более общем смысле, выполнение некоторого кода) вызывает определенное событие в моем объекте. В этом отношении моя проблема похожа на модульное тестирование, которое вызывает событие в 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);
}

Как видите, моя проблема заключается в создании Delegate соответствующего типа для этого события. Делегат не должен делать ничего, кроме вызова incrementer.

Из-за всего синтаксического сиропа в C # мое представление о том, как на самом деле работают делегаты и события, немного смутно. Это также первый раз, когда я балуюсь размышлениями. Что за недостающая часть?

Ответы [ 4 ]

7 голосов
/ 23 апреля 2010

Недавно я написал серию постов в блоге о юнит-тестировании последовательностей событий для объектов, которые публикуют синхронные и асинхронные события. Посты описывают подход и структуру модульного тестирования и предоставляют полный исходный код с тестами.

Я описываю реализацию «монитора событий», который позволяет писать модульные тесты последовательности событий более аккуратно, т. Е. Избавиться от всего грязного стандартного кода.

Используя монитор событий, описанный в моей статье, тесты можно записать так:

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/

7 голосов
/ 02 апреля 2010

С лямбдами вы можете сделать это с очень небольшим кодом. Просто назначьте лямбду событию и установите значение в обработчике. Нет необходимости в рефлексии, и вы получаете строго типизированный рефакторинг

[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);
        }
    }
}
2 голосов
/ 02 апреля 2010

Решение в предложенном вами стиле, которое охватывает ВСЕ дела, будет чрезвычайно сложно реализовать. Но если вы согласны с тем, что типы делегатов с параметрами ref и out или возвращаемыми значениями не покрываются, вы можете использовать DynamicMethod.

Во время разработки создайте класс для хранения счетчика, назовем его CallCounter.

В AssertRaisesEvent:

  • создайте экземпляр вашего CallCounterclass, сохраняя его в строго типизированной переменной
  • Инициализировать счет до нуля
  • построить DynamicMethod в вашем классе счетчиков

    new DynamicMethod(string.Empty, typeof(void), parameter types extracted from the eventInfo, typeof(CallCounter))

  • получить метод MethodBuilder в DynamicMethod и использовать отражение. Emit для добавления кодов операций для увеличения поля

    • ldarg.0 (указатель this)
    • ldc_I4_1 (постоянный)
    • ldarg.0 (указатель this)
    • ldfld (прочитать текущее значение счетчика)
    • добавить
    • stfld (поместите обновленный счетчик обратно в переменную-член)
  • call двухпараметрическая перегрузка CreateDelegate , первый параметр - это тип события, взятый из eventInfo, второй параметр - ваш экземпляр CallCounter
  • передать полученный делегат в eventInfo.AddEventHandler (у вас есть это) Теперь вы готовы выполнить тестовый пример (у вас есть это).
  • наконец, считайте счет обычным способом.

Единственный шаг, на который я не на 100% уверен, как вы поступите, это получение типов параметров из EventInfo. Вы используете EventHandlerType свойство , а затем? Что ж, на этой странице есть пример, показывающий, что вы просто захватываете MethodInfo для метода Invoke делегата (я предполагаю, что имя "Invoke" гарантировано где-то в стандарте), а затем GetParameters и затем извлекаете все значения ParameterType, проверяя, что на пути нет параметров ref / out.

1 голос
/ 02 апреля 2010

Как насчет этого:

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);
    }
...