Поблочное тестирование, что событие генерируется в C#, с помощью отражения

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

12
задан Community 23 May 2017 в 12:07
поделиться

4 ответа

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

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

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

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 /

8
ответ дан 2 December 2019 в 18:18
поделиться

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

[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);
        }
    }
}
8
ответ дан 2 December 2019 в 18:18
поделиться

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

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);
    }
1
ответ дан 2 December 2019 в 18:18
поделиться

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

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

В AssertRaisesEvent:

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

    new DynamicMethod(string. Empty, typeof(void), типы параметров, извлеченные из eventInfo, typeof(CallCounter))

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

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

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

2
ответ дан 2 December 2019 в 18:18
поделиться
Другие вопросы по тегам:

Похожие вопросы: