NSubstitute icon indicating copy to clipboard operation
NSubstitute copied to clipboard

Testing default interface methods in a mocked interface

Open ursmeili opened this issue 3 years ago • 7 comments

I would like to mock an interface which features a default interface method (introduced in C#8).

However, it seems like the default interface method also gets mocked away, so in the example below, the method HasNumber() is never called, and the test fails.

Is there any solution for it already? If not, I'd request a feature to make this work.

using System.Linq;
using FluentAssertions;
using NSubstitute;
using Xunit;

public interface IFoo
{
    int[] Numbers { get; }

    bool HasNumber(int n)
    {
        return this.Numbers.Contains(n);
    }
}

public class Test
{
    [Fact]
    public void HasNumber()
    {
        // arrange
        var mock = Substitute.For<IFoo>();
        mock.Numbers.Returns(new[] { 1, 2 });

        // act
        var actual = mock.HasNumber(1);

        // assert
        actual.Should().BeTrue();
    }
}

ursmeili avatar Sep 07 '22 12:09 ursmeili

Note that Moq supports this since April 2022 (see https://jeremybytes.blogspot.com/2019/09/c-8-interfaces-unit-testing-default.html). It's a pity NSubstitute does not support it.

ursmeili avatar Sep 14 '22 14:09 ursmeili

Hi @ursmeili , Sorry, this is not implemented. Do you have any time to attempt a PR?

dtchepak avatar Oct 15 '22 04:10 dtchepak

@dtchepak sorry, no, I have no spare time currently.

ursmeili avatar Oct 17 '22 14:10 ursmeili

This is still an issue, right?

If so, is there some starting tips for a PR to handle this, @dtchepak ? I can try to contribute, but I have no idea about where to start...

andreminelli avatar Mar 24 '23 15:03 andreminelli

Hi @andreminelli ,

Here's a test to start:

#if NET5_0_OR_GREATER
using System.Linq;
using NUnit.Framework;

namespace NSubstitute.Acceptance.Specs.FieldReports
{
    public class InterfaceWithDefaults
    {
        // From https://github.com/nsubstitute/NSubstitute/issues/703
        public interface IHaveDefaultMembers
        {
            int[] Numbers { get; }
            bool HasNumber(int n) => this.Numbers.Contains(n);
        }

        [Test]
        public void Test_Defaults()
        {
            var sub = Substitute.For<IHaveDefaultMembers>();
            sub.Numbers.Returns(new[] { 1, 2 });
            //sub.When(x => x.HasNumber(Arg.Any<int>())).CallBase();

            Assert.That(sub.HasNumber(2), Is.True);
        }
    }
}
#endif

With the CallBase() line uncommented the test fails with:

NSubstitute.Exceptions.CouldNotConfigureCallBaseException : Cannot configure the base method call as base method implementation is missing. You can call base method only if you create a class substitute and the method is not abstract.

This is thrown when ICall.CanCallBase is false, so I'm guessing first thing is to work out whether that state is correct. It might be a matter of updating that to detect default members on interfaces. If not we might need to look at whether we need to do anything specific to the generated proxy to allow this (maybe can take inspiration from Moq's approach here?).

Hope this helps! Thanks so much for taking a look at this. :bow:

dtchepak avatar Apr 01 '23 05:04 dtchepak

@dtchepak, very nice! This is much more than I was expecting 😊

But only thank me if - or "when", let's get optimistic - I could come up with some solution.

andreminelli avatar Apr 01 '23 13:04 andreminelli

@dtchepak , as a first look, even after solving that state problem we will probably have an issue with Castle.DynamicProxy which still does not support default members on interfaces, too :(

The approach made on Moq uses System.Reflection.Emit to workaround this lack on Castle.DynamicProxy. I would rather check if we could get this support on it before trying something like that in NSubstitute - I have poked the related issue to get an update.

Meanwhile I will study more the NSubstitute repo and check that state problem.

andreminelli avatar Apr 01 '23 17:04 andreminelli