Как вы можете провести модульное тестирование ваших контроллеров без контейнера IoC? - PullRequest
6 голосов
/ 25 июня 2009

Я начинаю изучать юнит-тестирование, Dependancy Injection и все такое прочее при создании моего последнего проекта ASP.NET MVC.

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

Возьмем, к примеру, простой контроллер:

public class QuestionsController : ControllerBase
{
    private IQuestionsRepository _repository = new SqlQuestionsRepository();

    // ... Continue with various controller actions
}

Этот класс не очень хорошо тестируется модулем из-за его прямого создания SqlQuestionsRepository. Итак, давайте пойдем по маршруту Depenancy Injection и сделаем:

public class QuestionsController : ControllerBase
{
    private IQuestionsRepository _repository;

    public QuestionsController(IQuestionsRepository repository)
    {
        _repository = repository;
    }
}

Это кажется лучше. Теперь я могу легко написать модульные тесты с помощью фиктивного IQuestionsRepository. Однако, что собирается создать экземпляр контроллера сейчас? Где-то дальше вверх по цепочке вызовов будет создан экземпляр SqlQuestionRepository. Кажется, что я просто перенес проблему в другое место, а не избавился от нее.

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

У меня вопрос: как можно провести модульное тестирование вещей такого типа без контейнера IoC?

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

Ответы [ 4 ]

2 голосов
/ 25 июня 2009

Вы можете иметь конструктор по умолчанию с вашим контроллером, который будет иметь какое-то поведение по умолчанию.

Что-то вроде ...

public QuestionsController()
    : this(new QuestionsRepository())
{
}

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

2 голосов
/ 25 июня 2009

Разве невозможно сохранить непосредственное создание поля и также предоставить установщик? В этом случае вы будете вызывать сеттер только во время модульного тестирования. Примерно так:

public class QuestionsController : ControllerBase
{
    private IQuestionsRepository _repository = new SqlQuestionsRepository();

    // Really only called during unit testing...
    public QuestionsController(IQuestionsRepository repository)
    {
        _repository = repository;
    }
}

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

Наша команда делала это раньше, и обычно мы устанавливаем видимость setter на package-private и оставляем пакет тестового класса таким же, чтобы он мог вызывать setter.

1 голос
/ 25 июня 2009

Один из вариантов - использовать подделки.

public class FakeQuestionsRepository : IQuestionsRepository {
    public FakeQuestionsRepository() { } //simple constructor
    //implement the interface, without going to the database
}

[TestFixture] public class QuestionsControllerTest {
    [Test] public void should_be_able_to_instantiate_the_controller() {
        //setup the scenario
        var repository = new FakeQuestionsRepository();
        var controller = new QuestionsController(repository);
        //assert some things on the controller
    }
}

Другой вариант - использовать макеты и фреймворк, который может автоматически генерировать эти макеты на лету.

[TestFixture] public class QuestionsControllerTest {
    [Test] public void should_be_able_to_instantiate_the_controller() {
        //setup the scenario
        var repositoryMock = new Moq.Mock<IQuestionsRepository>();
        repositoryMock
            .SetupGet(o => o.FirstQuestion)
            .Returns(new Question { X = 10 });
        //repositoryMock.Object is of type IQuestionsRepository:
        var controller = new QuestionsController(repositoryMock.Object);
        //assert some things on the controller
    }
}

Относительно того, где все объекты построены. В модульном тесте вы устанавливаете только минимальный набор объектов: реальный объект, который тестируется, и некоторые фиктивные или поддельные зависимости, которые требуются для реального тестируемого объекта. Например, реальный тестируемый объект является экземпляром QuestionsController - он имеет зависимость от IQuestionsRepository, поэтому мы присваиваем ему либо фальшивый IQuestionsRepository, как в первом примере, или макет IQuestionsRepository, как во втором пример.

Однако в реальной системе весь контейнер установлен на самом верхнем уровне программного обеспечения. Например, в веб-приложении вы настраиваете контейнер, подключая все интерфейсы и реализующие классы, в GlobalApplication.Application_Start.

0 голосов
/ 08 января 2010

Я немного уточню ответ Питера.

В приложениях с большим количеством типов сущностей для контроллера нередко требуются ссылки на несколько репозиториев, сервисов и т. Д. Я считаю утомительным ручное прохождение всех этих зависимостей в моем тестовом коде (тем более что данный тест может включать только одну или две из них). В этих сценариях я предпочитаю IOC в стиле сеттеров, чем в конструктор. Шаблон, которым я пользуюсь так:

public class QuestionsController : ControllerBase
{
    private IQuestionsRepository Repository 
    {
        get { return _repo ?? (_repo = IoC.GetInstance<IQuestionsRepository>()); }
        set { _repo = value; }
    }
    private IQuestionsRepository _repo;

    // Don't need anything fancy in the ctor
    public QuestionsController()
    {
    }
}

Замените IoC.GetInstance<> тем синтаксисом, который используется вашей конкретной платформой IOC.

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

В тесте вам просто нужно вызвать сеттер до вызова любых методов контроллера:

var controller = new QuestionsController { 
    Repository = MakeANewMockHoweverYouNormallyDo(...); 
}

Преимущества такого подхода, ИМХО:

  1. Все еще использует преимущества МОК в производстве.
  2. Проще вручную построить ваши контроллеры во время тестирования. Вам нужно только инициализировать зависимости, которые ваш тест будет фактически использовать.
  3. Возможно создание тестовых конфигураций IOC, если вы не хотите вручную настраивать общие зависимости.
...