Why do I get a NullReferenceException when testing this async method with MSpec/Moq?

怎甘沉沦 提交于 2019-12-07 13:58:13

问题


I want test if the correct type is returned from an async method. This method uses another async method in a dependency class. The dependency class implements this interface:

Task<string> DownloadStringAsync(string url);

The method I want to test is this:

public async Task<T> GetData<T>(string url) where T : class , new()
{
    var jsonData = await _webClientWrapper.DownloadStringAsync(url);

    if (string.IsNullOrEmpty(jsonData))
        return new T();

    try
    {
        return await JsonConvert.DeserializeObjectAsync<T>(jsonData);
    }
    catch (JsonException inner)
    {
        throw new JsonConvertException("Error converting Json string", inner) { JsonString = jsonData };
    }
}

Testing with xUnit and Moq succeeds:

public class Testes
{
    private const string ValidJson = "{'Nome':'Rogerio','Idade':'51'}";
    protected static JsonWebServiceClassProvider JsonWebServiceClassProvider;
    private static Mock<IWebClientWrapper> _webClientWrapperMoq;
    private static FakeClassFromJson _resultClass;

    [Fact]
    public async static void When_calling_GetData_it_should_return_a_class_of_same_type()
    {
        _webClientWrapperMoq = new Mock<IWebClientWrapper>();
        _webClientWrapperMoq
            .Setup(w => w.DownloadStringAsync(Moq.It.IsAny<string>()))
            .Returns(Task.FromResult(ValidJson));

        JsonWebServiceClassProvider = new JsonWebServiceClassProvider(_webClientWrapperMoq.Object);

        _resultClass = await JsonWebServiceClassProvider
            .GetData<FakeClassFromJson>(Moq.It.IsAny<string>());

        Assert.IsType<FakeClassFromJson>(_resultClass);
    }
}

Testing with MSpec and Moq:

[Subject("JsonWebServiceClassProvider")]
public class When_calling_GetData_with_a_valid_Json_Service_Url
{
    private const string ValidJson = "{'Nome':'Rogerio','Idade':'51'}";
    protected static JsonWebServiceClassProvider JsonWebServiceClassProvider;
    protected static Mock<IWebClientWrapper> WebClientWrapperMoq;
    protected static FakeClassFromJson ResultClass;

    Establish context = () =>
    {
        WebClientWrapperMoq = new Mock<IWebClientWrapper>();
        WebClientWrapperMoq
            .Setup(w => w.DownloadStringAsync(Moq.It.IsAny<string>()))
            .Returns(Task.FromResult(ValidJson));

        JsonWebServiceClassProvider = new JsonWebServiceClassProvider(WebClientWrapperMoq.Object);
    };

    Because of = () => ResultClass = JsonWebServiceClassProvider
        .GetData<FakeClassFromJson>(Moq.It.IsAny<string>())
        .Await();

    It should_return_a_class_of_same_type = () => ResultClass.ShouldBeOfType<FakeClassFromJson>();
}

It also fails with these Because statements

Because of = () => JsonWebServiceClassProvider
    .GetData<FakeClassFromJson>(Moq.It.IsAny<string>())
    .ContinueWith(task => ResultClass = task.Result)
    .Wait();

Because of = () => ResultClass = JsonWebServiceClassProvider
    .GetData<FakeClassFromJson>(Moq.It.IsAny<string>())
    .Result;

This fails with a NullReferenceException in the line

public async Task<T> GetData<T>(string url) where T : class , new()
{
    string jsonData = await _webClientWrapper.DownloadStringAsync(url);
    // ...
}

SOLVED

While waiting for a response, did some refactoring and voilà! I created a base class with an Establish statement and initiated the mock object there:

public class JsonWebServiceClassProviderSpecs
{
    protected static JsonWebServiceClassProvider JsonWebServiceClassProvider;
    protected static Mock<IWebClientWrapper> WebClientWrapperMoq; 

    Establish context = () =>
    {
        WebClientWrapperMoq = new Mock<IWebClientWrapper>();
        JsonWebServiceClassProvider = new JsonWebServiceClassProvider(WebClientWrapperMoq.Object);
    };
}

And I updated the test class:

[Subject("JsonWebServiceClassProvider")]
public class When_ask_data_with_a_valid_Json_Service_Url : JsonWebServiceClassProviderSpecs
{
    private const string ValidJson = "{'Nome':'Rogerio','Idade':'51'}";
    protected static FakeClassFromJson ResultClass;

    Establish context = () =>
    {
        WebClientWrapperMoq
            .Setup(w => w.DownloadStringAsync(Moq.It.IsAny<string>()))
            .Returns(Task.FromResult(ValidJson));
    };

    Because of = () => ResultClass = JsonWebServiceClassProvider
        .GetData<FakeClassFromJson>(Moq.It.IsAny<string>())
        .Await();

    It should_return_a_class_of_same_type = () => ResultClass.ShouldBeOfType<FakeClassFromJson>();
}

回答1:


This is a slimmed-down version of your spec that works. No NullReferenceException to be seen. Note:

  • The It doesn't check the type of the AwaitResult but rather gets the wrapped Task.Result
  • I don't pass Moq.It<string>.Any... in the Because, that's too much noise. If the parameter is ignored, use a value that communicates that fact.

(Just some text such that the code block below is formatted correctly.)

using System.Diagnostics;
using System.Threading.Tasks;

using Machine.Specifications;

using Moq;

using YourApp;

using It = Machine.Specifications.It;

namespace YourApp
{
  class Foo
  {
  }

  public interface IWebClientWrapper
  {
    Task<string> DownloadStringAsync(string url);
  }

  public class JsonWebServiceClassProvider
  {
    readonly IWebClientWrapper _webClientWrapper;

    public JsonWebServiceClassProvider(IWebClientWrapper webClientWrapper)
    {
      _webClientWrapper = webClientWrapper;
    }

    public async Task<T> GetData<T>(string url) where T : class, new()
    {
      string jsonData = await _webClientWrapper.DownloadStringAsync(url);
      Debug.Assert(jsonData != null);
      return new T();
    }
  }
}

namespace Specs
{
  public class When_calling_GetData_with_a_valid_Json_Service_Url
  {
    const string ValidJson = "{'Nome':'Rogerio','Idade':'51'}";
    static JsonWebServiceClassProvider JsonWebServiceClassProvider;
    static Mock<IWebClientWrapper> Wrapper;
    static AwaitResult<Foo> Result;

    Establish context = () =>
    {
      Wrapper = new Mock<IWebClientWrapper>();
      Wrapper.Setup(w => w.DownloadStringAsync(Moq.It.IsAny<string>()))
             .Returns(Task.FromResult(ValidJson));

      JsonWebServiceClassProvider = new JsonWebServiceClassProvider(Wrapper.Object);
    };

    Because of = () => Result = JsonWebServiceClassProvider.GetData<Foo>("ignored").Await();

    It should_return_a_class_of_same_type = () => Result.AsTask.Result.ShouldBeOfType<Foo>();
  }
}



回答2:


I'm using this approach with IoC:

1) Creating task runner interface and implementation

public interface ITaskRunner
{
    Task<TNewResult> Execute<TResult, TNewResult>(Func<TResult> action, Func<Task<TResult>, TNewResult> continueWith);
}

public class TaskRunner : ITaskRunner
{
    public Task<TNewResult> Execute<TResult, TNewResult>(Func<TResult> action, Func<Task<TResult>, TNewResult> continueWith)
    {
        return Task.Factory.StartNew(action).ContinueWith(continueWith);
    }
}

and usage:

public Task<JsonResult> CheckForOnline(Int64? adId)
    {
        ITaskRunner taskRunner = IoCFactory.Instance.TryResolve<ITaskRunner>();
        return taskRunner.Execute(() => CheckForOnlineFunc(adId),
            r => Json(r.Result, JsonRequestBehavior.AllowGet));
    }

2) Creating Fake implementation which runs the func in Sync mode (no async calls)

internal class FakeTaskRunner : ITaskRunner
{
    public Task<TNewResult> Execute<TResult, TNewResult>(Func<TResult> action, Func<Task<TResult>, TNewResult> continueWith)
    {
        Task<TResult> task = new Task<TResult>(action);
        try
        {
            task.RunSynchronously();
            if (task.Exception != null)
                throw task.Exception;
            return task.ContinueWith(continueWith);
        }
        catch (Exception ex)
        {
            throw ((AggregateException)ex).InnerExceptions[0];
        }
    }
}

So, same code would run async in live version and async in test. Just make sure to configure IoC to use normal TaskRunner in live and FakeTaskRunner in tests.



来源:https://stackoverflow.com/questions/17503287/why-do-i-get-a-nullreferenceexception-when-testing-this-async-method-with-mspec

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!