Как отслеживать тестируемый класс с помощью AutofacContrib.NSubstitute

Я запускаю модульные тесты в проекте библиотеки классов с инфраструктурой NSpec, AutofacContrib.NSubstitute v3.3.2.0, NSubstitute v1.7.0.0 (последняя на данный момент версия 1.8.2).

Экземпляр Class Under Test создан с использованием AutoSubstitute, чтобы автоматически создавать макеты всех необходимых зависимостей.

AutoSubstitute autoSubstitute = new AutoSubstitute();

MainPanelViewModel viewModel = autoSubstitute.Resolve<MainPanelViewModel>();

При правильной работе мой тестируемый класс в какой-то момент вызовет один из методов его базового класса с определенным входным параметром (базовый класс находится вне моего контроля):

// ...
base.ActivateItem(nextScreen);
// ...

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

viewModel.Received().ActivateItem(Arg.Any<SomeSpecificScreenType>());

Вот проблема: когда я пытаюсь это сделать, во время выполнения NSubstitute жалуется, что я могу запустить только Received() для объекта, созданного с помощью Substitute.For<>(). Я также быстро проверил исходный код AutofacContrib.NSubstitute, но не смог найти способ получить экземпляр с автомокингом и при этом завернуть его как-то в шпионский объект или что-то в этом роде.

Я также подумал, что, возможно, Substitute.ForPartsOf<>() может быть полезен, но этот метод, похоже, не найден в NSubstitute v1.7.0.

Для полноты здесь полная ошибка NSubstitute:

Методы расширения NSubstitute, такие как .Received(), можно вызывать только для объектов, созданных с помощью Substitute.For() и связанных методов.


person superjos    schedule 25.06.2015    source источник
comment
Вы действительно должны проверить, имеет ли место поведение ActivateItem в базовом классе, а не проверять, вызывается ли сам метод (что, по сути, является деталью реализации). Есть ли причина, по которой вы не можете этого сделать? Subsitute.ForPartsOf может помочь, но, как правило, не стоит имитировать класс, который вы тестируете. Метод, который вы хотите протестировать, также должен быть виртуальным, поэтому без его тестирования я скептически отношусь к тому, что явный вызов base.ActivateItem все равно вызовет замену. Тестирование того, что делает метод, вероятно, будет проще.   -  person forsvarir    schedule 25.06.2015
comment
Я полностью согласен с этим в целом. Я собирался ответить на ваш комментарий, что это модель представления CaliburnMicro, и проверить поведение ActivateItem будет сложно, учитывая, что мы переходим в область просмотра пользовательского интерфейса. Но потом я просто вспомнил, что еще должно быть свойство ActiveItem ... так что сейчас я просто проверю, что   -  person superjos    schedule 25.06.2015
comment
ну вот! Теперь я думаю, должен ли я просто закрыть этот вопрос или что   -  person superjos    schedule 25.06.2015
comment
Вы можете просто опубликовать ответ и принять его, как только пройдет время... Думаю, это около 24 часов.   -  person forsvarir    schedule 25.06.2015


Ответы (2)


Для полноты картины я поэкспериментировал с частичными заменами NSubstitute на ForPartsOf.

Принцип работы ForPartsOf заключается в том, чтобы определить новый класс, который наследуется от класса, который вы используете в качестве шаблона. Это ограничивает то, что фиктивная структура может перехватывать методами, которые определены как abstract или virtual. Это то же самое ограничение, которое у вас было бы для изменения поведения класса, если бы вы наследовали его своим собственным классом.

Принимая эту информацию, давайте посмотрим на вашу проблему. Вы хотите перехватить этот вызов:

base.ActivateItem(nextScreen);

Итак, из-за указанных выше ограничений, чтобы вы могли перехватить вызов ActivateItem, метод должен быть помечен как virtual в базовом классе. Если это не так, вы ничего не можете сделать без изменения структуры приложения.

Если метод помечен как virtual, то вы можете перехватить его с помощью NSubstitute, но вы можете сделать это, только если вызывается реализация NSubstituted. Это работает через обычную диспетчеризацию метода, потому что при вызове виртуального метода вызывается реализация виртуального метода самого высокого уровня (предоставленная NSubstitute). Однако это не работает, когда вы вызываете метод через ссылку base.

Итак, пока вы можете перехватить это:

ActivateItem(nextScreen)

Вы просто не можете перехватить это:

base.ActivateItem(nextScreen);

Тот факт, что вы используете base.ActivateItem в своем коде, предполагает, что ваш тестируемый класс имеет собственную реализацию метода, который вы не хотите вызывать, поэтому с вашими текущими инструментами вы не можете достичь того, что пытались сделать. Вот почему хорошо, что вы нашли обходной путь.

Вы находитесь в той же ситуации с большинством других фиктивных фреймворков, включая Moq. Исключением является TypeMock, который использует совершенно другой способ вызовы методов перехвата, что означает, что он может делать то, что другие фреймворки просто не могут.

person forsvarir    schedule 25.06.2015
comment
спасибо за ваше расследование, я думаю, что это полезное разъяснение для всех, кто заходит в эту тему. Всего 2 бита: я написал base.Xxxx просто для ясности, в коде я просто вызываю ActivateItem и не переопределяю его (хотя он переопределяемый). Кроме того, до перехода на NSubstitute я использовал Moq с Moq.AutoMock, и я успешно шпионил с помощью mocker.CreateSelfMock<MainPanelViewModel>(), а затем Mock.Get(viewModel).Verify(vm => vm.ActivateItem(It.IsAny<SomeSpecificScreenType>())); - person superjos; 26.06.2015
comment
@superjos Забавно, именно звонок base.Xxx сделал его интересным. Если это обычный виртуальный вызов, вы можете использовать Received для частичной замены, как вы сказали в своем вопросе, и он должен работать нормально. Примечания к выпуску для NSubstitute могут быть немного неверными, но они, кажется, говорят, что ForPartsOf должен быть в версии 1.7.0... github.com/nsubstitute/NSubstitute/blob/master/CHANGELOG.txt - person forsvarir; 26.06.2015
comment
Но опять же, экземпляр тестируемого класса создается AutofacContrib.NSubstitute с автоматическим макетированием, и я не смог увидеть ни одного из его методов, которые могли бы вернуть частичную замену. Кстати, я играл с его исходным кодом в VS, и, если я не ошибаюсь, я думаю, что ForPartsOf‹› был недоступен. Так или иначе ... - person superjos; 26.06.2015

Таким образом, фактическая проблема не была на самом деле решена: просто проблема исчезла сама по себе.

Чтобы проверить правильность поведения, я вспомнил, что могу также прибегнуть к общедоступному свойству ActiveItem из базового класса, поэтому я прекратил использовать Receive() и вернулся к простому сравнению значений.

Тем не менее, на будущее, я не нашел способа шпионить за тестируемым классом с помощью этих библиотек. Я знаю, что следует избегать шпионажа за тестируемым классом, но, как и во многих других случаях, иногда вам нужно это делать.

ХТН

person superjos    schedule 25.06.2015