TDD — хотите протестировать мой сервисный уровень с поддельным репозиторием, но как?

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

public class RegistrationService: IRegistrationService
{
    public void Register(User user)
    {
        IRepository<User> userRepository = new UserRepository();
        // add user, etc
    }
}

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

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

Предложения?


person Alex    schedule 01.06.2009    source источник
comment
Вы на правильном пути, но передача зависимости через конструктор не делает ваш код вонючим. Это общепринятый принцип проектирования (инверсия управления).   -  person ebrown    schedule 01.06.2009
comment
Это действительно хороший доклад о модульном тестировании с DI: youtube.com/watch?v=wEhu57pih5w< /а>   -  person chickeninabiscuit    schedule 09.06.2010


Ответы (7)


Вам нужно использовать внедрение зависимостей. UserRepository является зависимостью вашего класса RegistrationService. Чтобы сделать ваши классы должным образом пригодными для модульного тестирования (т. е. отдельно от их зависимостей), вам нужно «инвертировать» то, что управляет созданием вашей зависимости. В настоящее время вы имеете прямой контроль и создаете их внутри. Просто инвертируйте этот элемент управления и позвольте чему-то внешнему (например, контейнеру IoC, такому как Castle Windsor) внедрить их:

public class RegistrationService: IRegistrationService
{
    public RegistrationService(IRepository<User> userRepo)
    {
        m_userRepo = userRepo;
    }

    private IRepository<User> m_userRepo;

    public void Register(User user)
    {
        // add user, etc with m_userRepo
    }
}

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

person jrista    schedule 01.06.2009
comment
Я предполагаю, что тогда это путь ... Я не хотел, чтобы мои IRepository были переменными класса, но я думаю, что для инъекции это путь, а затем перегрузка конструктора. Спасибо! - person Alex; 01.06.2009
comment
Вместо внедрения самого репо вы можете внедрить фабрику и создать из нее репо по требованию. - person jrista; 01.06.2009

Внедрение зависимостей может очень пригодиться для таких вещей:

public class RegistrationService: IRegistrationService
{
    public void Register(User user)
    {
        this(user, new UserRepository());
    }

    public void Register(User user, IRepository<User> userRepository)
    {
        // add user, etc
    }

}

Таким образом, вы получаете возможность использовать UserRepository по умолчанию и передавать настраиваемый (Mock или Stub для тестирования) IRepository для всего, что вам нужно. Вы также можете изменить область действия метода 2-го регистра, если вы не хотите подвергать его воздействию всего.

Еще лучшей альтернативой было бы использование контейнера IoC, который купил бы вам синтаксис, который еще более развязан, чем в приведенном выше примере. Вы можете получить что-то вроде этого:

public class RegistrationService: IRegistrationService
{
    public void Register(User user)
    {
        this(user, IoC.Resolve<IRepository<User>>());
    }

    public void Register(User user, IRepository<User> userRepository)
    {
        // add user, etc
    }

}

Существует много контейнеров IoC, как указано в ответах других людей, или вы можете откатить свой собственный.

person Joseph    schedule 01.06.2009
comment
Это выглядит довольно хорошо, но имеет недостаток, заключающийся в том, что мне нужно будет создать второй метод с параметром userRepository для каждого метода в RegistrationService... - person Alex; 01.06.2009
comment
@Алекс, это правильно. Если вы не хотели этого делать, вы могли бы в качестве альтернативы внедрить репозиторий в свой конструктор, но вы уклонялись от этого в своем вопросе. - person Joseph; 01.06.2009
comment
@Alex Однако также обратите внимание, что один из методов является всего лишь оболочкой для определения поведения по умолчанию и не имеет в себе никакого реального поведения. - person Joseph; 01.06.2009
comment
Я не могу сказать, что мне нравится ваш второй метод, в котором вы пытаетесь разрешить IRepo. На самом деле это мало что вам дает, поскольку вы создаете зависимость от контейнера IOC... - person Chad Ruppert; 01.06.2009
comment
@Chad Это верно для ЛЮБОГО контейнера IoC, который вы используете. В конечном итоге вам придется создать зависимость от ЧЕГО-ТО, вопрос в том, от чего вы хотите зависеть? В первом случае RegistrationService зависит от UserRepository. Во втором случае RegistrationService зависит от IoC (что бы это ни было). Конечно, это что-то вроде сценария с отравлением. - person Joseph; 01.06.2009
comment
Почему вы должны зависеть от своего контейнера IOC? Я использую Unity, и он ищет конструкторы и делает это автоматически, конечно, я должен их зарегистрировать, но в ЛЮБОМ из моих доменов или уровней обслуживания нет кода Unity. Чисто на уровне пользовательского интерфейса/приложения (читай веб). RegistrationService должен зависеть от IRepo, как вы сделали, однако, если Ninject или Windsor выпустят новую функцию, я, конечно же, не хочу изменять все свои классы службы/домена просто для того, чтобы вывести то, что на самом деле удобный инструмент. - person Chad Ruppert; 01.06.2009
comment
@Chad Я не защищаю зависимость от Unity, Ninject, StructureMap или любого конкретного контейнера IoC. Я хочу проиллюстрировать здесь то, что вам потребуется некоторая конструкция для разрешения интерфейса (в данном случае IRepository‹User›). В своем ответе я решил обозначить это как IoC.Resolve... В моем предыдущем комментарии я пытался понять, что в какой-то момент вы станете зависимым от чего-то, поэтому вам придется сделать выбор. так или иначе (например, вы выбрали Unity). В первом примере я выбрал зависимость от самого UserRepository. - person Joseph; 01.06.2009
comment
Я не оспариваю твое заявление о том, что ты должен от чего-то зависеть. Что меня не устраивает в вашем втором методе, так это то, что теперь вы становитесь зависимым от ОБА КОНКРЕТНОЙ реализации контейнера IOC и некоторой реализации репо. Вы просто не можете обойтись без репозитория a. Я бы не хотел видеть, как кто-то использует этот второй пример и попадает в очередь, потому что на самом деле пытается решить ВНУТРИ класса. IOC должен быть сделан извне, ИМО, иначе вы побеждаете его цель. Вы бы не согласились на это? - person Chad Ruppert; 01.06.2009
comment
@Чад Думаю, теперь я понимаю недопонимание. Во втором примере НЕ используется конкретная реализация какого-либо контейнера IoC, а также не используется какая-либо реализация IRepository‹User› (в данном случае UserRepository). IoC.Resolve также может быть оболочкой. На самом деле, так и должно быть. Кроме того, во втором примере ничего не известно о UserRepository, в отличие от первого примера. В этом весь смысл второго примера, чтобы показать, как можно удалить зависимость UserRepository. - person Joseph; 01.06.2009
comment
Правда, я действительно верил, что вы делаете бит IoC.Resolve как реальный контейнер IOC. Даже если это оболочка, вам все равно нужно получить доступ к классу, даже если он статичен, как вы демонстрируете. Используете ли вы контейнер IOC для внедрения контейнера IOC в оболочку? :) В любом случае, спасибо за здоровую дискуссию. Я думаю, что наша дискуссия проясняет любой вопрос, который, как я опасался, может возникнуть. Я на самом деле не верю, что у нас вообще разногласия, и ваш совет здравый, просто эта мелочь просто застревает у меня в желудке. Даже тогда это не ужасно, просто мнение. - person Chad Ruppert; 01.06.2009
comment
@Чад И тебе спасибо. Я всегда рад хорошему обсуждению! Честно говоря, единственная причина, по которой я бы выполнял разрешение IoC внутри класса, как описано, - это если бы я по какой-то причине выставлял его извне. Скажем, например, RegistrationService выставлялся как служба WCF или что-то в этом роде. Я бы не хотел, чтобы потребители этой службы передавали свою собственную реализацию IRepository‹User›, это было бы ужасно! Но в целом, я считаю, что внедрение зависимости - это хорошая практика. - person Joseph; 01.06.2009

Что вы должны делать, так это использовать инверсию управления или инъекцию зависимостей.

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

Существует очень много контейнеров IOC, Unity, Ninject, Castle Windsor и StructureMap, и это лишь некоторые из них.


Я просто хотел внести ясность: во время ваших модульных тестов я не уверен, что вы действительно захотите использовать контейнер IOC. Интеграционные тесты, конечно, но для модульных тестов просто обновите объект, который вы тестируете, и предоставьте свою подделку во время этого процесса. Контейнеры IOC, по крайней мере, IMO, помогают на уровне тестирования приложений или интеграции и упрощают настройку.

Ваши модульные тесты, вероятно, должны оставаться «чистыми» и работать только с объектами, которые вы пытаетесь протестировать. По крайней мере, это то, что работает для меня.

person Chad Ruppert    schedule 01.06.2009

Просто чтобы добавить немного ясности (или, возможно, нет) к тому, что все сказали, то, как в основном работает IOC, вы говорите ему заменить X на Y. Итак, что вы должны сделать, это принять IRepository (как заявили другие) как параметр а затем в IOC вы бы сказали заменить IRepository на MockedRepository (например) для этого конкретного класса.

Если вы используете IoC, который позволяет инициализировать web.config, переход от dev к prod становится очень плавным. Вы просто изменяете соответствующие параметры в конфигурационном файле, и вы уже в пути — без необходимости прикасаться к коду. Позже, если вы решите перейти к другому источнику данных, вы просто отключите его, и все готово.

IoC — это забавная штука.

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

person Chance    schedule 01.06.2009

Я сейчас над чем-то работаю, ну, почти дословно. Я использую MOQ для макета прокси-экземпляра моего IUserDataRepository.

С точки зрения тестирования:

//arrange
var mockrepository = new Mock<IUserDataRepository>();
// InsertUser method takes a MembershipUser and a string Role
mockrepository.Setup( repos => repos.InsertUser( It.IsAny<MembershipUser>(), It.IsAny<string>() ) ).AtMostOnce();

//act
var service = new UserService( mockrepository.Object );
var createResult = service.CreateUser( new MembershipUser(), "WebUser" );

//assert
Assert.AreEqual( MembershipCreateUserResult.Success, createResult );
mockrepository.VerifyAll();

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

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

person MotoWilliams    schedule 02.06.2009

Вы можете использовать контейнер IOC, а затем зарегистрировать другой репозиторий.

person Jon Masters    schedule 01.06.2009

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

person Eduardo Scoz    schedule 01.06.2009