удобочитаемая замена IEnumerable интерфейсов

У меня есть следующие интерфейсы

public interface IRibbonCommandsProvider
{
    IEnumerable<IRibbonCommand> GetRibbonCommands();
}
public interface IRibbonCommand
{
    string Group { get; }
    string Tab { get; }
    string Name { get; }
    string Image { get; }
    void Execute();
}

И следующий код замены:

public class TabsViewModelTests
{
    [Fact]
    public void Initialize_BuildsCorrectRibbonTree()
    {
        var commands = Substitute.For<IRibbonCommandsProvider>();
        commands.GetRibbonCommands().Returns(
            new[]
            {
                new RibbonCommand { Tab = "Tab1", Group = "Group1", Name = "Name1" },
                new RibbonCommand { Tab = "Tab1", Group = "Group1", Name = "Name2" },
                new RibbonCommand { Tab = "Tab2", Group = "Group1", Name = "Name3" },
                new RibbonCommand { Tab = "Tab2", Group = "Group2", Name = "Name3" }
            });
           ...
    }

    private class RibbonCommand : IRibbonCommand
    {
        public string Group { get; set; }
        public string Tab { get; set; }
        public string Name { get; set; }
        public string Image { get; set; }
        public void Execute() {}
    }
}

Есть ли с помощью NSubstitute умный способ избавиться от класса-заглушки RibbonCommand (это не что иное, как фальшивая реализация IRibbonCommand — и это работа NSubstitute) и по-прежнему иметь список поддельных команд ленты, легко читаемый как указано выше?.

Я не могу придумать удобочитаемый способ использования метода NSubsitute .Returns() fluent, не заканчивая намного большим (и нечитаемым) кодом.

Обновление: Классный метод расширения NSubstitute мог бы выглядеть так. Я просто не знаю, можно ли и как это построить:

public static ConfiguredCall ReturnsMany<T>(
    this IEnumerable<T> value,
    Action<T> configureThis,
    params Action<T>[] configureThese)
{
    ...
}

Он будет использоваться следующим образом:

commands.GetRibbonCommands().ReturnsMany(
    subst =>
    {
        subst.Tab.Returns("Tab1");
        subst.Group.Returns("Group1");
        subst.Name.Returns("Name1");
    },
    subst =>
    {
        subst.Tab.Returns("Tab1");
        subst.Group.Returns("Group1");
        subst.Name.Returns("Name2");
    },
    subst =>
    {
        subst.Tab.Returns("Tab2");
        subst.Group.Returns("Group1");
        subst.Name.Returns("Name3");
    },
    subst =>
    {
        subst.Tab.Returns("Tab2");
        subst.Group.Returns("Group1");
        subst.Name.Returns("Name3");
    });

person bitbonk    schedule 10.06.2015    source источник


Ответы (4)


Я думаю, что то, что у вас есть, очень хорошо — довольно кратко и ясно.

Если вы действительно хотите избавиться от класса, вы можете использовать метод создания замены для IRibbonCommand:

    private IRibbonCommand Create(string tab, string group, string name)
    {
        var cmd = Substitute.For<IRibbonCommand>();
        cmd.Tab.Returns(tab);
        cmd.Group.Returns(group);
        cmd.Name.Returns(name);
        return cmd;
    }

    [Fact]
    public void Initialize_BuildsCorrectRibbonTree()
    {
        var ribbonCommands = new[] {
            Create("tab1", "group1", "name1"),
            Create("tab1", "group1", "name2"),
            Create("tab2", "group1", "name3"),
            Create("tab2", "group1", "name4")
        };
        var commands = Substitute.For<IRibbonCommandsProvider>();
        commands.GetRibbonCommands().Returns(ribbonCommands);
        // ...
    }

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


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

    Create(tab: "tab1", group: "group1", name: "name1"),
person David Tchepak    schedule 11.06.2015
comment
Я думал об этом подходе, однако создание метода для каждого интерфейса, а не создание класса, казалось, что это просто обмен одной дополнительной работы на другую, поэтому в своем ответе я отказался от общего подхода. - person forsvarir; 11.06.2015
comment
В нынешнем виде ваш код не будет работать (по крайней мере, для меня). Вы не можете настроить Returns для IRibbonCommand заменителей из Returns для метода GetRibbonCommands. Сначала вы должны создать массив, а затем передать его, поэтому я использую переменную ribbonCommands в своем ответе. В противном случае вы получите исключение от NSubstitute при запуске теста. - person forsvarir; 11.06.2015
comment
@forsvarir: я согласен, что это не сильно выигрывает/проигрывает нам с точки зрения размера кода/требуемой работы. Основные преимущества заключаются в том, что код становится более устойчивым к изменениям интерфейса и позволяет нам запрашивать полученные вызовы и заглушать другие элементы для каждого элемента. - person David Tchepak; 11.06.2015
comment
@forsvarir: о, очень хорошая мысль об ошибке в моем коде! Я обновил ответ, чтобы исправить это. Спасибо! :) - person David Tchepak; 11.06.2015
comment
Я думаю, что проблемы с дополнительной переменной можно избежать, используя лямбду для отсрочки создания возвращаемого значения. См. github.com/nsubstitute/NSubstitute/issues/. - person David Tchepak; 11.06.2015

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

ОБНОВЛЕНО до рабочего теста

[Test]
public void Test()
{
    Func<Action<IRibbonCommand>, IRibbonCommand> cmd = config =>
    {
        var c = Substitute.For<IRibbonCommand>();
        config(c);
        return c;
    };

    var ribbonCommands = new[]
    {
        cmd(c => { c.Tab.Returns("Tab1"); c.Group.Returns("Group1"); c.Name.Returns("Name1"); }),
        cmd(c => { c.Tab.Returns("Tab1"); c.Group.Returns("Group1"); c.Name.Returns("Name2"); }),
        cmd(c => { c.Tab.Returns("Tab2"); c.Group.Returns("Group1"); c.Name.Returns("Name3"); }),
        cmd(c => { c.Tab.Returns("Tab2"); c.Group.Returns("Group1"); c.Name.Returns("Name4"); })
    };

    var commandsProvider = Substitute.For<IRibbonCommandsProvider>();
    commandsProvider.GetRibbonCommands().Returns(ribbonCommands);
}
person dadhi    schedule 11.06.2015
comment
В вашем вызове config(c) отсутствует точка с запятой в конце строки. Если это исправлено, то код в его нынешнем виде работать не будет, это приведет к CouldNotSetReturnDueToNoLastCallException из внешнего вызова Returns. Возможно, вы упускаете что-то еще. - person forsvarir; 12.06.2015
comment
@forsvarir Спасибо, что нашли ошибку в коде. Прошу прощения, писал с мобильного, не успел проверить. Обновил до рабочей версии. Довольно лаконично. - person dadhi; 18.06.2015
comment
Я переключил свой отрицательный голос теперь, когда ваш код работает - person forsvarir; 18.06.2015

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

public static class ReadOnlySubstitute {
   static public T For<T>(object source) where T : class {
      var sub = Substitute.For<T>();

      foreach (var prop in source.GetType().GetProperties()) {
         sub.GetType().GetProperty(prop.Name).GetValue(sub).Returns(prop.GetValue(source));
      }
      return sub;
   }
}

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

Затем это можно использовать в вашем тесте, чтобы предоставить анонимным объектам параметры:

[Test]
public void Initialize_BuildsCorrectRibbonTree() {
    var ribbonCommands = new[]
    {
       ReadOnlySubstitute.For<IRibbonCommand>(new {Tab="Tab1", Group="Grp1", Name="Nam1"}),
       ReadOnlySubstitute.For<IRibbonCommand>(new {Tab="Tab1", Group="Grp1", Name="Nam2"}),
       ReadOnlySubstitute.For<IRibbonCommand>(new {Tab="Tab2", Group="Grp1", Name="Nam3"}),
       ReadOnlySubstitute.For<IRibbonCommand>(new {Tab="Tab2", Group="Grp2", Name="Nam3"})
    };

    var commands = Substitute.For<IRibbonCommandsProvider>();
    commands.GetRibbonCommands().Returns(ribbonCommands);
    ....
}

Это не так лаконично, как использование класса RibbonCommand, так как вы должны построить массив перед передачей его в метод Returns, потому что NSubstitute запутается, если вы попытаетесь установить Returns для элементов одновременно с GetRibbonCommands, но я думаю, что это довольно близко.

person forsvarir    schedule 10.06.2015
comment
Другим недостатком является то, что у меня нет intellisense для параметра source метода For. - person bitbonk; 11.06.2015
comment
@bitbonk Действительно... Я упустил это из виду, так как вас беспокоила удобочитаемость. Я думаю, что только наличие геттеров для свойств в интерфейсе немного связывает ваши руки, когда дело доходит до intellisense. Лично я, вероятно, остановился бы на классе RibbonCommand, потому что накладные расходы не такие велики, и это, вероятно, упростит вашу жизнь в долгосрочной перспективе. Конечно, со временем кто-то может придумать лучший ответ, который докажет, что я ошибаюсь :) - person forsvarir; 11.06.2015

Это действительно улучшение (субъективное) ответа @dadhi в сочетании с ответом @David Tchepak на другой вопрос.

Таким образом, вместо того, чтобы создавать новый Func для каждого интерфейса, который вы хотите использовать, как описано @dadhi, вы можете вместо этого создать общий метод, который принимает Action. Вы можете быть этим в общем классе, примерно так:

static class ConfiguredSub {
    public static T For<T>(Action<T> config) where T : class {
        var c = Substitute.For<T>();
        config(c);
        return c;
    }
}

Проблема, с которой я столкнулся в моем другом ответе, заключалась в том, что если вы вложили Returns, NSubstitute запутается и начнет генерировать исключения. Оказывается, как описано @David здесь, вы можете передать Func, чтобы отложить выполнение и обойти эту проблему. Если вы объедините эти две вещи, вы получите что-то очень близкое к тому, что вам нужно.

[Test]
public void Initialize_BuildsCorrectRibbonTree() {

    var commands = Substitute.For<IRibbonCommandsProvider>();
    commands.GetRibbonCommands().Returns(x => new[] {    
        ConfiguredSub.For<IRibbonCommand>(subst => 
                                      { 
                                          subst.Tab.Returns("Tab1"); 
                                          subst.Group.Returns("Group1"); 
                                          subst.Name.Returns("Name1"); 
                                      }),
        ConfiguredSub.For<IRibbonCommand>(subst => 
                                      { 
                                          subst.Tab.Returns("Tab1"); 
                                          subst.Group.Returns("Group1"); 
                                          subst.Name.Returns("Name2"); 
                                      }),
        ConfiguredSub.For<IRibbonCommand>(subst => 
                                      { 
                                          subst.Tab.Returns("Tab2"); 
                                          subst.Group.Returns("Group1"); 
                                          subst.Name.Returns("Name3"); 
                                      }),
        ConfiguredSub.For<IRibbonCommand>(subst => 
                                      { 
                                          subst.Tab.Returns("Tab2"); 
                                          subst.Group.Returns("Group1"); 
                                          subst.Name.Returns("Name4"); 
                                      })
    });

    // ...

}
person forsvarir    schedule 18.06.2015