Правильный способ проверки параметров, передаваемых в макет, установлен так, как ожидается - PullRequest
6 голосов
/ 19 апреля 2011

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

У меня есть ситуация, когда у меня есть некоторая логика в классе Presenter, который получает значения на основе входных данных и передает их в класс Creator. Чтобы проверить логику в классе Presenter, я хочу убедиться, что при вызове Creator соблюдаются правильные производные значения. Я придумал приведенный ниже пример, который работает, но я не уверен, что мне нравится такой подход:

[TestFixture]
public class WidgetCreatorPresenterTester
{
    [Test]
    public void Properly_Generates_DerivedName()
    {
        var widgetCreator = new Mock<IWidgetCreator>();
        widgetCreator.Setup(a => a.Create(It.IsAny<Widget>()))
                     .Callback((Widget widget) => 
                     Assert.AreEqual("Derived.Name", widget.DerivedName));

        var presenter = new WidgetCreatorPresenter(widgetCreator.Object);
        presenter.Save("Name");

        widgetCreator.Verify(a => a.Create(It.IsAny<Widget>()), Times.Once());
    }
}

Я обеспокоен тем, что без вызова Verify в конце, нет никакой гарантии, что assert в обратном вызове будет вызвано. Другой подход - установить локальную переменную в обратном вызове:

[Test]
public void Properly_Generates_DerivedName()
{
    var widgetCreator = new Mock<IWidgetCreator>();
    Widget localWidget = null;
    widgetCreator.Setup(a => a.Create(It.IsAny<Widget>()))
        .Callback((Widget widget) => localWidget = widget);

    var presenter = new WidgetCreatorPresenter(widgetCreator.Object);
    presenter.Save("Name");

    widgetCreator.Verify(a => a.Create(It.IsAny<Widget>()), Times.Once());
    Assert.IsNotNull(localWidget);
    Assert.AreEqual("Derived.Name", localWidget.DerivedName);
}

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

В случае, если это полезно, вот остаток кода для этого примера:

public class Widget
{
    public string Name { get; set; }
    public string DerivedName { get; set; }
}

public class WidgetCreatorPresenter
{
    private readonly IWidgetCreator _creator;

    public WidgetCreatorPresenter(IWidgetCreator creator)
    {
        _creator = creator;
    }

    public void Save(string name)
    {
        _creator.Create(
            new Widget { Name = name, DerivedName = GetDerivedName(name) });
    }

    //This is the method I want to test
    private static string GetDerivedName(string name)
    {
        return string.Format("Derived.{0}", name);
    }
}

public interface IWidgetCreator
{
    void Create(Widget widget);
}

РЕДАКТИРОВАТЬ
Я обновил код, чтобы облегчить использование второго подхода, который я изложил в этом вопросе. Я вытащил создание выражения, используемого в Setup / Verify, в отдельную переменную, поэтому мне нужно определить его только один раз. Я чувствую, что этот метод мне удобнее всего, его легко настроить и он не работает с хорошими сообщениями об ошибках.

[Test]
public void Properly_Generates_DerivedName()
{
    var widgetCreator = new Mock<IWidgetCreator>();
    Widget localWidget = null;

    Expression<Action<IWidgetCreator>> expressionCreate = 
        (w => w.Create(It.IsAny<Widget>()));
    widgetCreator.Setup(expressionCreate)
        .Callback((Widget widget) => localWidget = widget);

    var presenter = new WidgetCreatorPresenter(widgetCreator.Object);
    presenter.Save("Name");

    widgetCreator.Verify(expressionCreate, Times.Once());
    Assert.IsNotNull(localWidget);
    Assert.AreEqual("Derived.Name", localWidget.DerivedName);
}

Ответы [ 4 ]

4 голосов
/ 19 апреля 2011

Я делаю Verify со спичками в соответствии с AAA.И из-за этого установка не требуется.Вы можете вставить его, но я выделил его, чтобы он выглядел чище.

[Test]
public void Properly_Generates_DerivedName()
{
    var widgetCreator = new Mock<IWidgetCreator>();

    var presenter = new WidgetCreatorPresenter(widgetCreator.Object);
    presenter.Save("Name");

    widgetCreator.Verify(a => a.Create(MatchesWidget("Derived.Name"));
}

private Widget MatchesWidget(string derivedName)
{
    return It.Is<Widget>(m => m.DerivedName == derivedName);
}
3 голосов
/ 08 августа 2013

Просто чтобы уточнить комментарий @ rsbarro - сообщение об ошибке сбоя Moq:

Ожидаемый вызов на макет хотя бы один раз, но никогда не выполнялся

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

Я часто сталкиваюсь с этим при использовании Moq Verifyпроверить большое количество условий в Verify, где метод должен вызываться с конкретными значениями параметров, которые не являются примитивами, такими как int или string.(Обычно это не проблема для примитивных типов, поскольку Moq перечисляет фактические "выполненные вызовы" для метода как часть исключения).

В результате в этом случае мне потребуетсязахватите переданные параметры (которые мне кажутся дублирующими работу Moq), или просто переместите утверждение в строку Setup / Callbacks.

, например, Проверка:

widgetCreator.Verify(wc => wc.Create(
      It.Is<Widget>(w => w.DerivedName == "Derived.Name"
                    && w.SomeOtherCondition == true),
      It.Is<AnotherParam>(ap => ap.AnotherCondition == true),
  Times.Exactly(1));

Будет перекодировано как

widgetCreator.Setup(wc => wc.Create(It.IsAny<Widget>(),
                                    It.IsAny<AnotherParam>())
             .Callback<Widget, AnotherParam>(
              (w, ap) =>
                {
                  Assert.AreEqual("Derived.Name", w.DerivedName);
                  Assert.IsTrue(w.SomeOtherCondition);
                  Assert.IsTrue(ap.AnotherCondition, "Oops");
                });

// *** Act => invoking the method on the CUT goes here

// Assert + Verify - cater for rsbarro's concern that the Callback might not have happened at all
widgetCreator.Verify(wc => wc.Create(It.IsAny<Widget>(), It.Is<AnotherParam>()),
  Times.Exactly(1));

На первый взгляд, это нарушает AAA , поскольку мы помещаем Assert в строку с Arrange (хотя обратный вызовтолько вызвано во время Act), но, по крайней мере, мы можем добраться до сути проблемы.

Также см. идею Хэди о перемещении лямбда-функции обратного вызова в свое собственное имяФункция, или, что еще лучше, в C # 7, ее можно переместить в Локальную функцию в нижней части метода модульного тестирования, чтобы можно было сохранить макет AAA.

3 голосов
/ 23 апреля 2011

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

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

1 голос
/ 27 апреля 2017

Основываясь на ответе StuartLC в этой теме, вы следуете тому, что он предлагает, не нарушая AAA , записывая функцию "inline", которая передается в Verify метод макета объекта.

Так, например:

// Arrange
widgetCreator
  .Setup(wc => wc.Create(It.IsAny<Widget>(), It.IsAny<AnotherParam>());

// Act
// Invoke action under test here...

// Assert
Func<Widget, bool> AssertWidget = request =>
{
  Assert.AreEqual("Derived.Name", w.DerivedName);
  Assert.IsTrue(w.SomeOtherCondition);
  Assert.IsTrue(ap.AnotherCondition, "Oops");
  return true;
};

widgetCreator
  .Verify(wc => wc.Create(It.Is<Widget>(w => AssertWidget(w)), It.Is<AnotherParam>()), Times.Exactly(1));
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...