IEnumerable接口的可读替代

时间:2015-06-10 14:16:14

标签: unit-testing nsubstitute

我有以下界面

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()流畅的方法而不会结束更多(并且不可读)的代码。

更新 一个很酷的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");
    });

4 个答案:

答案 0 :(得分:2)

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

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

答案 1 :(得分:2)

作为替代方案,您可以在测试中设置Command。然后将config func移出测试,并可以随意推广其他类型。 Yagni吧。

更新为工作测试

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

答案 2 :(得分:1)

我没有看到任何开箱即用的事情。一种选择可能是您编写自己的扩展方法以使构造更容易。所以,像这样:

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,但我认为它相当接近。

答案 3 :(得分:1)

这实际上是@ 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"); 
                                      })
    });

    // ...

}