Статьи

Пересмешивание внутренних интерфейсов с помощью Moq

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

Предположим, у вас есть библиотека классов с именем  Base,  содержащая следующие классы и интерфейсы

  • ICommandHandler — который является общедоступным интерфейсом
  • CommandHandler — в основном его конкретная реализация
  • IUndoRedoStack<T> — интерфейс, который используется только внутри
  • UndoRedoStack<T> — его конкретная реализация

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

Тестирование классов с внутренней видимостью

Давайте внимательнее посмотрим на  CommandHandler класс

class CommandHandler : ICommandHandler
{
    //...
}

При создании теста для  CommandHandler класса вы должны действовать следующим образом

[TestClass]
public class CommandHandlerTest
{
    
    private CommandHandler commandHandler;

    [TestInitialize]
    public void Setup()
    {
        commandHandler = new CommandHandler();
    }

    [TestCleanup]
    public void Teardown()
    {
        commandHandler = null;
    }


    [TestMethod]
    publiic void ShouldExecuteAGivenCommand()
    {
        //the test content
    }
}

Когда вы выполните такой тест, он не будет скомпилирован. Лучше всего помещать тесты в отдельную DLL (я обычно называю ее так, как  Base.UnitTests будто вызывается тестируемая сборка  Base), и поэтому  CommandHandler не будет виден, так как определено, что она имеет только внутреннюю видимость. В  предыдущем сообщении в блоге  я уже объяснил, как преодолеть эту проблему, а именно, указав  InternalsVisibleTo атрибут в тестируемой сборке. Проверьте это сообщение в блоге для более подробной информации.

Mocking интерфейсы с внутренней видимостью с использованием Moq

Теперь  CommandHandler имеет зависимость от IUndoRedoStack<T>

class CommandHandler : ICommandHandler
{
    public CommandHandler(IUndoRedoStack<ICommand> undoRedoStack) 
    {
        //...
    }
}

У   метода CommandHandler есть  Execute(command)метод, и предположим, что мы хотели бы проверить тот факт, что при вызове его с заданным  ICommand объектом этот конкретный объект добавляется в  undoRedoStack. Мы бы написали

    [TestInitialize]
    public void Setup()
    {
        mockUndoRedo = new Mock<IUndoRedoStack<ICommand<<();
        handler = new CommandHandler(mockUndoRedo.Object);
    }

    [TestMethod]
    public void ShouldAddTheCommandToTheUndoStack()
    {
        //arrange
        var myCommand = new MyTestCommand();

        //act
        handler.Execute(myCommand);

        //assert
        mockUndoRedo.Verify(x =< x.AddItem(myCommand), Times.Once(), "The command should have been added to the undo stack");
    }

При выполнении теста это не удается с

Сообщение: метод инициализации Base.UnitTests.Command.CommandHandlerTest, исключение.
Castle.DynamicProxy.Generators.GeneratorException:
Castle.DynamicProxy.Generators.GeneratorException: Тип Base.Command.IUndoRedoStackBase.Command.ICommand, Base, Version = 1.0.0.0, Culture = нейтральный, PublicKeyToken = null не является открытым. Невозможно создать прокси для типов, которые недоступны.

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

[assembly: InternalsVisibleTo("Moq")]

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

[assembly:InternalsVisibleTo("DynamicProxyGenAssembly2")]

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

[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=...")]