Aspnetcore.docs: ActionResult<T> controller method example

Created on 17 Aug 2018  Â·  24Comments  Â·  Source: dotnet/AspNetCore.Docs

How do I test a controller method returning ActionResult<T> or Task<ActionResult<T>> in 2.1?

Thanks


Document Details

⚠ Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.

P2 Source - Docs.ms

Most helpful comment

_Yes!_ Now, we're cook'in. This works ...

// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnValue.Ideas.Count());
Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);

@guardrex your solution just saved me from hours of struggle. Thanks :)

All 24 comments

Hello @mberginbh ... I've updated the sample tracking update issue to see what we can do about the gap in coverage when this topic and the sample app are addressed on that issue. In the meantime (and in addition to my remarks below :point_down:), I recommend that you engage with the devs on a support forum, such as Stack Overflow, support chat, such as Slack or Gitter, and access blogs on the subject until we look at how to cover it.

RE: Integration Tests

The integration tests topic was recently updated to use the latest testing approach offered by the engineers, WebApplicationFactory and the Microsoft.AspNetCore.Mvc.Testing package. Although we don't cover it at the moment, you should be able to deserialize the response content into a type that you can assert against.

RE: Unit Tests

I created some _RexHacks_:tm: :smile: just so I could play with it for a sec.

I put an action in this test app (converted to netcoreapp2.1 so that ActionResult<T> is available). I just have it return a new BrainstormSession ...

public ActionResult<BrainstormSession> Test()
{
    var session = new BrainstormSession()
    {
        DateCreated = DateTimeOffset.Now,
        Name = "MY SESSION"
    };

    return session;
}

I set up a test on that action, and all I did there is check the type and then read the session name. It passed.

// Act
var result = controller.Test();

// Assert
var brainStormSession = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.Equal("MY SESSION", brainStormSession.Value.Name);

@scottaddie Do u have any additional tips for testing ActionResult<T>-based actions?

@guardrex The sample you provided above reflects what I would've recommended.

Ty @scottaddie I'll get something along those lines surfaced when I perform the sample upgrade on #5495.

Thank you both. Might be worth adding an example unit test for a failure case e.g. BadRequestResult too.

@mberginbh As far as I can tell, a BadRequestResult won't be returned ... the ActionResult<T> resolves to NotFoundObjectResult when the controller is executed and returns a NotFound on whatever non-existent ID is given to it. What we'll do is have the engineers (who really know how to write software :smile:) tell me if the test I'm setting up is sane.

Regarding the Task<ActionResult<T>> test, which was your original suggestion :point_up:, that seems fine the way I have it set up. I think the engineers will be ok with it, but we'll find out soon enough.

I'll have this PR submitted soon ... I still need to update the topic text for the action and tests and make sure the 2.1 updates are all good. I'll ping u on the PR.

Do unit tests really change that much when using

ActionResult<T>

instead of

IActionResult

?

Doesn't

// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);

just become

// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result.Result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);

?

Or am i doing it wrong?

@pascalpfeil

Those would be fine for integration tests. This topic focuses on unit tests. In a unit test, I'd like to confirm the following ...

  1. Is the return type an ActionResult?
  2. Is the ActionResult's value the correct type?
  3. Is the first value of that returned object the correct?

... then when the ActionResult comes back with no objects, I want to know ...

  1. (Same as before ...) Is the return type an ActionResult?
  2. Is the ActionResult a NotFoundObjectResult?

Take a look at the tests I did for the PR: https://github.com/aspnet/Docs/blob/74ba2ecf00796a1ac7589320a8fc5ddf1e053308/aspnetcore/mvc/controllers/testing/sample/TestingControllersSample/tests/TestingControllersSample.Tests/UnitTests/ApiIdeasControllerTests.cs#L134-L170

@guardrex thank you for your response. That cleared things up.

But how would i test the following code?

public async Task<ActionResult<Entity>> PostEntity(/* ... */)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);
    try
    {
        var entity = await _repo.CreateEntity(/* ... */);
        return CreatedAtAction("GetEntity", new { id = entity.Id }, entity);
    }
    /* ... */
}

My Test with Microsoft.VisualStudio.TestTools.UnitTesting looks something like this:

[TestMethod]
public async Task TestPostEntity_OK()
{
    //Arrange
    var entity= new Entity { /* ... */ };
    mockRepo.Setup(repo=> repo.CreateEntity(/* ...*/)).Returns(Task.FromResult(entity));

    //Act
    var result = await sutController.PostEntity(/* ...*/);

    //Assert
    //This works
    Assert.IsInstanceOfType(result, typeof(ActionResult<Entity>));

    //This fails, entity is null
    Assert.AreEqual(entity, result.Value);
}

As you can see, the result.Value property stays null, while your actionResult.Value in line 148 is populated.

Not sure ... what is result.Result when result.Value is null?

It is a CreatedAtActionResult whose object Value property is entity. This must be because i do return CreatedAtAction(/* ... */) instead of return entity, right? What's the right way to unit test this?

I understand you want to return a 201 with your POST. A CreatedAtActionResult is something that we may need to add to the examples. Thus far, the example just returns a list of IdeoDTO objects ...

public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(int sessionId)
{
    var session = await _sessionRepository.GetByIdAsync(sessionId);

    if (session == null)
    {
        return NotFound(sessionId);
    }

    var result = session.Ideas.Select(idea => new IdeaDTO()
    {
        Id = idea.Id,
        Name = idea.Name,
        Description = idea.Description,
        DateCreated = idea.DateCreated
    }).ToList();

    return result;
}

I need to step out for a couple of hours, but I'll be back later today. I'll see what I can do with the sample app (and PR) and then report back to you. I'm new to ActionResult<T> and testing ActionResult<T> myself, so I'll need to work through it and see what's what. I'll get back to you in a couple of hours.

Thank you very much for your time!

@pascalpfeil I think this worked out fairly well so far. I created a method to receive a POST, lookup a session, add a new idea to it, and then return the session. It's nearly a copy of the existing method (Create) that was already in the topic sample ...

[HttpPost("createactionresult")]
[ProducesResponseType(201)]
public async Task<ActionResult<BrainstormSession>> CreateActionResult([FromBody]NewIdeaModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var session = await _sessionRepository.GetByIdAsync(model.SessionId);
    if (session == null)
    {
        return NotFound(model.SessionId);
    }

    var idea = new Idea()
    {
        DateCreated = DateTimeOffset.Now,
        Description = model.Description,
        Name = model.Name
    };
    session.AddIdea(idea);

    await _sessionRepository.UpdateAsync(session);

    return session;
}

Then, I based three tests on it ...

[Fact]
public async Task CreateActionResult_ReturnsNewlyCreatedIdeaForSession()
{
    // Arrange
    int testSessionId = 123;
    string testName = "test name";
    string testDescription = "test description";
    var testSession = GetTestSession();
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .Returns(Task.FromResult(testSession));
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = testSessionId
    };
    mockRepo.Setup(repo => repo.UpdateAsync(testSession))
        .Returns(Task.CompletedTask)
        .Verifiable();

    // Act
    var result = await controller.CreateActionResult(newIdea);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    var returnValue = Assert.IsType<BrainstormSession>(actionResult.Value);
    mockRepo.Verify();
    Assert.Equal(2, returnValue.Ideas.Count());
    Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
    Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}

[Fact]
public async Task CreateActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
    // Arrange
    var nonExistentSessionId = 999;
    string testName = "test name";
    string testDescription = "test description";
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = nonExistentSessionId
    };

    // Act
    var result = await controller.CreateActionResult(newIdea);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}

[Fact]
public async Task CreateActionResult_ReturnsBadRequest_GivenInvalidModel()
{
    // Arrange & Act
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    controller.ModelState.AddModelError("error", "some error");

    // Act
    var result = await controller.CreateActionResult(model: null);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    Assert.IsType<BadRequestObjectResult>(actionResult.Result);
}

... and those tests pass.

Before going any further and adding text to the topic in the PR, I'd like to wait until @scottaddie gets back (he's OOF for a bit) so that we can discuss further the 200/201/400 situation.

I added the ProducesResponseType decorations ...

[ProducesResponseType(200)]

... and ...

[ProducesResponseType(201)]

The question is are those obtainable in the tests when the methods fully execute and return a valid result?

The ActionResult<T>.Result only appears to receive a value when the method returns a non-200/201.

The tests with the failure assertions get the right types checked with ...

Assert.IsType<NotFoundObjectResult>(actionResult.Result);

... and ...

Assert.IsType<BadRequestObjectResult>(actionResult.Result);

... but I am unclear on when ActionResult<T>.Value receives a value and the result is a 200/201 how to test for a 200/201 status code (e.g., ControllerBase.Ok).

I don't think

[HttpPost("createactionresult")]
[ProducesResponseType(201)]
public async Task<ActionResult<BrainstormSession>> CreateActionResult([FromBody]NewIdeaModel model)
{
    /* ... */
    return session;
}

will actually return 201 but will return 200.

As shown here you'd have to return CreatedAtAction(/* ... */) to get a 201. Or am i wrong?

Yes, I copied the original test too closely. I'll mirror the one for async ActionResult<T> that Scott placed here ...

https://docs.microsoft.com/aspnet/core/web-api/action-return-types#asynchronous-action-1

CreatedAtAction did produce the desired ActionResult<T>.Result of CreatedAtActionResult and ...

Assert.IsType<CreatedAtActionResult>(actionResult.Result);

... passes. However, now ActionResult<T>.Value is null. Again, .Value and .Result never seem to be populated simultaneously.

@guardrex
Yes, this is what i oberserved, too. So in unit tests, you would just check the CreatedAtActionResult.Value property, right?

_Yes!_ Now, we're cook'in. This works ...

// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnValue.Ideas.Count());
Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);

... and now, I can set the other response types on the method ...

[ProducesResponseType(400)]
[ProducesResponseType(404)]

... and check for the status codes. For example for Not Found ...

// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
var createdAtActionResult = Assert.IsType<NotFoundObjectResult>(actionResult.Result);
Assert.Equal(404, createdAtActionResult.StatusCode);

hannibalsmith

_"I love it when a plan comes together!"_ - John "Hannibal" Smith (George Peppard), The A-Team, ©1983-87 Universal Television Stephen J. Cannell

Shouldn't it be

// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
var notFoundObjectResult= Assert.IsType<NotFoundObjectResult>(actionResult.Result);
Assert.Equal(404, notFoundObjectResult.StatusCode);

?

And aren't those two lines redundant?

var createdAtActionResult = Assert.IsType<NotFoundObjectResult>(actionResult.Result);
Assert.Equal(404, createdAtActionResult.StatusCode);

Also nice meme :ok_hand:

aren't those two lines redundant?

Probably ... I just wanted to see the status code. There are two approaches, and the first one I had is better (shorter) ...

Assert.IsType<NotFoundObjectResult>(actionResult.Result);

I note that I didn't have to have the ProducesResponseTypes to get those to pass either. Anyway, I'll leave the ProducesResponseTypes on the method.

Still wish I could get an OkObjectResult out of the return ...

// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);

Assert.IsType<OkObjectResult>(actionResult.Result);

var returnValue = Assert.IsType<List<IdeaDTO>>(actionResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);

ActonResult<T>.Result is null, so that doesn't work.

[EDIT] Just tacking on to the discussion here that the engineer said that OkObjectResult can't be tested under this scenario where the method succeeds and returns the list. In this case, we'll just check the type and a test item. If the test succeeds, we assume a 200 Ok is the HTTP response.

_Yes!_ Now, we're cook'in. This works ...

// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnValue.Ideas.Count());
Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);

@guardrex your solution just saved me from hours of struggle. Thanks :)

Still wish I could get an OkObjectResult out of the return ...

// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);

Assert.IsType<OkObjectResult>(actionResult.Result);

var returnValue = Assert.IsType<List<IdeaDTO>>(actionResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);

ActonResult<T>.Result is null, so that doesn't work.

[EDIT] Just tacking on to the discussion here that the engineer said that OkObjectResult can't be tested under this scenario where the method succeeds and returns the list. In this case, we'll just check the type and a test item. If the test succeeds, we assume a _200 Ok_ is the HTTP response.

I was able to gain access to the OkObjectResult.Value in .NET Core 2.2 using xUnit. Not sure which testing framework you are using or if that matters here. Anyways, I was having trouble accessing the returned IEnumerable<T> until I set var okObjectResult = Assert.IsType<OkObjectResult>(...).

The below is an example of a working test that accesses the value of the OkObjectResult:

// Arrange
var controller = new AircraftStatusController(_context);

// Act
var actionResult = await controller.GetAircraftStatuses();

// Assert
Assert.IsType<ActionResult<IEnumerable<ObstAircraftStatus>>>(actionResult);
var okObjectResult = Assert.IsType<OkObjectResult>(actionResult.Result);
var returnValue = Assert.IsAssignableFrom<IEnumerable<ObstAircraftStatus>>(okObjectResult.Value).ToList();
Assert.NotEmpty(returnValue);

// Included this for fun...
Assert.True(returnValue.Count == _context.ObstAircraftStatus.ToList().Count);

I found some tests I wrote in a .NET Core 1.1 project that also make use of the OkObjectResult.Value. Dont judge, it was early in my testing education :)

Other example of accessing this value:

// Arrange
var controller = _fixture.GetAircraftController(true);

// Act
var objectResult = controller.GetAll().Result as OkObjectResult;
var enumerable = objectResult?.Value as IEnumerable<SharedModels.Aircraft>;
var result = enumerable?.ToList();

// Assert
Assert.NotNull(result);
Assert.True(_fixture.AircraftCdi.Any());
Assert.True(result.Count == _fixture.Aircraft.Count);
Was this page helpful?
0 / 5 - 0 ratings