使用 NSubstitute 模拟 IFlurl 库方法会抛出空引用异常

Mocking IFlurl library methods using NSubstitute is throwing null reference exception

我正在使用 flurl 并且正在尝试对以下代码进行单元测试:

public class MyRestClient
{
    public async Task<T> Request<T>(IFlurlRequest flurlRequest)
    {
        try  
        {
            return await flurlRequest
                    .WithOAuthBearerToken("my-hardcoded-token")
                    .GetAsync()
                    .ReceiveJson<T>();  
        }
        catch(HttpFlurlException)
        {
            throw new MyCustomException();
        }
    }
}

我要测试的是,如果flurlRequest抛出类型HttpFlurlException的异常,那么它将抛出MyCustomException。我的想法是最小化 flurlrequest 并抛出异常。这就是我布置测试的方式:

var moq = Substitute.For<IFlurlRequest>();
// Problem is with line below:
moq.When(x => x.WithOAuthBearerToken("dummy")).Do(x => { throw new HttpFlurlException(); } );

var myClient = new MyRestClient();

Func<Task> call = async () => { await myClient.Request<object>(moq); };

// FluentAssertions
call.Should().Throw<MyCustomException>();

运行 returns NullReferenceException 时的代码:

Exception has occurred: CLR/System.NullReferenceException
An exception of type 'System.NullReferenceException' occurred in 
Flurl.Http.dll but was not handled in user code: 'Object reference not 
set to an instance of an object.'
at Flurl.Http.HeaderExtensions.WithHeader[T](T clientOrRequest, String name, Object value)

所以我看到它与 headers 相关...所以我也尝试通过添加来嘲笑它:

var moq = Substitute.For<IFlurlRequest>();
moq.Headers.Returns(new Dictionary<string, object> { {"dummy", new {} };

但我经常遇到同样的异常。我究竟做错了什么?

WithOAuthBearerToken is an extension method, which means it cannot be mocked directly by NSubstitute. When you call When..Do or Returns on an extension method it will run the real code of the extension method. (I recommend adding NSubstitute.Analyzers 添加到您的测试项目以检测这些情况。)

在撰写本文时通过扩展方法实现进行跟踪,应该可以模拟 Headers 属性 以抛出所需的异常,但我认为这拖得太深了库的内部知识,并将导致与特定实现紧密耦合的脆弱测试(这是我们旨在通过模拟避免的!)。

我会非常谨慎地以这种方式模拟第三方库,正如我在 :

中概述的那样

The other option is to test this at a different level. I think the friction in testing the current code is that we are trying to substitute for details of [a third-party library], rather than interfaces we've created for partitioning the logical details of our app. Search for "don't mock types you don't own" for more information on why this can be a problem (I've written about it before here).

如果可能,我建议尝试使用 Flurl's built-in testing support。这应该使您能够伪造出所需的行为,而无需有关 Flurl 内部实现的具体细节。