【问题标题】:readable substitution of IEnumerable of InterfacesIEnumerable 接口的可读替换
【发布时间】:2015-06-10 14:16:13
【问题描述】:

我有以下接口

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");
    });

【问题讨论】:

    标签: unit-testing nsubstitute


    【解决方案1】:

    我认为你所掌握的内容非常好——非常简洁明了。

    如果你真的想摆脱这个类,你可以使用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"),
    

    【讨论】:

    • 我考虑过这种方法,但是为每个接口创建一个方法,而不是创建一个类似乎只是用一个额外的工作换另一个工作,这就是我采用通用方法的原因用我的回答。
    • 就目前而言,您的代码将无法工作(至少它对我不起作用)。您不能在 IRibbonCommand 替换 Returns 中为 GetRibbonCommands 方法设置 Returns。您必须先创建数组,然后将其传入,这就是我在答案中使用 ribbonCommands 变量的原因。否则,当您运行测试时,您会收到 NSubstitute 的异常。
    • @forsvarir:我同意这在代码大小/所需工作方面不会给我们带来太多好处。主要好处是让代码更能容忍接口的变化,让我们查询收到的呼叫并为每个项目存根其他成员。
    • @forsvarir:哦,关于我的代码中的错误非常好!我已经更新了答案来修复它。谢谢! :)
    • 我认为可以通过使用 lambda 延迟创建返回值来避免“额外变量”问题。见github.com/nsubstitute/NSubstitute/issues/…
    【解决方案2】:

    作为替代方案,您可以在测试中设置命令。然后将 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);
    }
    

    【讨论】:

    • 您对config(c) 的调用在行尾缺少分号。如果这是固定的,那么代码将无法工作,它将导致来自外部Returns 调用的CouldNotSetReturnDueToNoLastCallException。也许您还缺少其他东西。
    • @forsvarir 感谢您在代码中发现错误。我的道歉,是从手机上写的,没有机会检查。更新到工作版本。很简洁。
    • 既然您的代码正在运行,我已经放弃了投票
    【解决方案3】:

    我没有看到任何开箱即用的东西可以满足您的需求。一种选择可能是您编写自己的扩展方法以使构建更容易。所以,是这样的:

    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 方法之前构造数组,因为如果您尝试在元素上设置Returns,NSubstitute 会混淆与GetRibbonCommands 上的时间相同,但我认为它相当接近。

    【讨论】:

    • 另一个缺点是我没有 source 方法的 source 参数的智能感知。
    • @bitbonk 确实...我忽略了这一点,因为您关心的是可读性。我认为,就智能感知而言,只有在接口中的属性上使用 getter 会有点束缚你的手。就个人而言,我可能会坚持使用RibbonCommand 类,因为开销不是那么 很大,从长远来看,它可能会让你的生活更简单。当然,随着时间的推移,有人可能会想出一个更好的答案来证明我错了:)
    【解决方案4】:

    这确实是对@dadhi 的answer 的增强(主观),结合@David Tchepak 对different question 的回答。

    因此,不必像@dadhi 所描述的那样为您要使用的每个接口创建一个新的Func,而是可以创建一个采用Action 的通用方法。你可以在一个共享类中是这样的,像这样:

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

    我的另一个answer 遇到的问题是,如果您嵌套了Returns,NSubstitute 会感到困惑并开始抛出异常。事实证明,正如@David here 所述,您可以通过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"); 
                                          })
        });
    
        // ...
    
    }
    

    【讨论】:

      猜你喜欢
      • 2013-03-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-20
      • 2017-10-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多