nsubstitute / NSubstitute

A friendly substitute for .NET mocking libraries.
https://nsubstitute.github.io
Other
2.65k stars 260 forks source link

Introdce a ReturnsMany() extension method to be able to substitute an IEnumerable of an interface #184

Closed bitbonk closed 6 years ago

bitbonk commented 9 years ago

In my application under test I have the following interfaces

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

And in a test the follwing substitution code:

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() {}
    }
}

To be able to have a readable arrange part in my test, I had to create the stub RibbonCommand class. This should be Nsubstitutes job! But because AFAIK there is actually no typesafe way to do this with NSubstiute that as anywhere as readable as the above solution, I just had to introduce this class.

I propose you introduce a ReturnsMany extension method with a signaturer that looks something like this:

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

In my example it would be used like this:

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");
    });
dtchepak commented 9 years ago

Thanks for the suggestion!

My first thought is that it would be nice to factor out the duplication into a method to create each substitute, in which case this starts to look like this.

Your suggestion of using lambdas per item also reminded me of another way to do this while avoiding the problem of requiring an extra variable mentioned in forsvarir's answer:

    [Fact]
    public void Initialize_BuildsCorrectRibbonTree2()
    {
        var commands = Substitute.For<IRibbonCommandsProvider>();
        commands.GetRibbonCommands().Returns(x => new [] {
            Create("tab1", "group1", "name1"),
            Create("tab1", "group1", "name1"),
            Create("tab1", "group1", "name1")
        });
    }

Would that get closer to what you want? Or are you keen to avoid any external function definition?

bitbonk commented 9 years ago

The downsides of this approach are that I do not have intellisense inside the Create method and the Create method is yet another thing I would have to declare outside the test itself.

dtchepak commented 9 years ago

You should still get intellisense inside Create (and when calling Create). All the types are known at compile time. Can you explain where intellisense is failing for you?

I agree it would be nice to not have to declare anything outside the test, but in this case i think the ReturnsMany sample ends up being more verbose than using Create, which outweighs the advantage of keeping it all in the same test.

dtchepak commented 6 years ago

Closing as part of a general cleanup of blocked issues. Please re-open if more information comes to light which will let us proceed with this.