Andrey on .NET | Юнит-тесты, internals и Moq

Юнит-тесты, internals и Moq

Наверняка многие использовали модификатор internal, чтобы скрыть внутренние объекты от других сборок. Рассмотрим одну интересую особенность использования таких классов в проектах с юнит-тестами.

Чтобы сделать internal классы доступными в проекте тестирования используется простое указание InternalsVisibleTo в AssemblyInfo.cs проекта:

[assembly: InternalsVisibleTo("MyProject.UnitTests")]

Это позволит разработчику писать код тестов с их использованием. Вроде бы все хорошо.

Проблема

Однако, это не все что необходимо, если используется библиотека Moq. Часть запускаемых тестов будет падать c исключением TypeLoadException. Если внимательно посмотреть текст сообщения, то можно увидеть что TypeBuilder не может получить доступ к internal классам.

System.TypeLoadException: Access is denied: 'MyProject.DataAccess.Dto.UserDto'.

Это может произойти например вот в таком тестовом коде  (здесь repository это Mock<T>):

repository
    .Setup(r => r.Add(It.IsAny<UserDto>()))
    .Callback<UserDto>(dataStore.Add);

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

Решение

На самом деле нужно указать:

[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]

Теперь можно запускать тесты.

Немного подробностей

А что же такое DynamicProxyGenAssembly2? Это динамически создаваемая сборка, которая порождается библиотекой CastleProxy и предназначена для генерации прокси-объектов. Как можно легко догадаться, Moq (и, кстати, NSubsitute) используют CastleProxy для порождения mock-объектов.

Комментарии (6) -

А по-моему, в таких ситуациях сначала должен возникать вопрос, а то ли я тестирую, если это всё равно не видно снаружи.

@ Андрей: Странный подход. А кто сказал что internal классы не надо тестировать, кто сказал что internal гарантирует безошибочную работу? Причем IMHO сборки должны внимательно следить за тем, что у них public и не выдавать лишнего из своей реализации. Поэтому по умолчанию классы создаются internal.

Например, давайте рассмотрим работу с данными с использованием реализации шаблона CQRS.

- реализации обработчиков команд и запросов (при наличии public интерфейсов для них). Их область видимости вполне разумно может быть ограничена сборкой DataAccess. Но оставлять такой ключевой код не тестируемым категорически противопоказано.

- сущности для работы БД (DTO) так же нет смысла выпускать дальше DataAccess (в BL возвращаются уже модели BL). При этом, для тестирования запросов к БД зачастую она подменяется на Mock с объектами в памяти. Вот тут и потребуется доступ к internal DTO в юнит-тестах (пример в стате как раз из такой ситуации).

Чувствую что задел тёзку за живое Smile. На этом поле за последние десятилетия сломано не мало копий. Не думаю что переманю на свою сторону, но прокомментировать хочется.

- ...сборки должны внимательно следить за тем, что у них public
Скажу только что сокрытие реализации в вашем понимании не спасает от вызова через reflection. Можно придумать другие способы, чтобы донести пользователю, что делать этого не стоит, или не заморачиваться.

- ...и не выдавать лишнего из своей реализации.
Вот именно! Реализация должна оставаться скрытой от пользователя, и поэтому не стоит её упоминать в тестах, чтобы не переписывать их каждый раз, поменяв реализацию. А вот результат работы и поведение - да, оставлять не оттестированным безответственно. А если это становится сложным, то возникает вопрос о разделении ответственностей. Это становится заметно в случаях, когда что-то не видно, а увидеть нужно.

-...Поэтому по умолчанию классы создаются internal.
Это в C#. В Scala например всё по умолчанию public, поэтому такого ключевого слова просто нет. Только private и protected, которые нужны гораздо реже.

@ Андрей: Не задели, но почему не обсудить

не спасает от вызова через reflection.

Против лома нет приема. Но в этом случае разработчик это делает уже сознательно. В случае public на использование класса может натолкнуть даже сама VS (особенно если кто-то включается в проект)

Реализация должна оставаться скрытой от пользователя, и поэтому не стоит её упоминать в тестах

И вот тут хорошо бы пример кода, как протестировать LINQ запрос не упоминая DTO. Smile

Кстати, сам тест никак не зависит от конкретной реализации. Есть данные для входа, черный ящик и результат. Вопрос в подготовки среды для тестов.

А если это становится сложным, то возникает вопрос о разделении ответственностей.

Вот именно от этого и могут появиться внутри сборки локальные сервисы, которые нет смысла или даже вредно вытаскивать наружу.

на использование класса может натолкнуть даже сама VS
Назвать класс соответственно ConvertMyTDOInternalImplBase, добавить [Browsable(false)] [ItsMyShit] или еще какой аттрибут
как протестировать LINQ запрос не упоминая DTOА зачем скрывать входные или выходные аргументы, если функциональность слоя и состоит в том чтобы спроецировать одно в другое. Искусственно прятать, чтобы потом элегантно доставать?
локальные сервисы вредно вытаскивать наружу Тут на мой взгляд разница между видно снаружи или вытащить наружу. Локальные сервисы и остаются локальными, если их не вытаскивать. Нельзя же получить цирроз печени от того что мне видно водку. Хотя у всех разный уровень паранойи :o)

[Browsable(false)]

Согласитесь это как полумера в данном случае. Еще не internal, но уже как бы и не совсем public.

Все же не будем уходить в сторону - Browsable это управление поведением редактора VS, а internal это уже управление уровня архитектуры проекта.

А зачем скрывать входные или выходные аргументы

Можно вопросом на вопрос - а зачем BL вообще знать как устроен DA? Это исключительно его внутренние дела. Причем Model и DTO со временем могут меняться независимо друг от друга (самый простой пример - учетная запись пользователя и логин). Или DA может использовать разные хранилища для разных DTO, что наложит на них свой отпечаток. Зачем все это наружу тянуть?

Тут на мой взгляд разница между видно снаружи или вытащить наружу.

Это если 1 человек на проекте, то он может помнить такие соглашения. Увеличьте команду хотя бы до 3 человек, а на полпути введите еще одного. Большая вероятность что рано или поздно кто-то вытащит такой "запретный" public.

Добавить комментарий