Building unit tests for MVC2 AsyncControllers

2019-04-05 15:11发布

问题:

I'm considering re-rewriting some of my MVC controllers to be async controllers. I have working unit tests for these controllers, but I'm trying to understand how to maintain them in an async controller environment.

For example, currently I have an action like this:

public ContentResult Transaction()
{
    do stuff...
    return Content("result");
}

and my unit test basically looks like:

var result = controller.Transaction();
Assert.AreEqual("result", result.Content);

Ok, that's easy enough.

But when your controller changes to look like this:

public void TransactionAsync()
{
    do stuff...
    AsyncManager.Parameters["result"] = "result";
}

public ContentResult TransactionCompleted(string result)
{
    return Content(result);
}

How do you suppose your unit tests should be built? You can of course invoke the async initiator method in your test method, but how do you get at the return value?

I haven't seen anything about this on Google...

Thanks for any ideas.

回答1:

As with any async code, unit testing needs to be aware of thread signalling. .NET includes a type called AutoResetEvent which can block the test thread until an async operation has been completed:

public class MyAsyncController : Controller
{
  public void TransactionAsync()
  {
    AsyncManager.Parameters["result"] = "result";
  }

  public ContentResult TransactionCompleted(string result)
  {
    return Content(result);
  }
}

[TestFixture]
public class MyAsyncControllerTests
{
  #region Fields
  private AutoResetEvent trigger;
  private MyAsyncController controller;
  #endregion

  #region Tests
  [Test]
  public void TestTransactionAsync()
  {
    controller = new MyAsyncController();
    trigger = new AutoResetEvent(false);

    // When the async manager has finished processing an async operation, trigger our AutoResetEvent to proceed.
    controller.AsyncManager.Finished += (sender, ev) => trigger.Set();

    controller.TransactionAsync();
    trigger.WaitOne()

    // Continue with asserts
  }
  #endregion
}

Hope that helps :)



回答2:

I've written short AsyncController extension method that simplifies unit testing a bit.

static class AsyncControllerExtensions
{
    public static void ExecuteAsync(this AsyncController asyncController, Action actionAsync, Action actionCompleted)
    {
        var trigger = new AutoResetEvent(false);
        asyncController.AsyncManager.Finished += (sender, ev) =>
        {
            actionCompleted();
            trigger.Set();
        };
        actionAsync();
        trigger.WaitOne();
    }
}

That way we can simply hide threading 'noise':

public class SampleAsyncController : AsyncController
{
    public void SquareOfAsync(int number)
    {
        AsyncManager.OutstandingOperations.Increment();

        // here goes asynchronous operation
        new Thread(() =>
        {
            Thread.Sleep(100);

            // do some async long operation like ... 
            // calculate square number
            AsyncManager.Parameters["result"] = number * number;

            // decrementing OutstandingOperations to value 0 
            // will execute Finished EventHandler on AsyncManager
            AsyncManager.OutstandingOperations.Decrement();
        }).Start();
    }

    public JsonResult SquareOfCompleted(int result)
    {
        return Json(result);
    }
}

[TestFixture]
public class SampleAsyncControllerTests
{
    [Test]
    public void When_calling_square_of_it_should_return_square_number_of_input()
    {
        var controller = new SampleAsyncController();
        var result = new JsonResult();
        const int number = 5;

        controller.ExecuteAsync(() => controller.SquareOfAsync(number),
                                () => result = controller.SquareOfCompleted((int)controller.AsyncManager.Parameters["result"]));

        Assert.AreEqual((int)(result.Data), number * number);
    }
}

If you want to know more I've written a blog post about how to Unit test ASP.NET MVC 3 asynchronous controllers using Machine.Specifications Or if you want to check this code it's on a github