Как проверить, что событие было отписано от макета

Минимальная версия: 3.1.416.3

Мы обнаружили ошибку, вызванную тем, что событие не было отписано. Я пытаюсь написать модульный тест, чтобы убедиться, что событие отписано. Можно ли это проверить с помощью Mock<T>.Verify(expression)?

Моя первоначальная мысль была:

mockSource.Verify(s => s.DataChanged -= It.IsAny<DataChangedHandler>());

Но видимо

Дерево выражений не может содержать оператор присваивания

Затем я попытался

mockSource.VerifySet(s => s.DataChanged -= It.IsAny<DataChangedHandler>());

Но это дает мне

System.ArgumentException: Expression не является вызовом задающего свойства.

Как я могу убедиться, что отписка произошла?

Как используется событие

public class Foo
{
    private ISource _source;

    public Foo(ISource source)
    {
        _source = source;
    }

    public void DoCalculation()
    {
        _source.DataChanged += ProcessData;

        var done = false;

        while(!done)
        {
            if(/*something is wrong*/)
            {
                Abort();
                return;
            }
            //all the things that happen
            if(/*condition is met*/)
            {
                done = true;
            }
        }

        _source.DataChanged -= ProcessData;
    }

    public void Abort()
    {
        _source.DataChanged -= ProcessData; //this line was added to fix the bug
         //other cleanup
    }

    private void ProcessData(ISource)
    {
        //process the data
    }
}

Не обращайте внимания на запутанный характер кода, мы имеем дело с сигналами от внешнего оборудования. Это действительно имеет смысл для алгоритма.


person Matt Ellen    schedule 26.09.2013    source источник
comment
Есть ли что-нибудь, препятствующее добавлению случая сбоя в событие? Вы можете подписаться на событие с чем-то вроде делегата Assert.Fail(), который, если событие не отписалось, должно сработать. Я не думаю, что вы можете внешне анализировать текущие обработчики событий: заголовок stackoverflow.com/questions/572647/   -  person Adam Kewley    schedule 26.09.2013
comment
@AdamKewley, было бы хорошо убедиться, что все обработчики удалены, но мне нужно убедиться, что удален только один. Хотя я мог бы изменить код, чтобы сгладить событие. Это действительно может сработать.   -  person Matt Ellen    schedule 26.09.2013
comment
Можете ли вы объяснить немного больше? Кто/что связывает событие? Это твой СУТ? Это SUT отвечает за отсоединение? Можете ли вы показать какой-нибудь сценарий использования?   -  person Sunny Milenov    schedule 27.09.2013
comment
@SunnyMilenov Я добавил пример   -  person Matt Ellen    schedule 27.09.2013
comment
См. этот stackoverflow.com/questions/1429587 /   -  person adPartage    schedule 16.01.2017


Ответы (2)


Предполагая, что ProcessData делает что-то значимое, то есть либо изменяет состояние SUT (тестируемой системы) значимым/наблюдаемым образом, либо действует на аргументы события, достаточно просто вызвать событие на макете и проверить, произошло ли изменение. .

Пример с изменением состояния:

....
public void ProcessData(ISource source)
{
   source.Counter ++;
}

...
[Test]
.....

sut.DoWork();
var countBeforeEvent = source.Count;
mockSource.Raise(s => s.DataChanged += null, new DataChangedEventArgs(fooValue));
Assert.AreEqual(countBeforeEvent, source.Count);

Конечно, вышеизложенное следует адаптировать к любой реализации, которая у вас есть в ProcessData.

При модульном тестировании вас не должны волновать детали реализации (т. е. если какое-то событие отписывается) и нужно проверять не это, а поведение — т. е. если вы вызываете событие, что-то происходит. В вашем случае достаточно убедиться, что ProcessData не вызывается. Конечно, вам нужен еще один тест, который демонстрирует, что событие вызывается во время нормальной работы (или определенных условий).

РЕДАКТИРОВАТЬ: вышесказанное относится к использованию Moq. Но… Moq — это инструмент, и, как любой инструмент, его следует использовать для правильной работы. Если вам действительно нужно проверить, вызывается ли "-=", вам следует выбрать лучший инструмент - например, реализовать собственную заглушку ISource. В следующем примере есть довольно бесполезный тестируемый класс, который просто подписывается, а затем отписывается от события, просто чтобы продемонстрировать, как вы можете тестировать.

using System;
using NUnit.Framework;
using SharpTestsEx;

namespace StackOverflowExample.Moq
{
    public interface ISource
    {
        event Action<ISource> DataChanged;
        int InvokationCount { get; set; }
    }

    public class ClassToTest
    {
        public void DoWork(ISource source)
        {
            source.DataChanged += this.EventHanler;
        }

        private void EventHanler(ISource source)
        {
            source.InvokationCount++;
            source.DataChanged -= this.EventHanler;
        }
    }

    [TestFixture]
    public class EventUnsubscribeTests
    {
        private class TestEventSource :ISource
        {
            public event Action<ISource> DataChanged;
            public int InvokationCount { get; set; }

            public void InvokeEvent()
            {
                if (DataChanged != null)
                {
                    DataChanged(this);
                }
            }

            public bool IsEventDetached()
            {
                return DataChanged == null;
            }
        }

        [Test]
        public void DoWork_should_detach_from_event_after_first_invocation()
        {
            //arrange
            var testSource = new TestEventSource();
            var sut = new ClassToTest();
            sut.DoWork(testSource);

            //act
            testSource.InvokeEvent();
            testSource.InvokeEvent(); //call two times :)

            //assert
            testSource.InvokationCount.Should("have hooked the event").Be(1);
            testSource.IsEventDetached().Should("have unhooked the event").Be.True();
        }
    }
} 
person Sunny Milenov    schedule 27.09.2013
comment
Что означает СУТ? - person Matt Ellen; 27.09.2013
comment
@MattEllen: тестируемая система, то есть класс, который вы тестируете. - person Sunny Milenov; 27.09.2013

существует грязный способ получить список вызовов из события вне целевого класса, хотя его следует использовать только для целей тестирования или отладки, поскольку это нарушает цель событий.

Это работает только в том случае, если событие не реализовано с реализацией клиента (добавить/удалить). Если у события есть средства доступа к событию, eventInfo2FieldInfo вернет значение null.

 Func<EventInfo, FieldInfo> eventInfo2FieldInfo = eventInfo => mockSource.GetType().GetField(eventInfo.Name, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField);
 IEnumerable<MulticastDelegate> invocationLists = mockSource.GetType().GetEvents().Select(selector => eventInfo2FieldInfo(selector).GetValue(mockSource)).OfType<MulticastDelegate>();

теперь у вас есть списки вызовов для всех событий целевого класса, и вы должны быть в состоянии утверждать, было ли отписано особое событие.

person quadroid    schedule 26.09.2013
comment
Интересный. Я попробую. - person Matt Ellen; 26.09.2013
comment
К сожалению, eventInfo2FieldInfo всегда возвращает только null. - person Matt Ellen; 26.09.2013
comment
возможно, вы что-то пропустили, я использую точно такой же код, чтобы определить, правильно ли отписываются мои события. Это дает мне ожидаемые результаты. - person quadroid; 26.09.2013
comment
Я скопировал ваш код и настроил его для работы с Mock<T>, и я получаю исключения с нулевыми ссылками. Я пробовал это и с не-фиктивным объектом, и я получил тот же результат. - person Matt Ellen; 26.09.2013
comment
Мне любопытно, я опубликую полный пример завтра - person quadroid; 27.09.2013