Unit testing 接口IEnumerable的可读替换

Unit testing 接口IEnumerable的可读替换,unit-testing,nsubstitute,Unit Testing,Nsubstitute,我有以下接口 public interface IRibbonCommandsProvider { IEnumerable<IRibbonCommand> GetRibbonCommands(); } public interface IRibbonCommand { string Group { get; } string Tab { get; } string Name { get; } string Image { get; }

我有以下接口

public interface IRibbonCommandsProvider
{
    IEnumerable<IRibbonCommand> GetRibbonCommands();
}
public interface IRibbonCommand
{
    string Group { get; }
    string Tab { get; }
    string Name { get; }
    string Image { get; }
    void Execute();
}
它的用法如下:

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");
    });
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;
   }
}
static class ConfiguredSub {
    public static T For<T>(Action<T> config) where T : class {
        var c = Substitute.For<T>();
        config(c);
        return c;
    }
}

我看不出有什么东西能做你想做的事。一种选择可能是编写自己的扩展方法,使构造更容易。比如说:

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");
    });
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;
   }
}
static class ConfiguredSub {
    public static T For<T>(Action<T> config) where T : class {
        var c = Substitute.For<T>();
        config(c);
        return c;
    }
}
公共静态类ReadOnlySubstitute{
(对象源)的静态公共T,其中T:class{
var sub=替换为();
foreach(source.GetType().GetProperties()中的var prop){
sub.GetType().GetProperty(prop.Name).GetValue(sub).Returns(prop.GetValue(source));
}
返回子节点;
}
}
上面的代码本质上是为给定接口创建一个替换,然后在提供的对象中指定的每个属性上设置一个返回

然后可以在您的测试中这样使用它来为匿名对象提供参数:

[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);
    ....
}
[测试]
public void Initialize_BuildsCorrectRibbonTree(){
var ribbonCommands=new[]
{
ReadOnlySubstitute.For(新的{Tab=“Tab1”,Group=“Grp1”,Name=“Nam1”}),
ReadOnlySubstitute.For(新的{Tab=“Tab1”,Group=“Grp1”,Name=“Nam2”}),
ReadOnlySubstitute.For(新的{Tab=“Tab2”,Group=“Grp1”,Name=“Nam3”}),
ReadOnlySubstitute.For(新的{Tab=“Tab2”,Group=“Grp2”,Name=“Nam3”})
};
var commands=Substitute.For();
commands.getRibbonCommand()。返回(RibbonCommand);
....
}

它不像使用
RibbonCommand
类那样简洁,因为在将数组传递到
Returns
方法之前必须先构造数组,因为如果试图在
GetRibbonCommand
的同时在元素上设置
Returns
,NSSubstitute会感到困惑,但我认为它相当接近。

我认为你所得到的非常好——非常简洁明了

如果您确实想摆脱该类,可以使用
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);
        // ...
    }

作为替代方案,您可以在测试内部设置命令。然后将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);
}
[测试]
公开无效测试()
{
Func cmd=config=>
{
var c=替换为();
配置(c);
返回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=替换.For();
commandsProvider.GetRibbonCommand().Returns(RibbonCommand);
}

这实际上是@dadhi的一个增强(主观),再加上@David Tchepak对一个问题的回答

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

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");
    });
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;
   }
}
static class ConfiguredSub {
    public static T For<T>(Action<T> config) where T : class {
        var c = Substitute.For<T>();
        config(c);
        return c;
    }
}

另一个缺点是,对于
for
方法的
source
参数,我没有intellisense。@bitbonk确实。。。我掩盖了这一点,因为你关心可读性。我认为,在intellisense方面,只有在接口中的属性上有getter才能束缚你的手脚。就个人而言,我可能会坚持使用
RibbonCommand
类,因为开销并没有那么大,从长远来看,它可能会让您的生活更简单。当然,随着时间的推移,可能会有人想出一个更好的答案来证明我的错误:)我考虑过这种方法,但是为每个接口创建一个方法,而不是创建一个类,似乎只是用一个额外的工作来换另一个,这就是为什么我选择了通用方法来回答问题。目前为止,你的代码不起作用(至少对我来说不起作用)。您不能在
IRibbonCommand
上设置
返回
,从
返回
中替换
GetRibbonCommand
方法。您必须首先创建数组,然后传入,这就是为什么我在回答中使用
ribbonCommands
变量的原因。否则,当您运行测试时,NSubstitute会出现异常。@forsvarir:我同意这不会在代码大小/所需工作量方面给我们带来太多好处/损失。主要的好处是使代码更能容忍对接口的更改,并允许我们查询收到的调用并为每个项存根其他成员。@forsvarir:噢,关于我代码中的错误,这是非常好的一点!我已经更新了答案来修复它。谢谢!:)我认为通过使用lambda延迟返回值的创建,可以避免“额外变量”问题。请参阅您对
config(c)的调用。
在行尾缺少分号。如果这是固定的,那么目前代码将无法工作,它将从外部
返回
调用导致
CouldNotSetReturnDueToNoLastCallException
。也许您遗漏了其他内容。@forsvarir感谢您在代码中发现错误。我的道歉是在手机上写的,没有机会查看。更新到工作版本。非常简洁。现在你的代码运行了,我已经改变了我的反对票