TDD может форсировать создание «поддельных» зависимостей - PullRequest
4 голосов
/ 22 февраля 2011

Я использую стандартную реализацию Model-View-Presenter в приложении ASP.NET WebForms.Мое представление имеет два следующих события: одно указывает, что пользователь заполнил достаточно полей в модели домена, чтобы инициировать проверку дублирования, а другое - обычное событие сохранения.Мой псевдокод выглядит следующим образом:

public class ItemNewPresenter : PresenterBase<IItemNewView>
{
public IItemService Service { get; private set; }
public IItemNewView View { get; private set; }

public ItemNewPresenter(IItemService service, IItemNewView view)
{
    Service = service;
    View = view;
    View.OnSave += DoItemSave;
    View.OnItemIsDuplicateCheck+= DoItemIsDuplicateCheck;
}


private void DoItemIsDuplicateCheck(object sender, CheckItemDuplicateEventArgs e)
{
    CheckForItemDuplication(e.Item);
}

private void CheckForItemDuplication(Item item){

if (Service.IsDuplicateItem(item))
    {
        View.RedirectWithNotification(BuildItemUrl(item), "This item already exists");
    }
}
private void DoItemSave(object sender, SaveItemEventArgs e)
{
    DoItemIsDuplicateCheck(this, e.ToItemDuplicateEventArgs());
    Service.Save(e.Item);
}

}

Вот мой тест, чтобы убедиться, что мой докладчик ведет себя правильно, когда OnItemIsDuplicateCheck поднимается из представления:

[Test]
public void presenter_checking_for_existing_item_should_call_redirect_if_found()
{
    var service = new Mock<IItemService>();
    var view = new Mock<IItemNewView>();
    var presenter = new ItemNewPresenter (service.Object, view.Object);

    var onCheckExistingHandler = view.CreateEventHandler <CheckItemDuplicateEventArgs>();
    view.Object.OnExistingDenominatorCheck += onCheckExistingHandler;
    var eventArgs = new CheckItemDuplicateEventArgs();

    service.Setup(s => s.IsDuplicate(It.Is<CheckItemDuplicateEventArgs>(c => c.Equals(eventArgs)))).Returns(true);

    onCheckExistingHandler.Raise(eventArgs);

    view.Verify(v => v.RedirectWithNotification(It.IsAny<String>(), It.IsAny<string>()), Times.Once());
    service.Verify();
}

Для согласованности я хотел быиметь такую ​​же проверку дубликатов, когда View вызывает событие OnSave.Мой вопрос заключается в том, как мне написать свой тест, когда один из методов, которые я хочу проверить (CheckForItemDuplication), объявлен в тестируемом классе.Альтернативой проверке вызова метода в SUT (плохо) было бы написать мой тест сохранения с лотов дублированного кода (настройка и утверждение всех моих mock будет скопировано из вышеупомянутого теста), и это такжеделает модульный тест менее сфокусированным.

   [Test]
    public void presenter_saving_item_should_check_for_dupe_and_save_if_not_one()    {
         //duplicate mocks/setups/asserts from duplicate check fixture
         //additional mocks/setups/asserts to test save logic
    }

Я думаю, TDD предложил бы вытащить этот закрытый метод из отдельного класса, который работает с моим Presenter и будет вводиться через DI.Но добавление еще одной зависимости к моему Presenter для функциональности, которая не заслуживает того, чтобы быть автономной абстракцией * и * представляет внутреннюю деталь реализации моего Presenter, кажется ... ну ... сумасшедшей.Я здесь далеко от базы?Должен быть какой-то шаблон проектирования или рефакторинг, который я могу применить, чтобы избежать необходимости превращать закрытый метод в зависимость.

Ответы [ 4 ]

5 голосов
/ 23 февраля 2011

Я думаю, что вы застряли в бесконечном споре между TDD и сокрытием информации, поскольку вы признаете, что инъекция, вероятно, правильная вещь (и, вероятно, так и есть), но также чувствуете, что внешнее взаимодействие не должно волноватьказалось бы, незначительная инъекция.

Пожалуйста, не голосуйте против меня за мою вонючесть за то, что я собираюсь сказать :-)

Теперь, что я иногда делал, когда сталкивался с этой дилеммой, этоизвлеките функцию, сделайте внутренний конструктор с объектом в качестве аргумента, И открытый конструктор без.Открытый ctor перенаправляется на внутреннее устройство с помощью нового объекта, такого как:

public class ClassThatUseInjection
{
    private readonly SomeClass _injectedClass;

    public ClassThatUseInjection(): this(new SomeClass()) {}

    internal ClassThatUseInjection(SomeClass injectedClass)
    {
        _injectedClass = injectedClass;
    }
}


public class SomeClass
{
    public object SomeProperty { get; set; }
}

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

Все еще немного вонючий, да, но не вонючий :-) Или что ты думаешь?

С уважением, Мортен

1 голос
/ 28 февраля 2011

Я бы пошел с тестированием класса как есть, добавив дубликат кода установки. После того, как этот тест пройден, и вы уверены, что все тестовые случаи покрыты, вы можете выполнить рефакторинг своего тестового кода, чтобы удалить дублирование.

Вы можете переместить зависимости (сервис и представление) в приватные поля, а затем добавить метод для создания SUT:

private Mock<IItemService> _service;
private Mock<IItemNewView> _view;

private PresenterBase<IItemNewView> CreateSUT()
{
    _service = new Mock<IItemService>();
    _view = new Mock<IItemNewView>();
    return new ItemNewPresenter (service.Object, view.Object);
}

(я думаю, что большинство людей предпочли бы инициализировать объекты Mock в методе Setup.)

Вызовите CreateSUT из ваших тестов, и теперь стало меньше дублирования. Затем вы можете захотеть добавить приватный метод (методы) для создания обработчика события / вызова события, если оно выполняется одинаково или аналогично в нескольких тестовых примерах.

Наличие этого метода CreateSUT сокращает объем тестового кода, вызывающего ваш конструктор, упрощая в будущем, если вы захотите добавить / удалить / изменить зависимости. Если вы рассматриваете свой тестовый код как любой другой код и используете принцип СУХОЙ, когда вы видите дублирование, это может привести к более явному, более простому для чтения и сопровождению тестовому коду. Работа с очень похожим контекстом настройки и тестирования является распространенной проблемой модульного тестирования и не всегда должна менять способ проектирования / разработки тестируемого класса.

1 голос
/ 26 февраля 2011

Проще провести юнит-тестирование расчета URL, чем юнит-тестирование, когда произошло перенаправление.

Если я вас правильно понял, вы хотите проверить, что mvp-s CheckForItemDuplication() перенаправляет на определенный URL, подняв событие view-mock-s OnItemIsDuplicateCheck.

private void CheckForItemDuplication(Item item)
{
    if (Service.IsDuplicateItem(item))
    {
        View.RedirectWithNotification(BuildItemUrl(item), 
                       "This item already exists");
    }
}

По моему мнению, вы делаете много. Что делать, если вы переписали свой код как

internal protected GetErrorUrlForItem(Item item)
{
    if (Service.IsDuplicateItem(item))
    {
        return BuildItemUrl(item, 
                            "This item already exists");
    }
    return null;
}

private void CheckForItemDuplication(Item item)
{
    var result = GetErrorUrlForItem(item);
    if (result != null)
    {
        View.RedirectWithNotification(result);
    }
}

В unittest просто протестируйте внутренний метод GetErrorUrlForItem(). Вы должны использовать атрибут InternalsVisibleTo, чтобы разрешить доступ к внутреннему методу.

1 голос
/ 26 февраля 2011

Мне будет интересно, если есть лучшие ответы, так как я сталкиваюсь с этим все время.

Альтернативой для проверки вызова метода на SUT (плохо) было бы написать мое сохранениетест с большим количеством дублированного кода (настройка и утверждение всех моих макетов будут скопированы из вышеупомянутого теста), и это также делает юнит-тест менее сфокусированным.

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

Лично я не являюсь поклонником того, чтобы показывать больше, чем необходимо, из класса и / или делать поведение, которое должнобыть ответственность SUT в зависимости просто для облегчения тестирования.«Естественная граница» ответственности класса не должна нарушаться только потому, что вы хотите это проверить.

...