Unit testing controller methods which return IActi

2020-02-04 06:53发布

I'm in the process of building an ASP.NET Core WebAPI and I'm attempting to write unit tests for the controllers. Most examples I've found are from the older WebAPI/WebAPI2 platforms and don't seem to correlate with the new Core controllers.

My controller methods are returning IActionResults. However, the IActionResult object only has a ExecuteResultAsync() method which requires a controller context. I'm instantiating the controller manually, so the controller context in this instance is null, which causes an exception when calling ExecuteResultAsync. Essentially this is leading me down a very hacky path to get these unit tests to successfully complete and is very messy. I'm left wondering that there must be a more simple/correct way of testing API controllers.

Also, my controllers are NOT using async/await if that makes a difference.

Simple example of what I'm trying to achieve:

Controller method:

[HttpGet(Name = "GetOrdersRoute")]
public IActionResult GetOrders([FromQuery]int page = 0)
{
     try
     {
        var query = _repository.GetAll().ToList();

        int totalCount = query.Count;
        int totalPages = (int)Math.Ceiling((double)totalCount / pageSize) - 1;
        var orders = query.Skip(pageSize * page).Take(pageSize);

        return Ok(new
        {
           TotalCount = totalCount,
           TotalPages = totalPages,

           Orders = orders
        });
     }
     catch (Exception ex)
     {
        return BadRequest(ex);
     }
}

Unit test:

[Fact]
public void GetOrders_WithOrdersInRepo_ReturnsOk()
{
     // arrange
     var controller = new OrdersController(new MockRepository());

     // act
     IActionResult result = controller.GetOrders();

     // assert
     Assert.Equal(HttpStatusCode.OK, ????);
}

4条回答
干净又极端
2楼-- · 2020-02-04 07:06

Assuming something like the

public IActionResult GetOrders() {
    var orders = repository.All();
    return Ok(orders);
}

the controller in this case is returning an OkObjectResult class.

Cast the result to the type of what you are returning in the method and perform your assert on that

[Fact]
public void GetOrders_WithOrdersInRepo_ReturnsOk() {
    // arrange
    var controller = new OrdersController(new MockRepository());

    // act
    var result = controller.GetOrders();
    var okResult = result as OkObjectResult;

    // assert
    Assert.IsNotNull(okResult);
    Assert.AreEqual(200, okResult.StatusCode);
}
查看更多
Root(大扎)
3楼-- · 2020-02-04 07:09

You also can use ActionResult class as a controller result (assuming you have type Orders). In that case you can use something like this:

[ProducesResponseType(typeof(Orders), StatusCodes.Status200OK)]
public ActionResult<Orders> GetOrders()
{
    return service.GetOrders();
}

and now in unit tests you have:

Assert.IsInstanceOf<Orders>(result.Value);

Besides, this is the recommendation of Microsoft - https://docs.microsoft.com/en-us/aspnet/core/web-api/action-return-types?view=aspnetcore-2.2#actionresultt-type

Unfortunately, I don't know why using Ok method

return Ok(service.GetOrders());

doesn't map it properly.

查看更多
▲ chillily
4楼-- · 2020-02-04 07:13

I noticed that response from the server sometimes can't be cast to ObjectResult. It could be NotFoundResult, ForbidResult, NoContentResult, OkResult, CreateResult and etc.. So you should return NotFoundObjectResult, OkObjectResult and etc instead. If it's not possible and you have mixed return types, but you are sure, that all of them have StatusCode property, then you can use dynamic type, and get rid of type cast cheking. I recommend using this hack only in test classes.

For example, if controller looks like this:

public class SampleController : ControllerBase
{
    public async Task<IActionResult> FooAsync(int? id)
    {
        if (id == 0)
        {
            // returned "NotFoundResult"
            return NotFound();
        }

        if (id == 1)
        {
            // returned "StatusCodeResult"
            return StatusCode(StatusCodes.Status415UnsupportedMediaType);
        }

        // returned "OkObjectResult"
        return new OkObjectResult("some message");
    }
}

Your method would be (using xUnit):

[Theory]
[InlineData(0, StatusCodes.Status404NotFound)]
[InlineData(1, StatusCodes.Status415UnsupportedMediaType)]
[InlineData(2, StatusCodes.Status200OK)]
public async Task Foo_ResponseTest(int id, int expectedCode)
{
    dynamic actionResult = await new SampleController().FooAsync(id);
    Assert.Equal(expectedCode, (int)actionResult.StatusCode);
}
查看更多
ゆ 、 Hurt°
5楼-- · 2020-02-04 07:17

You can also do cool things like:

    var result = await controller.GetOrders();//
    var okResult = result as ObjectResult;

    // assert
    Assert.NotNull(okResult);
    Assert.True(okResult is OkObjectResult);
    Assert.IsType<TheTypeYouAreExpecting>(okResult.Value);
    Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode);

Thanks

查看更多
登录 后发表回答