2

I have a simple wrapper for stackexchange redis:

public interface IRedisClient
{        
    Task<RedisResult> ScriptEvaluate(LuaScript script, object parameters);
}

I have a method that calls ScriptEvaluate

public class Foo
{
    private readonly IRedisClient _client;

    public Foo(IRedisClient client)
    {
        _client = client;
    }

    public void RunScript()
    {
        _client.ScriptEvaluate(LuaScript.Prepare(""), new object());
    }
}

Now when I use NSubstitute to mock IRedisClient that is injected to Foo and then call RunScript

public void Test()
{
    _foo = new Foo(Substitute.For<IRedisClient>());
    _foo.RunScript();
}

I get the following error:

System.TypeLoadException: Method 'AsBoolean' in type 'Castle.Proxies.RedisResultProxy' from assembly 'DynamicProxyGenAssembly2, Version=0.0.0.0, Culture=neutral, PublicKeyToken=a621a9e7e5c32e69' does not have an implementation.

As far as I can see Nsubstitute/Castle internals do not manage to work with RedisResult properly. I did not manage to find out any workarounds.

Is it possible to do something with this?

P.S. I get the same error when I try to configure the mock to return a value (same exception):

_client
    .ScriptEvaluate(null, null)
    .ReturnsForAnyArgs(RedisResult.Create((RedisKey)"result"));
Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207
  • So why not mock `RedisResult` – Nkosi Mar 17 '20 at 11:10
  • Not sure what you mean? It seems it does not matter if I mock or not, when the substitute is created and uses the method it will always use RedisResult on it's own because that's what method returns (regardless of what I setup for the return) – Ilya Chernomordik Mar 17 '20 at 11:16
  • This is a `NSubstitute` issue as I was able to exercise the test to completion with MOQ – Nkosi Mar 17 '20 at 11:52
  • Strange, does not NSubstitute use the same castle proxy as Moq? – Ilya Chernomordik Mar 17 '20 at 11:55
  • Yes it does. So it has to be something with NSub's implementation. – Nkosi Mar 17 '20 at 11:56
  • Any ideas for workaround here? – Ilya Chernomordik Mar 17 '20 at 11:56
  • I am still evaluating that. – Nkosi Mar 17 '20 at 12:02
  • I am convinced this is a bug with nsubstitute. If using an actual implementation of `RedisResult` the mocking library should have no reason to even evaluate it. – Nkosi Mar 17 '20 at 12:27
  • Sounds reasonable, I did not find any way around it and just using my own implementation :( I should probably post it on NSubstitute github. I guess Moq will actually fail as well if it will need to evaluate this class – Ilya Chernomordik Mar 17 '20 at 13:00
  • I have raised this as [NSub issue 615](https://github.com/nsubstitute/NSubstitute/issues/615). Thank you IlyaChernomordik and @Nkosi for the helpful repro cases. If you have any more info please add it to the issue (current platform/env/package versions would be good to know). Thanks again! – David Tchepak Mar 18 '20 at 00:02
  • I was not able to reproduce this on dotnetcore3.1 on Mac. Please let me know which platform/env/package versions you are using. – David Tchepak Mar 18 '20 at 00:10
  • 1
    I have commented on github issue with more details @DavidTchepak, thanks for posting it :) – Ilya Chernomordik Mar 18 '20 at 14:53

2 Answers2

1

I was curious about why mocking the abstract RedisResult was not a simple solution.

This appears to be an issue with NSubstitute's implementation.

Using the following to try and recreate the problem

public class Foo {
    private readonly IRedisClient _client;

    public Foo(IRedisClient client) {
        _client = client;
    }

    public Task<RedisResult> RunScript() {
        return _client.ScriptEvaluate(LuaScript.Prepare(""), new object());
    }
}

I was able to reproduce it using NSubstitute but was able to exercise the test to completion when using another mocking framework (MOQ)

[TestClass]
public class MyTestClass {
    [TestMethod]
    public async Task Test1() {
        //Arrange
        var expected = RedisResult.Create((RedisKey)"result");
        var _client = Substitute.For<IRedisClient>();

        _client
            .ScriptEvaluate(Arg.Any<LuaScript>(), Arg.Any<object>())
            .Returns(expected);

        var _foo = new Foo(_client);

        //Act
        var actual = await _foo.RunScript();

        //Assert
        actual.Should().Be(expected);
    }

    [TestMethod]
    public async Task Test2() {
        //Arrange
        var expected = RedisResult.Create((RedisKey)"result");
        var _client = Mock.Of<IRedisClient>(_ => _.ScriptEvaluate(It.IsAny<LuaScript>(), It.IsAny<object>()) == Task.FromResult(expected));

        var _foo = new Foo(_client);

        //Act
        var actual = await _foo.RunScript();

        //Assert
        actual.Should().Be(expected);
    }
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
0

RedisResult is an abstract type, but there are static Create methods for common scenarios, and a few static properties such as EmptyArray, NullArray, etc. I can't tell you how to configure your particular faking layer, but ultimately, I'd expect something involving RedisResult.Create

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Thanks for the answer, I have actually done that: RedisResult.Create, but it does not seem to help, I get the same error when I do configure substitute, I'll update the question shortly – Ilya Chernomordik Mar 17 '20 at 11:02
  • 1
    I have updated the question. It seems to me that being abstract class is not the issue here but perhaps the fact that there are a number of `abstract internal` methods (`AsBoolean` from my exception being one of them). My guess is that Castle proxy creates a dynamic implementation of RedisResult but it does not "see" the internal ones, and then when this type is being used the runtime figures out not everything is implemented – Ilya Chernomordik Mar 17 '20 at 11:05
  • @IlyaChernomordik that's a very good hypothesis. Could you try [`InternalsVisibleTo`](https://stackoverflow.com/a/19974248/906)? It might also be worth trying to use `Configure` when stubbing this; `_client.Configure().ScriptEvaluate(null, null).ReturnsForAnyArgs(...)`. – David Tchepak Mar 18 '20 at 00:14
  • I am not sure how can I use InternalsVisibleTo, I would have to compile the library from source with this attribute I assume? In the other stackoverflow post it was owned class, while my classes are in the library... – Ilya Chernomordik Mar 18 '20 at 14:58