NSubstitute icon indicating copy to clipboard operation
NSubstitute copied to clipboard

Support for ref returns

Open metakirby5 opened this issue 5 years ago • 7 comments

C# 7.0 added support for ref returns. I tried to mock the value of one such method via .Returns, but got the following:

System.InvalidProgramException : Invalid IL code in Castle.Proxies.IPlayerManagerProxy:get_PlayerInfo (): IL_0030: ret

Where we have:

  public struct PlayerInfo {
    // ...
  }

  public interface IPlayerManager {
    ref PlayerInfo PlayerInfo { get; }
  }

Is there a different way I should be mocking ref returns?

metakirby5 avatar Jan 23 '20 20:01 metakirby5

Hi @metakirby5 ,

Thanks for raising this. I tried to reproduce it but got this instead:

System.NullReferenceException : Object reference not set to an instance of an object.
   at Castle.Proxies.ObjectProxy.get_PlayerInfo()
   at NSubWorkshop.Tests...

This is with the following test (dotnet core 2, mac):

[Fact]
public void Example() {
    var sub = Substitute.For<IPlayerManager>();
    var result = sub.PlayerInfo;
}

Could you please let me know what .NET environment/platform you're running, and maybe show the full test as well?

dtchepak avatar Jan 24 '20 00:01 dtchepak

Hi @dtchepak, thank you for the swift response!

Environment:

  • Unity 2019.2.14f1
  • NSubstitute 3.1.0.0

Below is a full self-contained repro. I actually forgot to add that I was working with ref readonly, my mistake.

using NSubstitute;
using NUnit.Framework;

namespace RefTestExample {
  public class RefTest {
    public readonly struct PlayerInfo {
      public readonly int Level;

      public PlayerInfo(int level) {
        Level = level;
      }
    }

    public interface IPlayerManager {
      ref readonly PlayerInfo PlayerInfo { get; }
    }

    [Test]
    public void GetRef() {
      var sub = Substitute.For<IPlayerManager>();
      sub.PlayerInfo.Returns(new PlayerInfo(5));
      Assert.AreEqual(5, sub.PlayerInfo.Level);
    }
  }
}

metakirby5 avatar Jan 24 '20 01:01 metakirby5

This seems to affect Moq (4.13.1) too:

    [Fact]
    public void MoqExample() {
        var mock = new Mock<IPlayerManager>().Object;
        var result = mock.PlayerInfo;
    }

Possibly related: https://github.com/moq/moq4/issues/829

//cc: @stakx (hope you have an awesome holiday! 😄 )

dtchepak avatar Jan 24 '20 01:01 dtchepak

Thanks for the full repro @metakirby5.

I don't think this is going to be a quick fix, so you may need to manually mock this interface for now sorry. :(

dtchepak avatar Jan 24 '20 01:01 dtchepak

@dtchepak no worries! The difference between using a class versus readonly struct in our case is imperceptible anyways, so we're erring on the side of testability for now and using class which doesn't have this problem. Thank you for taking a look!

metakirby5 avatar Jan 24 '20 01:01 metakirby5

@metakirby5 Thanks for your understanding. :)

I worry about compromising the code for testability here. There is nothing untestable about this from a design perspective I don't think; it is just a limitation of our library (and based on https://github.com/moq/moq4/issues/829, potentially a limit to what we can do with .NET reflection?).

If you want to keep the struct you could try something like this:

    public class TestPlayerManager : IPlayerManager {
        static PlayerInfo DefaultPlayer = new PlayerInfo();
        public ref PlayerInfo PlayerInfo {
            get {
                return ref DefaultPlayer;
            }
        }
        public void SetPlayerInfo(ref PlayerInfo info) => DefaultPlayer = info;
    }

If IPlayerManager has other members that don't rely on ref return you may be able to use partial subs to get a mix of both worlds:

    public interface IPlayerManager {
        ref PlayerInfo PlayerInfo { get; }
        void DoStuff();
    }

    public abstract class TestPlayerManager : IPlayerManager {
        static PlayerInfo DefaultPlayer = new PlayerInfo();
        // Non-virtual, so NSubstitute won't interfere with calls to this. 
        // Make sure to install NSubstitute.Analyzers which will warn you if you accidentally try 
        // to configure this with NSubstitute.
        public ref PlayerInfo PlayerInfo {
            get {
                return ref DefaultPlayer;
            }
        }
        // Non-virtual; used to configure `PlayerInfo` return value.
        public void SetPlayerInfo(ref PlayerInfo info) => DefaultPlayer = info;

        // This one is abstract/virtual so you can configure it with NSubstitute:
        public abstract void DoStuff();
    }

    static PlayerInfo info = new PlayerInfo();

    [Fact]
    public void NewExample() {
        var sub = Substitute.For<TestPlayerManager>();
        // Arrange
        sub.SetPlayerInfo(ref info);
        // Act (er, fake act. Pretend subject under test is calling `sub.DoStuff`)
        sub.DoStuff();
        // Assert
        sub.Received().DoStuff();
    }

If it really doesn't make a difference then I guess it is fine to leave as is. If you need to switch to struct then hopefully this option will work for you.

dtchepak avatar Jan 24 '20 02:01 dtchepak

@dtchepak Oh, I didn't know about partial subs! That sounds like a great solution, thank you :)

metakirby5 avatar Jan 24 '20 04:01 metakirby5