Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit test is failing when ContinueWith is used with System.Threading.Tasks.Task

I am trying to add unit-tests for my code where I am using Task from TPL to update values into a database. For unit-test, I am using NUnit and Moq. Here are some code snippets from my project.

*//service*
public interface IServiceFacade{
   Task SynchronizeDataset (string datasetName);
}

*//The method call I want to test*
_ServiceFacade.SynchronizeDataset(DATASET_NAME);

*//In my test, I want to verify if this method is called*
mock_IServicesFacade.Setup(sf => sf.SynchronizeDataset(It.IsAny<string>())).Returns(It.IsAny<Task>());
presenter.InitializeView();
mock_IServicesFacade.Verify(sf => sf.SynchronizeDataset(NSUserUtilStrings.DATASET_ACHIEVEMENT), Times.Once());

This is working. But when I add ContinueWith with the service method call like this...

_ServiceFacade.SynchronizeDataset(DATASET_NAME).ContinueWith(t =>
{
    if (t.IsFaulted)
    {
        //do something
    }
});

this test code is not working.Test is failed and it shows this error...

System.NullReferenceException : Object reference not set to an instance of an object

Stacktrace:

atPresenters.UnitTests.DeviceCategoryPresenterTest.InitializeView_Called () [0x00241] in DeviceCategoryPresenterTest.cs:56 at (wrapper managed-to-native) System.Reflection.MonoMethod:InternalInvoke (System.Reflection.MonoMethod,object,object[],System.Exception&) at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00038] in /private/tmp/source-mono-4.8.0/bockbuild-mono-4.8.0-branch/profiles/mono-mac-xamarin/build-root/mono-x86/mcs/class/corlib/System.Reflection/MonoMethod.cs:305

and I am not sure how can I fix it. Please help. Thanks in advance.

like image 816
Md. Tahmid Mozaffar Avatar asked Feb 22 '18 05:02

Md. Tahmid Mozaffar


People also ask

What does calling Task ContinueWith() do?

ContinueWith(Action<Task,Object>, Object, TaskScheduler)Creates a continuation that receives caller-supplied state information and executes asynchronously when the target Task completes.

What is ContinueWith c#?

The ContinueWith function is a method available on the task that allows executing code after the task has finished execution. In simple words it allows continuation. Things to note here is that ContinueWith also returns one Task. That means you can attach ContinueWith one task returned by this method.

What is the unit testing Task?

Unit testing is a software development process in which the smallest testable parts of an application, called units, are individually and independently scrutinized for proper operation. This testing methodology is done during the development process by the software developers and sometimes QA staff.

Why unit testing is not enough?

Unit tests do not check the integration between all the functional components of the application. They are, in fact, furthest from the end user experience. Today, you need to test the application end-to-end to drive quality.


2 Answers

In your setup you set up the function to return null. You already stated this in a comment, It.IsAny<Task>() returns null.

Setup(sf => sf.SynchronizeDataset(It.IsAny<string>()))
    .Returns(It.IsAny<Task>());

So if we break this down:

_ServiceFacade.SynchronizeDataset(DATASET_NAME).ContinueWith(t =>
           {
               if (t.IsFaulted)
               {
                  //do something
               }
           });

... equals

// This works, but returns null, so testing anything from this point is limited.
var myNullTask = _ServiceFacade.SynchronizeDataset(DATASET_NAME);

myNullTask.ContinueWith(t => ... ); // This yields NullReferenceException
((Task)null).ContinueWith(t => ... ); // Equivalent to line above

Seems like you are writing an integration test which does not apply to your code (if your actual code does assume non-null as return). If this is the case, I suggest changing your setup to something like:

Setup(sf => sf.SynchronizeDataset(It.IsAny<string>()))
    .Returns(Task.CompletedTask);
like image 37
Tewr Avatar answered Oct 26 '22 11:10

Tewr


The fact here is that you are skipping your continuation by passing the valid task instead of It.IsAny<Task>. The one example is to do something like this

.NET < v4.6

mock_IServicesFacade
    .Setup(sf => sf.SynchronizeDataset(It.IsAny<string>()))
    .Returns(Task.FromResult(true)))

.NET >= v4.6

mock_IServicesFacade
    .Setup(sf => sf.SynchronizeDataset(It.IsAny<string>()))
    .Returns(Task.CompletedTask))

You can even try to make your continuation with option TaskContinuationOptions.OnlyOnFaulted because you are only interested in IsFaulted scenario.

Be aware that you are not testing continuation part only skipping it. If you really want to test\verify continuation part be careful with it. It seems that your logic is service side logic so there TaskScheduler will use default SynchronizationContext and schedule continuation on the ThreadPool thread. Of course this is executed within unit test runner context which is the same. Basically your tests could finish even before continuation task is executed.

like image 79
Johnny Avatar answered Oct 26 '22 13:10

Johnny